@emeryld/rrroutes-contract 2.7.1 → 2.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,739 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Finalized Leaves Viewer</title>
7
+ <style>
8
+ :root {
9
+ --bg: #f5f7fb;
10
+ --surface: #ffffff;
11
+ --surface-2: #f8faff;
12
+ --border: #d6ddea;
13
+ --text: #172033;
14
+ --muted: #5b6680;
15
+ --accent: #1858c6;
16
+ --ok: #1f8f4e;
17
+ }
18
+
19
+ * {
20
+ box-sizing: border-box;
21
+ }
22
+
23
+ body {
24
+ margin: 0;
25
+ font-family: 'Iosevka Web', 'SFMono-Regular', Menlo, Consolas, monospace;
26
+ color: var(--text);
27
+ background: radial-gradient(circle at top right, #eaf0ff, transparent 45%),
28
+ linear-gradient(180deg, var(--bg), #eef2fa);
29
+ }
30
+
31
+ mark {
32
+ background: #ffec99;
33
+ color: #402f00;
34
+ border-radius: 2px;
35
+ padding: 0 1px;
36
+ }
37
+
38
+ .wrap {
39
+ max-width: 1200px;
40
+ margin: 0 auto;
41
+ padding: 20px;
42
+ }
43
+
44
+ .card {
45
+ background: var(--surface);
46
+ border: 1px solid var(--border);
47
+ border-radius: 12px;
48
+ padding: 14px;
49
+ }
50
+
51
+ .controls {
52
+ display: grid;
53
+ gap: 10px;
54
+ }
55
+
56
+ .field-row {
57
+ display: flex;
58
+ flex-wrap: wrap;
59
+ gap: 8px;
60
+ }
61
+
62
+ .field-item {
63
+ display: inline-flex;
64
+ align-items: center;
65
+ gap: 6px;
66
+ padding: 4px 8px;
67
+ border: 1px solid var(--border);
68
+ border-radius: 8px;
69
+ background: var(--surface-2);
70
+ }
71
+
72
+ input[type='text'] {
73
+ width: 100%;
74
+ border: 1px solid var(--border);
75
+ border-radius: 8px;
76
+ padding: 10px 12px;
77
+ font: inherit;
78
+ }
79
+
80
+ .meta {
81
+ color: var(--muted);
82
+ font-size: 12px;
83
+ }
84
+
85
+ #results {
86
+ margin-top: 14px;
87
+ display: grid;
88
+ gap: 10px;
89
+ }
90
+
91
+ details.leaf {
92
+ background: var(--surface);
93
+ border: 1px solid var(--border);
94
+ border-radius: 10px;
95
+ padding: 8px 10px;
96
+ }
97
+
98
+ summary {
99
+ cursor: pointer;
100
+ font-weight: 700;
101
+ color: var(--accent);
102
+ }
103
+
104
+ .leaf-content {
105
+ display: grid;
106
+ gap: 10px;
107
+ margin-top: 10px;
108
+ }
109
+
110
+ .section {
111
+ border: 1px solid var(--border);
112
+ border-radius: 8px;
113
+ padding: 10px;
114
+ background: #fbfcff;
115
+ }
116
+
117
+ .section h3 {
118
+ margin: 0 0 8px;
119
+ font-size: 13px;
120
+ }
121
+
122
+ .grid-2 {
123
+ display: grid;
124
+ gap: 8px;
125
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
126
+ }
127
+
128
+ .kv {
129
+ border: 1px solid var(--border);
130
+ border-radius: 6px;
131
+ padding: 6px 8px;
132
+ background: white;
133
+ }
134
+
135
+ .kv .k {
136
+ font-size: 11px;
137
+ color: var(--muted);
138
+ }
139
+
140
+ .kv .v {
141
+ margin-top: 2px;
142
+ word-break: break-word;
143
+ }
144
+
145
+ .chips {
146
+ display: flex;
147
+ gap: 6px;
148
+ flex-wrap: wrap;
149
+ }
150
+
151
+ .chip {
152
+ border: 1px solid var(--border);
153
+ border-radius: 999px;
154
+ padding: 2px 8px;
155
+ font-size: 12px;
156
+ background: #fff;
157
+ }
158
+
159
+ .chip.ok {
160
+ border-color: #b5e4c8;
161
+ color: var(--ok);
162
+ background: #effbf4;
163
+ }
164
+
165
+ .empty {
166
+ color: var(--muted);
167
+ }
168
+
169
+ .mono {
170
+ font-family: inherit;
171
+ }
172
+
173
+ .schema-block {
174
+ border: 1px solid var(--border);
175
+ border-radius: 8px;
176
+ padding: 8px;
177
+ margin-bottom: 8px;
178
+ background: white;
179
+ }
180
+
181
+ .schema-block:last-child {
182
+ margin-bottom: 0;
183
+ }
184
+
185
+ .schema-header {
186
+ font-weight: 700;
187
+ margin-bottom: 6px;
188
+ }
189
+
190
+ .schema-tree details {
191
+ margin-left: 12px;
192
+ border-left: 1px dashed var(--border);
193
+ padding-left: 8px;
194
+ }
195
+
196
+ .tree-row {
197
+ display: grid;
198
+ grid-template-columns: 1fr auto auto auto;
199
+ gap: 8px;
200
+ padding: 3px 0;
201
+ align-items: center;
202
+ }
203
+
204
+ .tree-col-muted {
205
+ color: var(--muted);
206
+ font-size: 12px;
207
+ }
208
+
209
+ .tree-pill {
210
+ border: 1px solid var(--border);
211
+ border-radius: 999px;
212
+ padding: 1px 6px;
213
+ font-size: 11px;
214
+ background: #f9fbff;
215
+ }
216
+ </style>
217
+ </head>
218
+ <body>
219
+ <div class="wrap">
220
+ <h1>Finalized Leaves Viewer</h1>
221
+ <div class="card controls">
222
+ <label>
223
+ Load export JSON file:
224
+ <input id="fileInput" type="file" accept="application/json,.json" />
225
+ </label>
226
+
227
+ <label>
228
+ Search text:
229
+ <input id="searchInput" type="text" placeholder="Type to search..." />
230
+ </label>
231
+
232
+ <div class="field-row">
233
+ <label class="field-item">
234
+ <input id="caseSensitive" type="checkbox" />
235
+ <span>case sensitive</span>
236
+ </label>
237
+ <label class="field-item">
238
+ <input id="regexSearch" type="checkbox" />
239
+ <span>regex</span>
240
+ </label>
241
+ </div>
242
+
243
+ <div id="fieldCheckboxes" class="field-row"></div>
244
+
245
+ <div id="status" class="meta">Load a JSON export to begin.</div>
246
+ </div>
247
+
248
+ <div id="results"></div>
249
+ </div>
250
+
251
+ <!--__FINALIZED_LEAVES_BAKED_PAYLOAD__-->
252
+ <script>
253
+ const SEARCH_FIELDS = [
254
+ { id: 'method', label: 'method', get: (leaf) => [leaf.method] },
255
+ { id: 'path', label: 'path', get: (leaf) => [leaf.path] },
256
+ { id: 'key', label: 'key', get: (leaf) => [leaf.key] },
257
+ { id: 'summary', label: 'summary', get: (leaf) => [leaf.cfg?.summary] },
258
+ {
259
+ id: 'description',
260
+ label: 'description',
261
+ get: (leaf) => [leaf.cfg?.description],
262
+ },
263
+ { id: 'docsGroup', label: 'docsGroup', get: (leaf) => [leaf.cfg?.docsGroup] },
264
+ { id: 'tags', label: 'tags', get: (leaf) => leaf.cfg?.tags || [] },
265
+ { id: 'stability', label: 'stability', get: (leaf) => [leaf.cfg?.stability] },
266
+ { id: 'docsMeta', label: 'docsMeta', get: (leaf) => [leaf.cfg?.docsMeta] },
267
+ { id: 'schemas', label: 'schemas', get: (leaf) => [leaf.cfg?.schemas] },
268
+ {
269
+ id: 'flatSchema',
270
+ label: 'flatSchema',
271
+ get: (leaf, schemaFlatByLeaf) => [schemaFlatByLeaf?.[leaf.key]],
272
+ },
273
+ ]
274
+
275
+ const SCHEMA_SECTIONS = ['params', 'query', 'body', 'output']
276
+
277
+ const state = { payload: null, leaves: [] }
278
+
279
+ const fileInput = document.getElementById('fileInput')
280
+ const searchInput = document.getElementById('searchInput')
281
+ const caseSensitiveInput = document.getElementById('caseSensitive')
282
+ const regexSearchInput = document.getElementById('regexSearch')
283
+ const fieldCheckboxes = document.getElementById('fieldCheckboxes')
284
+ const statusEl = document.getElementById('status')
285
+ const resultsEl = document.getElementById('results')
286
+
287
+ function escapeHtml(value) {
288
+ return String(value)
289
+ .replace(/&/g, '&amp;')
290
+ .replace(/</g, '&lt;')
291
+ .replace(/>/g, '&gt;')
292
+ .replace(/"/g, '&quot;')
293
+ .replace(/'/g, '&#39;')
294
+ }
295
+
296
+ function escapeRegExp(value) {
297
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
298
+ }
299
+
300
+ function createSearchEngine(queryRaw, options) {
301
+ const query = queryRaw || ''
302
+ const caseSensitive = Boolean(options.caseSensitive)
303
+ const regex = Boolean(options.regex)
304
+
305
+ if (!query) {
306
+ return {
307
+ active: false,
308
+ error: null,
309
+ test: () => true,
310
+ highlight: (text) => escapeHtml(text ?? ''),
311
+ }
312
+ }
313
+
314
+ if (regex) {
315
+ try {
316
+ const flags = caseSensitive ? 'g' : 'gi'
317
+ const rx = new RegExp(query, flags)
318
+ return {
319
+ active: true,
320
+ error: null,
321
+ test: (text) => {
322
+ const source = String(text ?? '')
323
+ const probe = new RegExp(rx.source, rx.flags)
324
+ return probe.test(source)
325
+ },
326
+ highlight: (text) => {
327
+ const source = String(text ?? '')
328
+ return escapeHtml(source).replace(
329
+ new RegExp(rx.source, rx.flags),
330
+ (m) => `<mark>${m}</mark>`,
331
+ )
332
+ },
333
+ }
334
+ } catch (error) {
335
+ return {
336
+ active: true,
337
+ error: error instanceof Error ? error.message : 'Invalid regex',
338
+ test: () => false,
339
+ highlight: (text) => escapeHtml(text ?? ''),
340
+ }
341
+ }
342
+ }
343
+
344
+ const needle = caseSensitive ? query : query.toLowerCase()
345
+ const flags = caseSensitive ? 'g' : 'gi'
346
+ const safe = escapeRegExp(query)
347
+
348
+ return {
349
+ active: true,
350
+ error: null,
351
+ test: (text) => {
352
+ const source = String(text ?? '')
353
+ const hay = caseSensitive ? source : source.toLowerCase()
354
+ return hay.includes(needle)
355
+ },
356
+ highlight: (text) =>
357
+ escapeHtml(String(text ?? '')).replace(
358
+ new RegExp(safe, flags),
359
+ (m) => `<mark>${m}</mark>`,
360
+ ),
361
+ }
362
+ }
363
+
364
+ function toTokens(value) {
365
+ if (value === null || value === undefined) return []
366
+ if (typeof value === 'string') return [value]
367
+ if (typeof value === 'number' || typeof value === 'boolean') return [String(value)]
368
+ if (Array.isArray(value)) return value.flatMap((item) => toTokens(item))
369
+ if (typeof value === 'object') return [JSON.stringify(value)]
370
+ return []
371
+ }
372
+
373
+ function selectedFieldIds() {
374
+ return SEARCH_FIELDS.filter((field) => {
375
+ const input = document.getElementById(`field-${field.id}`)
376
+ return Boolean(input && input.checked)
377
+ }).map((field) => field.id)
378
+ }
379
+
380
+ function matchesLeaf(leaf, engine, selectedIds) {
381
+ if (!engine.active) return true
382
+ if (selectedIds.length === 0) return false
383
+ const schemaFlatByLeaf = state.payload?.schemaFlatByLeaf || {}
384
+
385
+ return SEARCH_FIELDS.some((field) => {
386
+ if (!selectedIds.includes(field.id)) return false
387
+ const tokens = toTokens(field.get(leaf, schemaFlatByLeaf))
388
+ return tokens.some((token) => engine.test(token))
389
+ })
390
+ }
391
+
392
+ function el(tag, className, text) {
393
+ const node = document.createElement(tag)
394
+ if (className) node.className = className
395
+ if (text !== undefined) node.textContent = text
396
+ return node
397
+ }
398
+
399
+ function setHighlighted(node, text, engine) {
400
+ node.innerHTML = engine.highlight(text ?? '')
401
+ }
402
+
403
+ function kv(key, value, engine) {
404
+ const box = el('div', 'kv')
405
+ box.appendChild(el('div', 'k', key))
406
+ const valueNode = el('div', 'v mono')
407
+ setHighlighted(valueNode, value === undefined ? '—' : String(value), engine)
408
+ box.appendChild(valueNode)
409
+ return box
410
+ }
411
+
412
+ function renderSchemaSummary(name, schema, engine) {
413
+ const row = el('div', 'kv')
414
+ row.appendChild(el('div', 'k', name))
415
+
416
+ const valueNode = el('div', 'v mono')
417
+ if (!schema) {
418
+ setHighlighted(valueNode, 'not defined', engine)
419
+ row.appendChild(valueNode)
420
+ return row
421
+ }
422
+
423
+ const parts = [schema.kind]
424
+ if (schema.optional) parts.push('optional')
425
+ if (schema.nullable) parts.push('nullable')
426
+ if (Array.isArray(schema.enumValues) && schema.enumValues.length > 0) {
427
+ parts.push(`enum: ${schema.enumValues.join('|')}`)
428
+ }
429
+ if (schema.properties) {
430
+ parts.push(`properties: ${Object.keys(schema.properties).length}`)
431
+ }
432
+
433
+ setHighlighted(valueNode, parts.join(' | '), engine)
434
+ row.appendChild(valueNode)
435
+ return row
436
+ }
437
+
438
+ function splitFlatSchemaBySection(flatSchema) {
439
+ const result = {
440
+ params: {},
441
+ query: {},
442
+ body: {},
443
+ output: {},
444
+ }
445
+
446
+ if (!flatSchema) return result
447
+
448
+ Object.entries(flatSchema).forEach(([path, info]) => {
449
+ const section = SCHEMA_SECTIONS.find(
450
+ (name) => path === name || path.startsWith(`${name}.`),
451
+ )
452
+ if (!section) return
453
+ result[section][path] = info
454
+ })
455
+
456
+ return result
457
+ }
458
+
459
+ function createTreeNode(name = '') {
460
+ return {
461
+ name,
462
+ info: null,
463
+ fullPath: null,
464
+ children: {},
465
+ }
466
+ }
467
+
468
+ function buildSchemaTree(entries, sectionName) {
469
+ const root = createTreeNode(sectionName)
470
+ Object.entries(entries)
471
+ .sort(([a], [b]) => a.localeCompare(b))
472
+ .forEach(([fullPath, info]) => {
473
+ const trimmed = fullPath === sectionName ? '' : fullPath.slice(sectionName.length + 1)
474
+ const segments = trimmed ? trimmed.split('.') : []
475
+
476
+ let current = root
477
+ segments.forEach((segment) => {
478
+ if (!current.children[segment]) {
479
+ current.children[segment] = createTreeNode(segment)
480
+ }
481
+ current = current.children[segment]
482
+ })
483
+
484
+ current.info = info
485
+ current.fullPath = fullPath
486
+ })
487
+
488
+ return root
489
+ }
490
+
491
+ function renderTreeNode(node, engine, isRoot) {
492
+ const childKeys = Object.keys(node.children)
493
+ const hasChildren = childKeys.length > 0
494
+ const hasInfo = Boolean(node.info)
495
+
496
+ if (isRoot || hasChildren) {
497
+ const details = el('details')
498
+ details.open = true
499
+ const summary = el('summary')
500
+ const row = el('div', 'tree-row')
501
+
502
+ const nameNode = el('span', 'mono')
503
+ setHighlighted(nameNode, node.name, engine)
504
+ row.appendChild(nameNode)
505
+
506
+ if (hasInfo) {
507
+ const type = el('span', 'tree-pill')
508
+ setHighlighted(type, node.info.type, engine)
509
+ row.appendChild(type)
510
+ row.appendChild(el('span', 'tree-col-muted', node.info.optional ? 'optional' : 'required'))
511
+ row.appendChild(el('span', 'tree-col-muted', node.info.nullable ? 'nullable' : 'non-null'))
512
+ } else {
513
+ row.appendChild(el('span'))
514
+ row.appendChild(el('span'))
515
+ row.appendChild(el('span'))
516
+ }
517
+
518
+ summary.appendChild(row)
519
+ details.appendChild(summary)
520
+
521
+ const container = el('div', 'schema-tree')
522
+ childKeys
523
+ .sort((a, b) => a.localeCompare(b))
524
+ .forEach((key) => container.appendChild(renderTreeNode(node.children[key], engine, false)))
525
+ details.appendChild(container)
526
+ return details
527
+ }
528
+
529
+ const row = el('div', 'tree-row')
530
+ const nameNode = el('span', 'mono')
531
+ setHighlighted(nameNode, node.name, engine)
532
+ row.appendChild(nameNode)
533
+
534
+ const type = el('span', 'tree-pill')
535
+ setHighlighted(type, node.info ? node.info.type : '—', engine)
536
+ row.appendChild(type)
537
+ row.appendChild(el('span', 'tree-col-muted', node.info?.optional ? 'optional' : 'required'))
538
+ row.appendChild(el('span', 'tree-col-muted', node.info?.nullable ? 'nullable' : 'non-null'))
539
+
540
+ return row
541
+ }
542
+
543
+ function renderSeparatedSchemas(flatSchema, engine) {
544
+ const section = el('div', 'section')
545
+ section.appendChild(el('h3', '', 'Schemas (separated by section)'))
546
+
547
+ const grouped = splitFlatSchemaBySection(flatSchema)
548
+
549
+ SCHEMA_SECTIONS.forEach((sectionName) => {
550
+ const block = el('div', 'schema-block')
551
+ const header = el('div', 'schema-header mono')
552
+ setHighlighted(header, sectionName, engine)
553
+ block.appendChild(header)
554
+
555
+ const entries = grouped[sectionName]
556
+ if (!entries || Object.keys(entries).length === 0) {
557
+ block.appendChild(el('div', 'empty', 'No entries'))
558
+ } else {
559
+ const tree = buildSchemaTree(entries, sectionName)
560
+ block.appendChild(renderTreeNode(tree, engine, true))
561
+ }
562
+
563
+ section.appendChild(block)
564
+ })
565
+
566
+ return section
567
+ }
568
+
569
+ function renderLeaf(leaf, engine) {
570
+ const details = el('details', 'leaf')
571
+ const summary = el('summary')
572
+ setHighlighted(summary, `${String(leaf.method || '').toUpperCase()} ${leaf.path || ''}`, engine)
573
+ details.appendChild(summary)
574
+
575
+ const content = el('div', 'leaf-content')
576
+ const cfg = leaf.cfg || {}
577
+ const flatSchema = state.payload?.schemaFlatByLeaf?.[leaf.key]
578
+
579
+ const overview = el('div', 'section')
580
+ overview.appendChild(el('h3', '', 'Overview'))
581
+ const grid = el('div', 'grid-2')
582
+ grid.appendChild(kv('key', leaf.key, engine))
583
+ grid.appendChild(kv('method', leaf.method, engine))
584
+ grid.appendChild(kv('path', leaf.path, engine))
585
+ grid.appendChild(kv('group', cfg.docsGroup, engine))
586
+ grid.appendChild(kv('stability', cfg.stability, engine))
587
+ grid.appendChild(kv('feed', cfg.feed ? 'true' : 'false', engine))
588
+ grid.appendChild(kv('deprecated', cfg.deprecated ? 'true' : 'false', engine))
589
+ grid.appendChild(kv('hidden', cfg.docsHidden ? 'true' : 'false', engine))
590
+ overview.appendChild(grid)
591
+ content.appendChild(overview)
592
+
593
+ const docs = el('div', 'section')
594
+ docs.appendChild(el('h3', '', 'Documentation'))
595
+ const docGrid = el('div', 'grid-2')
596
+ docGrid.appendChild(kv('summary', cfg.summary, engine))
597
+ docGrid.appendChild(kv('description', cfg.description, engine))
598
+ docs.appendChild(docGrid)
599
+
600
+ const tagsRow = el('div', 'chips')
601
+ ;(cfg.tags || []).forEach((tag) => {
602
+ const chip = el('span', 'chip')
603
+ setHighlighted(chip, tag, engine)
604
+ tagsRow.appendChild(chip)
605
+ })
606
+ if ((cfg.tags || []).length === 0) {
607
+ tagsRow.appendChild(el('span', 'empty', 'No tags'))
608
+ }
609
+ docs.appendChild(tagsRow)
610
+
611
+ if (cfg.docsMeta && Object.keys(cfg.docsMeta).length > 0) {
612
+ Object.entries(cfg.docsMeta).forEach(([k, v]) => {
613
+ docs.appendChild(kv(`meta.${k}`, typeof v === 'string' ? v : JSON.stringify(v), engine))
614
+ })
615
+ }
616
+
617
+ content.appendChild(docs)
618
+
619
+ const schemas = el('div', 'section')
620
+ schemas.appendChild(el('h3', '', 'Schema Summaries'))
621
+ const schemaGrid = el('div', 'grid-2')
622
+ const schemaObj = cfg.schemas || {}
623
+ schemaGrid.appendChild(renderSchemaSummary('params', schemaObj.params, engine))
624
+ schemaGrid.appendChild(renderSchemaSummary('query', schemaObj.query, engine))
625
+ schemaGrid.appendChild(renderSchemaSummary('body', schemaObj.body, engine))
626
+ schemaGrid.appendChild(renderSchemaSummary('output', schemaObj.output, engine))
627
+ schemaGrid.appendChild(renderSchemaSummary('outputMeta', schemaObj.outputMeta, engine))
628
+ schemaGrid.appendChild(renderSchemaSummary('queryExtension', schemaObj.queryExtension, engine))
629
+ schemas.appendChild(schemaGrid)
630
+ content.appendChild(schemas)
631
+
632
+ const files = el('div', 'section')
633
+ files.appendChild(el('h3', '', 'Body Files'))
634
+ if (Array.isArray(cfg.bodyFiles) && cfg.bodyFiles.length > 0) {
635
+ const chips = el('div', 'chips')
636
+ cfg.bodyFiles.forEach((file) => {
637
+ const chip = el('span', 'chip ok')
638
+ setHighlighted(chip, `${file.name} (max ${file.maxCount})`, engine)
639
+ chips.appendChild(chip)
640
+ })
641
+ files.appendChild(chips)
642
+ } else {
643
+ files.appendChild(el('div', 'empty', 'No file upload fields.'))
644
+ }
645
+ content.appendChild(files)
646
+
647
+ content.appendChild(renderSeparatedSchemas(flatSchema, engine))
648
+
649
+ details.appendChild(content)
650
+ return details
651
+ }
652
+
653
+ function renderResults() {
654
+ const engine = createSearchEngine(searchInput.value.trim(), {
655
+ caseSensitive: caseSensitiveInput.checked,
656
+ regex: regexSearchInput.checked,
657
+ })
658
+
659
+ if (engine.error) {
660
+ statusEl.textContent = `Invalid regex: ${engine.error}`
661
+ resultsEl.innerHTML = ''
662
+ resultsEl.appendChild(el('div', 'empty', 'Fix the regex to continue.'))
663
+ return
664
+ }
665
+
666
+ const selectedIds = selectedFieldIds()
667
+ const filtered = state.leaves.filter((leaf) => matchesLeaf(leaf, engine, selectedIds))
668
+
669
+ statusEl.textContent = `${filtered.length} / ${state.leaves.length} routes matched.`
670
+ resultsEl.innerHTML = ''
671
+
672
+ if (filtered.length === 0) {
673
+ resultsEl.appendChild(el('div', 'empty', 'No matches.'))
674
+ return
675
+ }
676
+
677
+ filtered.forEach((leaf) => {
678
+ resultsEl.appendChild(renderLeaf(leaf, engine))
679
+ })
680
+ }
681
+
682
+ function renderFieldCheckboxes() {
683
+ fieldCheckboxes.innerHTML = ''
684
+ SEARCH_FIELDS.forEach((field) => {
685
+ const label = el('label', 'field-item')
686
+ const input = document.createElement('input')
687
+ input.type = 'checkbox'
688
+ input.id = `field-${field.id}`
689
+ input.checked = true
690
+ input.addEventListener('change', renderResults)
691
+ label.appendChild(input)
692
+ label.appendChild(el('span', '', field.label))
693
+ fieldCheckboxes.appendChild(label)
694
+ })
695
+ }
696
+
697
+ async function handleFile(file) {
698
+ const text = await file.text()
699
+ const parsed = JSON.parse(text)
700
+ if (!parsed || !Array.isArray(parsed.leaves)) {
701
+ throw new Error('Invalid export file: expected top-level "leaves" array.')
702
+ }
703
+
704
+ state.payload = parsed
705
+ state.leaves = parsed.leaves
706
+ renderResults()
707
+ }
708
+
709
+ function initializeFromBakedPayload() {
710
+ const baked = window.__FINALIZED_LEAVES_PAYLOAD
711
+ if (!baked || !Array.isArray(baked.leaves)) return
712
+
713
+ state.payload = baked
714
+ state.leaves = baked.leaves
715
+ statusEl.textContent = `Loaded baked payload with ${state.leaves.length} routes.`
716
+ renderResults()
717
+ }
718
+
719
+ fileInput.addEventListener('change', async (event) => {
720
+ const file = event.target.files?.[0]
721
+ if (!file) return
722
+
723
+ try {
724
+ await handleFile(file)
725
+ } catch (error) {
726
+ statusEl.textContent = error instanceof Error ? error.message : String(error)
727
+ resultsEl.innerHTML = ''
728
+ }
729
+ })
730
+
731
+ searchInput.addEventListener('input', renderResults)
732
+ caseSensitiveInput.addEventListener('change', renderResults)
733
+ regexSearchInput.addEventListener('change', renderResults)
734
+
735
+ renderFieldCheckboxes()
736
+ initializeFromBakedPayload()
737
+ </script>
738
+ </body>
739
+ </html>