tangled
alpha
login
or
join now
retr0.id
/
pdsls
forked from
pds.ls/pdsls
1
fork
atom
atproto explorer
1
fork
atom
overview
issues
pulls
pipelines
lexicon schema doc
handle.invalid
4 months ago
5c63396a
461d0ad4
verified
This commit was signed with the committer's
known signature
.
handle.invalid
SSH Key Fingerprint:
SHA256:mBrT4x0JdzLpbVR95g1hjI1aaErfC02kmLRkPXwsYCk=
+531
-19
2 changed files
expand all
collapse all
unified
split
src
components
lexicon-schema.tsx
views
record.tsx
+490
src/components/lexicon-schema.tsx
···
1
1
+
import { Nsid } from "@atcute/lexicons";
2
2
+
import { useLocation, useNavigate } from "@solidjs/router";
3
3
+
import { createEffect, For, Show } from "solid-js";
4
4
+
import { resolveLexiconAuthority } from "../utils/api.js";
5
5
+
6
6
+
interface LexiconSchema {
7
7
+
lexicon: number;
8
8
+
id: string;
9
9
+
description?: string;
10
10
+
defs: {
11
11
+
[key: string]: LexiconDef;
12
12
+
};
13
13
+
}
14
14
+
15
15
+
interface LexiconDef {
16
16
+
type: string;
17
17
+
description?: string;
18
18
+
key?: string;
19
19
+
record?: LexiconObject;
20
20
+
parameters?: LexiconObject;
21
21
+
input?: { encoding: string; schema?: LexiconObject };
22
22
+
output?: { encoding: string; schema?: LexiconObject };
23
23
+
errors?: Array<{ name: string; description?: string }>;
24
24
+
properties?: { [key: string]: LexiconProperty };
25
25
+
required?: string[];
26
26
+
nullable?: string[];
27
27
+
maxLength?: number;
28
28
+
minLength?: number;
29
29
+
items?: LexiconProperty;
30
30
+
refs?: string[];
31
31
+
closed?: boolean;
32
32
+
enum?: string[];
33
33
+
const?: string;
34
34
+
default?: any;
35
35
+
minimum?: number;
36
36
+
maximum?: number;
37
37
+
}
38
38
+
39
39
+
interface LexiconObject {
40
40
+
type: string;
41
41
+
description?: string;
42
42
+
ref?: string;
43
43
+
refs?: string[];
44
44
+
closed?: boolean;
45
45
+
properties?: { [key: string]: LexiconProperty };
46
46
+
required?: string[];
47
47
+
nullable?: string[];
48
48
+
}
49
49
+
50
50
+
interface LexiconProperty {
51
51
+
type: string;
52
52
+
description?: string;
53
53
+
ref?: string;
54
54
+
refs?: string[];
55
55
+
closed?: boolean;
56
56
+
format?: string;
57
57
+
items?: LexiconProperty;
58
58
+
minLength?: number;
59
59
+
maxLength?: number;
60
60
+
maxGraphemes?: number;
61
61
+
minimum?: number;
62
62
+
maximum?: number;
63
63
+
enum?: string[];
64
64
+
const?: string | boolean | number;
65
65
+
default?: any;
66
66
+
knownValues?: string[];
67
67
+
accept?: string[];
68
68
+
maxSize?: number;
69
69
+
}
70
70
+
71
71
+
const TypeBadge = (props: { type: string; format?: string; refType?: string }) => {
72
72
+
const navigate = useNavigate();
73
73
+
const displayType =
74
74
+
props.refType ? props.refType.replace(/^#/, "")
75
75
+
: props.format ? `${props.type}:${props.format}`
76
76
+
: props.type;
77
77
+
78
78
+
const isLocalRef = () => props.refType?.startsWith("#");
79
79
+
const isExternalRef = () => props.refType && !props.refType.startsWith("#");
80
80
+
81
81
+
const handleClick = async (e: MouseEvent) => {
82
82
+
e.preventDefault();
83
83
+
if (isLocalRef()) {
84
84
+
const defName = props.refType!.slice(1);
85
85
+
window.history.replaceState(null, "", `#schema:${defName}`);
86
86
+
const element = document.getElementById(`def-${defName}`);
87
87
+
if (element) {
88
88
+
element.scrollIntoView({ behavior: "instant", block: "start" });
89
89
+
}
90
90
+
} else if (isExternalRef()) {
91
91
+
try {
92
92
+
const [nsid, anchor] = props.refType!.split("#");
93
93
+
const authority = await resolveLexiconAuthority(nsid as Nsid);
94
94
+
95
95
+
const hash = anchor ? `#schema:${anchor}` : "#schema";
96
96
+
navigate(`/at://${authority}/com.atproto.lexicon.schema/${nsid}${hash}`);
97
97
+
} catch (err) {
98
98
+
console.error("Failed to resolve lexicon authority:", err);
99
99
+
}
100
100
+
}
101
101
+
};
102
102
+
103
103
+
return (
104
104
+
<>
105
105
+
<Show when={props.refType}>
106
106
+
<a
107
107
+
href={props.refType}
108
108
+
onClick={handleClick}
109
109
+
class="inline-block rounded bg-blue-100 px-1.5 py-0.5 font-mono text-xs text-blue-800 hover:bg-blue-200 hover:underline active:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:hover:bg-blue-900/50 dark:active:bg-blue-900/50"
110
110
+
>
111
111
+
{displayType}
112
112
+
</a>
113
113
+
</Show>
114
114
+
<Show when={!props.refType}>
115
115
+
<span class="inline-block rounded bg-blue-100 px-1.5 py-0.5 font-mono text-xs text-blue-800 dark:bg-blue-900/30 dark:text-blue-300">
116
116
+
{displayType}
117
117
+
</span>
118
118
+
</Show>
119
119
+
</>
120
120
+
);
121
121
+
};
122
122
+
123
123
+
const UnionBadges = (props: { refs: string[] }) => (
124
124
+
<div class="flex flex-wrap gap-2">
125
125
+
<For each={props.refs}>{(refType) => <TypeBadge type="union" refType={refType} />}</For>
126
126
+
</div>
127
127
+
);
128
128
+
129
129
+
const ConstraintsList = (props: { property: LexiconProperty }) => (
130
130
+
<div class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-neutral-500 dark:text-neutral-400">
131
131
+
<Show when={props.property.minLength !== undefined}>
132
132
+
<span>minLength: {props.property.minLength}</span>
133
133
+
</Show>
134
134
+
<Show when={props.property.maxLength !== undefined}>
135
135
+
<span>maxLength: {props.property.maxLength}</span>
136
136
+
</Show>
137
137
+
<Show when={props.property.maxGraphemes !== undefined}>
138
138
+
<span>maxGraphemes: {props.property.maxGraphemes}</span>
139
139
+
</Show>
140
140
+
<Show when={props.property.minimum !== undefined}>
141
141
+
<span>min: {props.property.minimum}</span>
142
142
+
</Show>
143
143
+
<Show when={props.property.maximum !== undefined}>
144
144
+
<span>max: {props.property.maximum}</span>
145
145
+
</Show>
146
146
+
<Show when={props.property.maxSize !== undefined}>
147
147
+
<span>maxSize: {props.property.maxSize}</span>
148
148
+
</Show>
149
149
+
<Show when={props.property.accept}>
150
150
+
<span>accept: [{props.property.accept!.join(", ")}]</span>
151
151
+
</Show>
152
152
+
<Show when={props.property.enum}>
153
153
+
<span>enum: [{props.property.enum!.join(", ")}]</span>
154
154
+
</Show>
155
155
+
<Show when={props.property.const}>
156
156
+
<span>const: {props.property.const?.toString()}</span>
157
157
+
</Show>
158
158
+
<Show when={props.property.default !== undefined}>
159
159
+
<span>default: {JSON.stringify(props.property.default)}</span>
160
160
+
</Show>
161
161
+
<Show when={props.property.knownValues}>
162
162
+
<span>knownValues: [{props.property.knownValues!.join(", ")}]</span>
163
163
+
</Show>
164
164
+
<Show when={props.property.closed}>
165
165
+
<span>closed: true</span>
166
166
+
</Show>
167
167
+
</div>
168
168
+
);
169
169
+
170
170
+
const PropertyRow = (props: { name: string; property: LexiconProperty; required?: boolean }) => {
171
171
+
const hasConstraints = (property: LexiconProperty) =>
172
172
+
property.minLength !== undefined ||
173
173
+
property.maxLength !== undefined ||
174
174
+
property.maxGraphemes !== undefined ||
175
175
+
property.minimum !== undefined ||
176
176
+
property.maximum !== undefined ||
177
177
+
property.maxSize !== undefined ||
178
178
+
property.accept ||
179
179
+
property.enum ||
180
180
+
property.const ||
181
181
+
property.default !== undefined ||
182
182
+
property.knownValues ||
183
183
+
property.closed;
184
184
+
185
185
+
return (
186
186
+
<div class="flex flex-col gap-2 py-3">
187
187
+
<div class="flex flex-wrap items-center gap-2">
188
188
+
<span class="font-mono text-sm font-semibold">{props.name}</span>
189
189
+
<Show when={!props.property.refs}>
190
190
+
<TypeBadge
191
191
+
type={props.property.type}
192
192
+
format={props.property.format}
193
193
+
refType={props.property.ref}
194
194
+
/>
195
195
+
</Show>
196
196
+
<Show when={props.property.refs}>
197
197
+
<span class="inline-block rounded bg-blue-100 px-1.5 py-0.5 font-mono text-xs text-blue-800 dark:bg-blue-900/30 dark:text-blue-300">
198
198
+
union
199
199
+
</span>
200
200
+
</Show>
201
201
+
<Show when={props.required}>
202
202
+
<span class="text-xs font-semibold text-red-500 dark:text-red-400">required</span>
203
203
+
</Show>
204
204
+
</div>
205
205
+
<Show when={props.property.refs}>
206
206
+
<UnionBadges refs={props.property.refs!} />
207
207
+
</Show>
208
208
+
<Show when={hasConstraints(props.property)}>
209
209
+
<ConstraintsList property={props.property} />
210
210
+
</Show>
211
211
+
<Show when={props.property.items}>
212
212
+
<div class="flex flex-col gap-2">
213
213
+
<div class="flex items-center gap-2 text-xs text-neutral-500 dark:text-neutral-400">
214
214
+
<span class="font-semibold">items:</span>
215
215
+
<Show when={!props.property.items!.refs}>
216
216
+
<TypeBadge
217
217
+
type={props.property.items!.type}
218
218
+
format={props.property.items!.format}
219
219
+
refType={props.property.items!.ref}
220
220
+
/>
221
221
+
</Show>
222
222
+
<Show when={props.property.items!.refs}>
223
223
+
<span class="inline-block rounded bg-blue-100 px-1.5 py-0.5 font-mono text-xs text-blue-800 dark:bg-blue-900/30 dark:text-blue-300">
224
224
+
union
225
225
+
</span>
226
226
+
</Show>
227
227
+
</div>
228
228
+
<Show when={props.property.items!.refs}>
229
229
+
<UnionBadges refs={props.property.items!.refs!} />
230
230
+
</Show>
231
231
+
</div>
232
232
+
</Show>
233
233
+
<Show when={props.property.items && hasConstraints(props.property.items)}>
234
234
+
<ConstraintsList property={props.property.items!} />
235
235
+
</Show>
236
236
+
<Show when={props.property.description}>
237
237
+
<p class="text-sm text-neutral-700 dark:text-neutral-300">{props.property.description}</p>
238
238
+
</Show>
239
239
+
</div>
240
240
+
);
241
241
+
};
242
242
+
243
243
+
const DefSection = (props: { name: string; def: LexiconDef }) => {
244
244
+
const defTypeColor = () => {
245
245
+
switch (props.def.type) {
246
246
+
case "record":
247
247
+
return "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300";
248
248
+
case "query":
249
249
+
return "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300";
250
250
+
case "procedure":
251
251
+
return "bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300";
252
252
+
case "subscription":
253
253
+
return "bg-pink-100 text-pink-800 dark:bg-pink-900/30 dark:text-pink-300";
254
254
+
case "object":
255
255
+
return "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300";
256
256
+
case "token":
257
257
+
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300";
258
258
+
default:
259
259
+
return "bg-neutral-100 text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300";
260
260
+
}
261
261
+
};
262
262
+
263
263
+
const handleHeaderClick = (e: MouseEvent) => {
264
264
+
e.preventDefault();
265
265
+
window.history.replaceState(null, "", `#schema:${props.name}`);
266
266
+
const element = document.getElementById(`def-${props.name}`);
267
267
+
if (element) {
268
268
+
element.scrollIntoView({ behavior: "instant", block: "start" });
269
269
+
}
270
270
+
};
271
271
+
272
272
+
return (
273
273
+
<div class="flex flex-col gap-3" id={`def-${props.name}`}>
274
274
+
<div class="flex items-center gap-2">
275
275
+
<a
276
276
+
href={`#schema:${props.name}`}
277
277
+
onClick={handleHeaderClick}
278
278
+
class="text-lg font-semibold hover:underline"
279
279
+
>
280
280
+
{props.name === "main" ? "Main Definition" : props.name}
281
281
+
</a>
282
282
+
<span class={`rounded px-2 py-0.5 text-xs font-semibold uppercase ${defTypeColor()}`}>
283
283
+
{props.def.type}
284
284
+
</span>
285
285
+
</div>
286
286
+
287
287
+
<Show when={props.def.description}>
288
288
+
<p class="text-sm text-neutral-700 dark:text-neutral-300">{props.def.description}</p>
289
289
+
</Show>
290
290
+
291
291
+
{/* Record key */}
292
292
+
<Show when={props.def.key}>
293
293
+
<div>
294
294
+
<span class="text-sm font-semibold">Record Key: </span>
295
295
+
<span class="font-mono text-sm">{props.def.key}</span>
296
296
+
</div>
297
297
+
</Show>
298
298
+
299
299
+
{/* Properties (for record/object types) */}
300
300
+
<Show when={props.def.properties || props.def.record?.properties}>
301
301
+
<div class="flex flex-col gap-2">
302
302
+
<h4 class="text-sm font-semibold text-neutral-600 uppercase dark:text-neutral-400">
303
303
+
Properties
304
304
+
</h4>
305
305
+
<div class="divide-y divide-neutral-200 rounded-lg border border-neutral-200 bg-neutral-50/50 px-3 dark:divide-neutral-700 dark:border-neutral-700 dark:bg-neutral-800/30">
306
306
+
<For each={Object.entries(props.def.properties || props.def.record?.properties || {})}>
307
307
+
{([name, property]) => (
308
308
+
<PropertyRow
309
309
+
name={name}
310
310
+
property={property}
311
311
+
required={(props.def.required || props.def.record?.required || []).includes(name)}
312
312
+
/>
313
313
+
)}
314
314
+
</For>
315
315
+
</div>
316
316
+
</div>
317
317
+
</Show>
318
318
+
319
319
+
{/* Parameters (for query/procedure) */}
320
320
+
<Show when={props.def.parameters?.properties}>
321
321
+
<div class="flex flex-col gap-2">
322
322
+
<h4 class="text-sm font-semibold text-neutral-600 uppercase dark:text-neutral-400">
323
323
+
Parameters
324
324
+
</h4>
325
325
+
<div class="divide-y divide-neutral-200 rounded-lg border border-neutral-200 bg-neutral-50/50 px-3 dark:divide-neutral-700 dark:border-neutral-700 dark:bg-neutral-800/30">
326
326
+
<For each={Object.entries(props.def.parameters!.properties!)}>
327
327
+
{([name, property]) => (
328
328
+
<PropertyRow
329
329
+
name={name}
330
330
+
property={property}
331
331
+
required={(props.def.parameters?.required || []).includes(name)}
332
332
+
/>
333
333
+
)}
334
334
+
</For>
335
335
+
</div>
336
336
+
</div>
337
337
+
</Show>
338
338
+
339
339
+
{/* Input */}
340
340
+
<Show when={props.def.input}>
341
341
+
<div class="flex flex-col gap-2">
342
342
+
<h4 class="text-sm font-semibold text-neutral-600 uppercase dark:text-neutral-400">
343
343
+
Input
344
344
+
</h4>
345
345
+
<div class="flex flex-col gap-2 rounded-lg border border-neutral-200 bg-neutral-50/50 p-3 dark:border-neutral-700 dark:bg-neutral-800/30">
346
346
+
<div class="text-sm">
347
347
+
<span class="font-semibold">Encoding: </span>
348
348
+
<span class="font-mono">{props.def.input!.encoding}</span>
349
349
+
</div>
350
350
+
<Show when={props.def.input!.schema?.ref}>
351
351
+
<div class="flex items-center gap-2">
352
352
+
<span class="text-sm font-semibold">Schema:</span>
353
353
+
<TypeBadge type="ref" refType={props.def.input!.schema!.ref} />
354
354
+
</div>
355
355
+
</Show>
356
356
+
<Show when={props.def.input!.schema?.refs}>
357
357
+
<div class="flex flex-col gap-2">
358
358
+
<div class="flex items-center gap-2">
359
359
+
<span class="text-sm font-semibold">Schema (union):</span>
360
360
+
</div>
361
361
+
<UnionBadges refs={props.def.input!.schema!.refs!} />
362
362
+
</div>
363
363
+
</Show>
364
364
+
<Show when={props.def.input!.schema?.properties}>
365
365
+
<div class="divide-y divide-neutral-200 rounded-lg border border-neutral-200 bg-neutral-50/50 px-3 dark:divide-neutral-700 dark:border-neutral-700 dark:bg-neutral-800/30">
366
366
+
<For each={Object.entries(props.def.input!.schema!.properties!)}>
367
367
+
{([name, property]) => (
368
368
+
<PropertyRow
369
369
+
name={name}
370
370
+
property={property}
371
371
+
required={(props.def.input!.schema?.required || []).includes(name)}
372
372
+
/>
373
373
+
)}
374
374
+
</For>
375
375
+
</div>
376
376
+
</Show>
377
377
+
</div>
378
378
+
</div>
379
379
+
</Show>
380
380
+
381
381
+
{/* Output */}
382
382
+
<Show when={props.def.output}>
383
383
+
<div class="flex flex-col gap-2">
384
384
+
<h4 class="text-sm font-semibold text-neutral-600 uppercase dark:text-neutral-400">
385
385
+
Output
386
386
+
</h4>
387
387
+
<div class="flex flex-col gap-2 rounded-lg border border-neutral-200 bg-neutral-50/50 p-3 dark:border-neutral-700 dark:bg-neutral-800/30">
388
388
+
<div class="text-sm">
389
389
+
<span class="font-semibold">Encoding: </span>
390
390
+
<span class="font-mono">{props.def.output!.encoding}</span>
391
391
+
</div>
392
392
+
<Show when={props.def.output!.schema?.ref}>
393
393
+
<div class="flex items-center gap-2">
394
394
+
<span class="text-sm font-semibold">Schema:</span>
395
395
+
<TypeBadge type="ref" refType={props.def.output!.schema!.ref} />
396
396
+
</div>
397
397
+
</Show>
398
398
+
<Show when={props.def.output!.schema?.refs}>
399
399
+
<div class="flex flex-col gap-2">
400
400
+
<div class="flex items-center gap-2">
401
401
+
<span class="text-sm font-semibold">Schema (union):</span>
402
402
+
</div>
403
403
+
<UnionBadges refs={props.def.output!.schema!.refs!} />
404
404
+
</div>
405
405
+
</Show>
406
406
+
<Show when={props.def.output!.schema?.properties}>
407
407
+
<div class="divide-y divide-neutral-200 rounded-lg border border-neutral-200 bg-neutral-50/50 px-3 dark:divide-neutral-700 dark:border-neutral-700 dark:bg-neutral-800/30">
408
408
+
<For each={Object.entries(props.def.output!.schema!.properties!)}>
409
409
+
{([name, property]) => (
410
410
+
<PropertyRow
411
411
+
name={name}
412
412
+
property={property}
413
413
+
required={(props.def.output!.schema?.required || []).includes(name)}
414
414
+
/>
415
415
+
)}
416
416
+
</For>
417
417
+
</div>
418
418
+
</Show>
419
419
+
</div>
420
420
+
</div>
421
421
+
</Show>
422
422
+
423
423
+
{/* Errors */}
424
424
+
<Show when={props.def.errors && props.def.errors.length > 0}>
425
425
+
<div class="flex flex-col gap-2">
426
426
+
<h4 class="text-sm font-semibold text-neutral-600 uppercase dark:text-neutral-400">
427
427
+
Errors
428
428
+
</h4>
429
429
+
<div class="divide-y divide-neutral-200 rounded-lg border border-neutral-200 bg-neutral-50/50 px-3 dark:divide-neutral-700 dark:border-neutral-700 dark:bg-neutral-800/30">
430
430
+
<For each={props.def.errors}>
431
431
+
{(error) => (
432
432
+
<div class="flex flex-col gap-1 py-2">
433
433
+
<div class="font-mono text-sm font-semibold">{error.name}</div>
434
434
+
<Show when={error.description}>
435
435
+
<p class="text-sm text-neutral-700 dark:text-neutral-300">
436
436
+
{error.description}
437
437
+
</p>
438
438
+
</Show>
439
439
+
</div>
440
440
+
)}
441
441
+
</For>
442
442
+
</div>
443
443
+
</div>
444
444
+
</Show>
445
445
+
</div>
446
446
+
);
447
447
+
};
448
448
+
449
449
+
export const LexiconSchemaView = (props: { schema: LexiconSchema }) => {
450
450
+
const location = useLocation();
451
451
+
452
452
+
// Handle scrolling to a definition when hash is like #schema:definitionName
453
453
+
createEffect(() => {
454
454
+
const hash = location.hash;
455
455
+
if (hash.startsWith("#schema:")) {
456
456
+
const defName = hash.slice(8);
457
457
+
setTimeout(() => {
458
458
+
const element = document.getElementById(`def-${defName}`);
459
459
+
if (element) {
460
460
+
element.scrollIntoView({ behavior: "instant", block: "start" });
461
461
+
}
462
462
+
}, 100);
463
463
+
}
464
464
+
});
465
465
+
466
466
+
return (
467
467
+
<div class="w-full max-w-4xl px-2">
468
468
+
{/* Header */}
469
469
+
<div class="flex flex-col gap-2 border-b border-neutral-300 pb-4 dark:border-neutral-700">
470
470
+
<h2 class="text-lg font-semibold">{props.schema.id}</h2>
471
471
+
<div class="flex gap-4 text-sm text-neutral-600 dark:text-neutral-400">
472
472
+
<span>
473
473
+
<span class="font-semibold">Lexicon version: </span>
474
474
+
<span class="font-mono">{props.schema.lexicon}</span>
475
475
+
</span>
476
476
+
</div>
477
477
+
<Show when={props.schema.description}>
478
478
+
<p class="text-sm text-neutral-700 dark:text-neutral-300">{props.schema.description}</p>
479
479
+
</Show>
480
480
+
</div>
481
481
+
482
482
+
{/* Definitions */}
483
483
+
<div class="flex flex-col gap-6 pt-4">
484
484
+
<For each={Object.entries(props.schema.defs)}>
485
485
+
{([name, def]) => <DefSection name={name} def={def} />}
486
486
+
</For>
487
487
+
</div>
488
488
+
</div>
489
489
+
);
490
490
+
};
+41
-19
src/views/record.tsx
···
8
8
import { RecordEditor, setPlaceholder } from "../components/create.jsx";
9
9
import { CopyMenu, DropdownMenu, MenuProvider, NavMenu } from "../components/dropdown.jsx";
10
10
import { JSONValue } from "../components/json.jsx";
11
11
+
import { LexiconSchemaView } from "../components/lexicon-schema.jsx";
11
12
import { agent } from "../components/login.jsx";
12
13
import { Modal } from "../components/modal.jsx";
13
14
import { pds } from "../components/navbar.jsx";
···
129
130
};
130
131
131
132
const RecordTab = (props: {
132
132
-
tab: "record" | "backlinks" | "info";
133
133
+
tab: "record" | "backlinks" | "info" | "schema";
133
134
label: string;
134
135
error?: boolean;
135
135
-
}) => (
136
136
-
<div class="flex items-center gap-0.5">
137
137
-
<A
138
138
-
classList={{
139
139
-
"flex items-center gap-1 border-b-2": true,
140
140
-
"border-transparent hover:border-neutral-400 dark:hover:border-neutral-600":
141
141
-
(!!location.hash && location.hash !== `#${props.tab}`) ||
142
142
-
(!location.hash && props.tab !== "record"),
143
143
-
}}
144
144
-
href={`/at://${did}/${params.collection}/${params.rkey}#${props.tab}`}
145
145
-
>
146
146
-
{props.label}
147
147
-
</A>
148
148
-
<Show when={props.error && (validRecord() === false || validSchema() === false)}>
149
149
-
<span class="iconify lucide--x text-red-500 dark:text-red-400"></span>
150
150
-
</Show>
151
151
-
</div>
152
152
-
);
136
136
+
}) => {
137
137
+
const isActive = () => {
138
138
+
if (!location.hash && props.tab === "record") return true;
139
139
+
if (location.hash === `#${props.tab}`) return true;
140
140
+
if (props.tab === "schema" && location.hash.startsWith("#schema:")) return true;
141
141
+
return false;
142
142
+
};
143
143
+
144
144
+
return (
145
145
+
<div class="flex items-center gap-0.5">
146
146
+
<A
147
147
+
classList={{
148
148
+
"flex items-center gap-1 border-b-2": true,
149
149
+
"border-transparent hover:border-neutral-400 dark:hover:border-neutral-600":
150
150
+
!isActive(),
151
151
+
}}
152
152
+
href={`/at://${did}/${params.collection}/${params.rkey}#${props.tab}`}
153
153
+
>
154
154
+
{props.label}
155
155
+
</A>
156
156
+
<Show when={props.error && (validRecord() === false || validSchema() === false)}>
157
157
+
<span class="iconify lucide--x text-red-500 dark:text-red-400"></span>
158
158
+
</Show>
159
159
+
</div>
160
160
+
);
161
161
+
};
153
162
154
163
return (
155
164
<Show when={record()} keyed>
···
157
166
<div class="dark:shadow-dark-700 dark:bg-dark-300 mb-3 flex w-full justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm shadow-xs dark:border-neutral-700">
158
167
<div class="flex gap-3">
159
168
<RecordTab tab="record" label="Record" />
169
169
+
<Show when={params.collection === "com.atproto.lexicon.schema"}>
170
170
+
<RecordTab tab="schema" label="Schema" />
171
171
+
</Show>
160
172
<RecordTab tab="backlinks" label="Backlinks" />
161
173
<RecordTab tab="info" label="Info" error />
162
174
</div>
···
224
236
<div class="w-max max-w-screen min-w-full px-4 font-mono text-xs wrap-anywhere whitespace-pre-wrap sm:px-2 sm:text-sm md:max-w-[48rem]">
225
237
<JSONValue data={record()?.value as any} repo={record()!.uri.split("/")[2]} />
226
238
</div>
239
239
+
</Show>
240
240
+
<Show
241
241
+
when={
242
242
+
(location.hash === "#schema" || location.hash.startsWith("#schema:")) &&
243
243
+
params.collection === "com.atproto.lexicon.schema"
244
244
+
}
245
245
+
>
246
246
+
<ErrorBoundary fallback={(err) => <div>Error: {err.message}</div>}>
247
247
+
<LexiconSchemaView schema={record()?.value as any} />
248
248
+
</ErrorBoundary>
227
249
</Show>
228
250
<Show when={location.hash === "#backlinks"}>
229
251
<ErrorBoundary fallback={(err) => <div class="break-words">Error: {err.message}</div>}>