@emeryld/rrroutes-contract 2.7.3 → 2.7.5

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@emeryld/rrroutes-contract",
3
3
  "description": "TypeScript contract definitions for RRRoutes",
4
- "version": "2.7.3",
4
+ "version": "2.7.5",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "main": "dist/index.cjs",
@@ -6,14 +6,15 @@
6
6
  <title>Finalized Leaves Viewer</title>
7
7
  <style>
8
8
  :root {
9
- --bg: #f5f7fb;
9
+ --bg: #f6f8fc;
10
10
  --surface: #ffffff;
11
- --surface-2: #f8faff;
12
- --border: #d6ddea;
13
- --text: #172033;
14
- --muted: #5b6680;
11
+ --surface-2: #f1f4f9;
12
+ --border: #d9e0eb;
13
+ --text: #182033;
14
+ --muted: #64708b;
15
15
  --accent: #1858c6;
16
16
  --ok: #1f8f4e;
17
+ --danger: #d12b2b;
17
18
  }
18
19
 
19
20
  * {
@@ -24,8 +25,7 @@
24
25
  margin: 0;
25
26
  font-family: 'Iosevka Web', 'SFMono-Regular', Menlo, Consolas, monospace;
26
27
  color: var(--text);
27
- background: radial-gradient(circle at top right, #eaf0ff, transparent 45%),
28
- linear-gradient(180deg, var(--bg), #eef2fa);
28
+ background: var(--bg);
29
29
  }
30
30
 
31
31
  mark {
@@ -43,9 +43,9 @@
43
43
 
44
44
  .card {
45
45
  background: var(--surface);
46
- border: 1px solid var(--border);
47
- border-radius: 12px;
48
- padding: 14px;
46
+ border: 1px solid #e7ecf4;
47
+ border-radius: 6px;
48
+ padding: 12px;
49
49
  }
50
50
 
51
51
  .controls {
@@ -59,22 +59,41 @@
59
59
  gap: 8px;
60
60
  }
61
61
 
62
+ .field-actions {
63
+ display: flex;
64
+ flex-wrap: wrap;
65
+ gap: 8px;
66
+ }
67
+
62
68
  .field-item {
63
69
  display: inline-flex;
64
70
  align-items: center;
65
71
  gap: 6px;
66
- padding: 4px 8px;
67
- border: 1px solid var(--border);
68
- border-radius: 8px;
69
- background: var(--surface-2);
72
+ padding: 3px 0;
70
73
  }
71
74
 
72
- input[type='text'] {
75
+ input[type='text'],
76
+ select {
73
77
  width: 100%;
74
78
  border: 1px solid var(--border);
75
- border-radius: 8px;
76
- padding: 10px 12px;
79
+ border-radius: 4px;
80
+ padding: 8px 10px;
81
+ font: inherit;
82
+ background: #fff;
83
+ }
84
+
85
+ button {
86
+ border: 1px solid var(--border);
87
+ border-radius: 4px;
88
+ padding: 7px 10px;
77
89
  font: inherit;
90
+ background: #fff;
91
+ color: var(--text);
92
+ cursor: pointer;
93
+ }
94
+
95
+ button:hover {
96
+ background: var(--surface-2);
78
97
  }
79
98
 
80
99
  .meta {
@@ -85,14 +104,12 @@
85
104
  #results {
86
105
  margin-top: 14px;
87
106
  display: grid;
88
- gap: 10px;
107
+ gap: 0;
89
108
  }
90
109
 
91
110
  details.leaf {
92
- background: var(--surface);
93
- border: 1px solid var(--border);
94
- border-radius: 10px;
95
- padding: 8px 10px;
111
+ border-top: 1px solid var(--border);
112
+ padding: 10px 2px;
96
113
  }
97
114
 
98
115
  summary {
@@ -103,15 +120,13 @@
103
120
 
104
121
  .leaf-content {
105
122
  display: grid;
106
- gap: 10px;
123
+ gap: 4px;
107
124
  margin-top: 10px;
108
125
  }
109
126
 
110
127
  .section {
111
- border: 1px solid var(--border);
112
- border-radius: 8px;
113
- padding: 10px;
114
- background: #fbfcff;
128
+ border-top: 1px solid #e8edf5;
129
+ padding: 10px 0 2px;
115
130
  }
116
131
 
117
132
  .section h3 {
@@ -121,15 +136,19 @@
121
136
 
122
137
  .grid-2 {
123
138
  display: grid;
124
- gap: 8px;
139
+ gap: 6px;
125
140
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
126
141
  }
127
142
 
143
+ .grid-3 {
144
+ display: grid;
145
+ gap: 6px;
146
+ grid-template-columns: repeat(3, minmax(0, 1fr));
147
+ }
148
+
128
149
  .kv {
129
- border: 1px solid var(--border);
130
- border-radius: 6px;
131
- padding: 6px 8px;
132
- background: white;
150
+ border-bottom: 1px dashed #e2e8f2;
151
+ padding: 4px 0 6px;
133
152
  }
134
153
 
135
154
  .kv .k {
@@ -148,12 +167,16 @@
148
167
  flex-wrap: wrap;
149
168
  }
150
169
 
170
+ .chips.filters {
171
+ margin-top: 2px;
172
+ }
173
+
151
174
  .chip {
152
- border: 1px solid var(--border);
153
- border-radius: 999px;
154
- padding: 2px 8px;
175
+ border: 1px solid #e0e6f0;
176
+ border-radius: 4px;
177
+ padding: 2px 6px;
155
178
  font-size: 12px;
156
- background: #fff;
179
+ background: #f6f8fc;
157
180
  }
158
181
 
159
182
  .chip.ok {
@@ -162,6 +185,30 @@
162
185
  background: #effbf4;
163
186
  }
164
187
 
188
+ .icon-row {
189
+ display: flex;
190
+ gap: 6px;
191
+ margin-top: 2px;
192
+ }
193
+
194
+ .icon-badge {
195
+ display: inline-flex;
196
+ align-items: center;
197
+ justify-content: center;
198
+ width: 18px;
199
+ height: 18px;
200
+ border: 1px solid #e0e6f0;
201
+ border-radius: 4px;
202
+ font-size: 11px;
203
+ background: #f6f8fc;
204
+ }
205
+
206
+ .icon-badge.warn {
207
+ border-color: #f2d5d5;
208
+ color: #a02424;
209
+ background: #fff4f4;
210
+ }
211
+
165
212
  .empty {
166
213
  color: var(--muted);
167
214
  }
@@ -171,11 +218,9 @@
171
218
  }
172
219
 
173
220
  .schema-block {
174
- border: 1px solid var(--border);
175
- border-radius: 8px;
176
- padding: 8px;
177
- margin-bottom: 8px;
178
- background: white;
221
+ border-top: 1px solid #e8edf5;
222
+ padding: 8px 0 2px;
223
+ margin-bottom: 6px;
179
224
  }
180
225
 
181
226
  .schema-block:last-child {
@@ -189,29 +234,51 @@
189
234
 
190
235
  .schema-tree details {
191
236
  margin-left: 12px;
192
- border-left: 1px dashed var(--border);
237
+ border-left: 1px solid #e8edf5;
193
238
  padding-left: 8px;
194
239
  }
195
240
 
196
241
  .tree-row {
197
242
  display: grid;
198
- grid-template-columns: 1fr auto auto auto;
243
+ grid-template-columns: 1fr auto;
199
244
  gap: 8px;
200
245
  padding: 3px 0;
201
246
  align-items: center;
202
247
  }
203
248
 
249
+ .schema-tree summary .tree-row {
250
+ display: inline-grid;
251
+ vertical-align: middle;
252
+ }
253
+
254
+ .tree-name {
255
+ display: inline-flex;
256
+ align-items: baseline;
257
+ gap: 2px;
258
+ }
259
+
204
260
  .tree-col-muted {
205
261
  color: var(--muted);
206
262
  font-size: 12px;
207
263
  }
208
264
 
265
+ .required-star {
266
+ color: var(--danger);
267
+ font-weight: 700;
268
+ }
269
+
209
270
  .tree-pill {
210
- border: 1px solid var(--border);
211
- border-radius: 999px;
271
+ border: 1px solid #e0e6f0;
272
+ border-radius: 4px;
212
273
  padding: 1px 6px;
213
274
  font-size: 11px;
214
- background: #f9fbff;
275
+ background: #f2f5fa;
276
+ }
277
+
278
+ @media (max-width: 720px) {
279
+ .grid-3 {
280
+ grid-template-columns: 1fr;
281
+ }
215
282
  }
216
283
  </style>
217
284
  </head>
@@ -238,9 +305,24 @@
238
305
  <input id="regexSearch" type="checkbox" />
239
306
  <span>regex</span>
240
307
  </label>
308
+ <label class="field-item">
309
+ <span>type match</span>
310
+ <select id="typeMatchMode">
311
+ <option value="contains" selected>contains</option>
312
+ <option value="exact">exact</option>
313
+ </select>
314
+ </label>
241
315
  </div>
242
316
 
243
317
  <div id="fieldCheckboxes" class="field-row"></div>
318
+ <div class="field-actions">
319
+ <button id="selectAllFields" type="button">Select all</button>
320
+ <button id="clearAllFields" type="button">Clear all</button>
321
+ <button id="schemasOnlyFields" type="button">Schemas only</button>
322
+ <button id="metadataOnlyFields" type="button">Metadata only</button>
323
+ <button id="resetFilters" type="button">Reset filters</button>
324
+ </div>
325
+ <div id="activeFilterChips" class="chips filters"></div>
244
326
 
245
327
  <div id="status" class="meta">Load a JSON export to begin.</div>
246
328
  </div>
@@ -264,15 +346,42 @@
264
346
  { id: 'tags', label: 'tags', get: (leaf) => leaf.cfg?.tags || [] },
265
347
  { id: 'stability', label: 'stability', get: (leaf) => [leaf.cfg?.stability] },
266
348
  { id: 'docsMeta', label: 'docsMeta', get: (leaf) => [leaf.cfg?.docsMeta] },
267
- { id: 'schemas', label: 'schemas', get: (leaf) => [leaf.cfg?.schemas] },
268
349
  {
269
- id: 'flatSchema',
270
- label: 'flatSchema',
271
- get: (leaf, schemaFlatByLeaf) => [schemaFlatByLeaf?.[leaf.key]],
350
+ id: 'types',
351
+ label: 'types',
352
+ get: (leaf, schemaFlatByLeaf) => schemaTypeTokens(schemaFlatByLeaf?.[leaf.key]),
353
+ },
354
+ {
355
+ id: 'params',
356
+ label: 'params',
357
+ get: (leaf, schemaFlatByLeaf) =>
358
+ schemaSectionToSearchTokens(schemaFlatByLeaf?.[leaf.key], 'params'),
359
+ },
360
+ {
361
+ id: 'query',
362
+ label: 'query',
363
+ get: (leaf, schemaFlatByLeaf) =>
364
+ schemaSectionToSearchTokens(schemaFlatByLeaf?.[leaf.key], 'query'),
365
+ },
366
+ {
367
+ id: 'body',
368
+ label: 'body',
369
+ get: (leaf, schemaFlatByLeaf) =>
370
+ schemaSectionToSearchTokens(schemaFlatByLeaf?.[leaf.key], 'body'),
371
+ },
372
+ {
373
+ id: 'output',
374
+ label: 'output',
375
+ get: (leaf, schemaFlatByLeaf) =>
376
+ schemaSectionToSearchTokens(schemaFlatByLeaf?.[leaf.key], 'output'),
272
377
  },
273
378
  ]
274
379
 
275
380
  const SCHEMA_SECTIONS = ['params', 'query', 'body', 'output']
381
+ const SCHEMA_FIELD_IDS = new Set(SCHEMA_SECTIONS)
382
+ const METADATA_FIELD_IDS = new Set(
383
+ SEARCH_FIELDS.map((field) => field.id).filter((id) => !SCHEMA_FIELD_IDS.has(id)),
384
+ )
276
385
 
277
386
  const state = { payload: null, leaves: [] }
278
387
 
@@ -280,10 +389,20 @@
280
389
  const searchInput = document.getElementById('searchInput')
281
390
  const caseSensitiveInput = document.getElementById('caseSensitive')
282
391
  const regexSearchInput = document.getElementById('regexSearch')
392
+ const typeMatchModeInput = document.getElementById('typeMatchMode')
283
393
  const fieldCheckboxes = document.getElementById('fieldCheckboxes')
394
+ const activeFilterChips = document.getElementById('activeFilterChips')
395
+ const selectAllFieldsBtn = document.getElementById('selectAllFields')
396
+ const clearAllFieldsBtn = document.getElementById('clearAllFields')
397
+ const schemasOnlyFieldsBtn = document.getElementById('schemasOnlyFields')
398
+ const metadataOnlyFieldsBtn = document.getElementById('metadataOnlyFields')
399
+ const resetFiltersBtn = document.getElementById('resetFilters')
284
400
  const statusEl = document.getElementById('status')
285
401
  const resultsEl = document.getElementById('results')
286
402
 
403
+ const URL_PARAM_KEY = 'filters'
404
+ let isHydratingFromUrl = false
405
+
287
406
  function escapeHtml(value) {
288
407
  return String(value)
289
408
  .replace(/&/g, '&amp;')
@@ -305,6 +424,9 @@
305
424
  if (!query) {
306
425
  return {
307
426
  active: false,
427
+ query,
428
+ caseSensitive,
429
+ regex,
308
430
  error: null,
309
431
  test: () => true,
310
432
  highlight: (text) => escapeHtml(text ?? ''),
@@ -317,6 +439,9 @@
317
439
  const rx = new RegExp(query, flags)
318
440
  return {
319
441
  active: true,
442
+ query,
443
+ caseSensitive,
444
+ regex,
320
445
  error: null,
321
446
  test: (text) => {
322
447
  const source = String(text ?? '')
@@ -334,6 +459,9 @@
334
459
  } catch (error) {
335
460
  return {
336
461
  active: true,
462
+ query,
463
+ caseSensitive,
464
+ regex,
337
465
  error: error instanceof Error ? error.message : 'Invalid regex',
338
466
  test: () => false,
339
467
  highlight: (text) => escapeHtml(text ?? ''),
@@ -347,6 +475,9 @@
347
475
 
348
476
  return {
349
477
  active: true,
478
+ query,
479
+ caseSensitive,
480
+ regex,
350
481
  error: null,
351
482
  test: (text) => {
352
483
  const source = String(text ?? '')
@@ -370,6 +501,62 @@
370
501
  return []
371
502
  }
372
503
 
504
+ function flatSchemaToSearchTokens(flatSchema) {
505
+ if (!flatSchema || typeof flatSchema !== 'object') return []
506
+
507
+ const tokens = new Set()
508
+
509
+ Object.entries(flatSchema).forEach(([path, info]) => {
510
+ if (path) {
511
+ tokens.add(path)
512
+ path.split('.').forEach((segment) => {
513
+ if (segment) tokens.add(segment)
514
+ })
515
+ }
516
+
517
+ if (info && typeof info === 'object') {
518
+ tokens.add(JSON.stringify(info))
519
+ if (info.type) tokens.add(String(info.type))
520
+ if (info.kind) tokens.add(String(info.kind))
521
+ if (info.description) tokens.add(String(info.description))
522
+ if (Array.isArray(info.enumValues)) {
523
+ info.enumValues.forEach((value) => tokens.add(String(value)))
524
+ }
525
+ } else if (info !== null && info !== undefined) {
526
+ tokens.add(String(info))
527
+ }
528
+ })
529
+
530
+ tokens.add(JSON.stringify(flatSchema))
531
+ return Array.from(tokens)
532
+ }
533
+
534
+ function schemaSectionToSearchTokens(flatSchema, sectionName) {
535
+ if (!flatSchema || typeof flatSchema !== 'object') return []
536
+ const sectionEntries = Object.entries(flatSchema).filter(
537
+ ([path]) => path === sectionName || path.startsWith(`${sectionName}.`),
538
+ )
539
+
540
+ if (sectionEntries.length === 0) return []
541
+ return flatSchemaToSearchTokens(Object.fromEntries(sectionEntries))
542
+ }
543
+
544
+ function schemaTypeTokens(flatSchema) {
545
+ if (!flatSchema || typeof flatSchema !== 'object') return []
546
+
547
+ const tokens = new Set()
548
+ Object.values(flatSchema).forEach((info) => {
549
+ if (!info || typeof info !== 'object') return
550
+ if (info.type) tokens.add(String(info.type))
551
+ if (info.kind) tokens.add(String(info.kind))
552
+ if (Array.isArray(info.enumValues)) {
553
+ info.enumValues.forEach((value) => tokens.add(String(value)))
554
+ }
555
+ })
556
+
557
+ return Array.from(tokens)
558
+ }
559
+
373
560
  function selectedFieldIds() {
374
561
  return SEARCH_FIELDS.filter((field) => {
375
562
  const input = document.getElementById(`field-${field.id}`)
@@ -385,6 +572,13 @@
385
572
  return SEARCH_FIELDS.some((field) => {
386
573
  if (!selectedIds.includes(field.id)) return false
387
574
  const tokens = toTokens(field.get(leaf, schemaFlatByLeaf))
575
+ if (field.id === 'types' && typeMatchModeInput.value === 'exact' && !engine.regex) {
576
+ const needle = engine.caseSensitive ? engine.query : engine.query.toLowerCase()
577
+ const normalized = engine.caseSensitive
578
+ ? tokens
579
+ : tokens.map((token) => String(token).toLowerCase())
580
+ return normalized.includes(needle)
581
+ }
388
582
  return tokens.some((token) => engine.test(token))
389
583
  })
390
584
  }
@@ -492,6 +686,29 @@
492
686
  const childKeys = Object.keys(node.children)
493
687
  const hasChildren = childKeys.length > 0
494
688
  const hasInfo = Boolean(node.info)
689
+ const appendNameCell = (row) => {
690
+ const nameWrap = el('span', 'mono tree-name')
691
+ const nameNode = el('span')
692
+ setHighlighted(nameNode, node.name, engine)
693
+ nameWrap.appendChild(nameNode)
694
+
695
+ if (node.info && !node.info.optional) {
696
+ const star = el('span', 'required-star', '*')
697
+ nameWrap.appendChild(star)
698
+ }
699
+
700
+ if (node.info?.nullable) {
701
+ nameWrap.appendChild(el('span', 'tree-col-muted', '-'))
702
+ }
703
+
704
+ row.appendChild(nameWrap)
705
+ }
706
+
707
+ const appendTypeCell = (row, info) => {
708
+ const type = el('span', 'tree-pill')
709
+ setHighlighted(type, info?.type || info?.kind || '—', engine)
710
+ row.appendChild(type)
711
+ }
495
712
 
496
713
  if (isRoot || hasChildren) {
497
714
  const details = el('details')
@@ -499,21 +716,8 @@
499
716
  const summary = el('summary')
500
717
  const row = el('div', 'tree-row')
501
718
 
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
- }
719
+ appendNameCell(row)
720
+ appendTypeCell(row, hasInfo ? node.info : null)
517
721
 
518
722
  summary.appendChild(row)
519
723
  details.appendChild(summary)
@@ -527,43 +731,37 @@
527
731
  }
528
732
 
529
733
  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'))
734
+ appendNameCell(row)
735
+ appendTypeCell(row, node.info)
539
736
 
540
737
  return row
541
738
  }
542
739
 
543
740
  function renderSeparatedSchemas(flatSchema, engine) {
741
+ if (!flatSchema || typeof flatSchema !== 'object') return null
544
742
  const section = el('div', 'section')
545
743
  section.appendChild(el('h3', '', 'Schemas (separated by section)'))
546
744
 
547
745
  const grouped = splitFlatSchemaBySection(flatSchema)
746
+ let hasAnySchemaEntries = false
548
747
 
549
748
  SCHEMA_SECTIONS.forEach((sectionName) => {
749
+ const entries = grouped[sectionName]
750
+ if (!entries || Object.keys(entries).length === 0) return
751
+
752
+ hasAnySchemaEntries = true
550
753
  const block = el('div', 'schema-block')
551
754
  const header = el('div', 'schema-header mono')
552
755
  setHighlighted(header, sectionName, engine)
553
756
  block.appendChild(header)
554
757
 
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
- }
758
+ const tree = buildSchemaTree(entries, sectionName)
759
+ block.appendChild(renderTreeNode(tree, engine, true))
562
760
 
563
761
  section.appendChild(block)
564
762
  })
565
763
 
566
- return section
764
+ return hasAnySchemaEntries ? section : null
567
765
  }
568
766
 
569
767
  function renderLeaf(leaf, engine) {
@@ -578,60 +776,43 @@
578
776
 
579
777
  const overview = el('div', 'section')
580
778
  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'))
779
+ const topGrid = el('div', 'grid-3')
780
+ topGrid.appendChild(kv('group', cfg.docsGroup, engine))
781
+ topGrid.appendChild(
782
+ kv('tags', cfg.tags && cfg.tags.length > 0 ? cfg.tags.join(', ') : undefined, engine),
783
+ )
784
+ topGrid.appendChild(kv('stability', cfg.stability, engine))
785
+ overview.appendChild(topGrid)
786
+ overview.appendChild(kv('summary', cfg.summary, engine))
787
+ overview.appendChild(kv('description', cfg.description, engine))
788
+
789
+ const iconRow = el('div', 'icon-row')
790
+ if (cfg.feed) {
791
+ const feed = el('span', 'icon-badge')
792
+ feed.title = 'Feed'
793
+ setHighlighted(feed, 'F', engine)
794
+ iconRow.appendChild(feed)
608
795
  }
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
- })
796
+ if (cfg.deprecated) {
797
+ const deprecated = el('span', 'icon-badge warn')
798
+ deprecated.title = 'Deprecated'
799
+ setHighlighted(deprecated, 'D', engine)
800
+ iconRow.appendChild(deprecated)
615
801
  }
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)
802
+ if (cfg.docsHidden) {
803
+ const hidden = el('span', 'icon-badge warn')
804
+ hidden.title = 'Hidden'
805
+ setHighlighted(hidden, 'H', engine)
806
+ iconRow.appendChild(hidden)
807
+ }
808
+ if (iconRow.childNodes.length > 0) {
809
+ overview.appendChild(iconRow)
810
+ }
811
+ content.appendChild(overview)
631
812
 
632
813
  const files = el('div', 'section')
633
- files.appendChild(el('h3', '', 'Body Files'))
634
814
  if (Array.isArray(cfg.bodyFiles) && cfg.bodyFiles.length > 0) {
815
+ files.appendChild(el('h3', '', 'Body Files'))
635
816
  const chips = el('div', 'chips')
636
817
  cfg.bodyFiles.forEach((file) => {
637
818
  const chip = el('span', 'chip ok')
@@ -639,12 +820,13 @@
639
820
  chips.appendChild(chip)
640
821
  })
641
822
  files.appendChild(chips)
642
- } else {
643
- files.appendChild(el('div', 'empty', 'No file upload fields.'))
823
+ content.appendChild(files)
644
824
  }
645
- content.appendChild(files)
646
825
 
647
- content.appendChild(renderSeparatedSchemas(flatSchema, engine))
826
+ const separatedSchemas = renderSeparatedSchemas(flatSchema, engine)
827
+ if (separatedSchemas) {
828
+ content.appendChild(separatedSchemas)
829
+ }
648
830
 
649
831
  details.appendChild(content)
650
832
  return details
@@ -660,6 +842,10 @@
660
842
  statusEl.textContent = `Invalid regex: ${engine.error}`
661
843
  resultsEl.innerHTML = ''
662
844
  resultsEl.appendChild(el('div', 'empty', 'Fix the regex to continue.'))
845
+ renderActiveFilterChips({
846
+ selectedIds: selectedFieldIds(),
847
+ hasRegexError: true,
848
+ })
663
849
  return
664
850
  }
665
851
 
@@ -668,15 +854,18 @@
668
854
 
669
855
  statusEl.textContent = `${filtered.length} / ${state.leaves.length} routes matched.`
670
856
  resultsEl.innerHTML = ''
857
+ renderActiveFilterChips({ selectedIds, hasRegexError: false })
671
858
 
672
859
  if (filtered.length === 0) {
673
860
  resultsEl.appendChild(el('div', 'empty', 'No matches.'))
861
+ syncFilterStateToUrl()
674
862
  return
675
863
  }
676
864
 
677
865
  filtered.forEach((leaf) => {
678
866
  resultsEl.appendChild(renderLeaf(leaf, engine))
679
867
  })
868
+ syncFilterStateToUrl()
680
869
  }
681
870
 
682
871
  function renderFieldCheckboxes() {
@@ -694,6 +883,105 @@
694
883
  })
695
884
  }
696
885
 
886
+ function setFieldSelection(allowedIds) {
887
+ SEARCH_FIELDS.forEach((field) => {
888
+ const input = document.getElementById(`field-${field.id}`)
889
+ if (!input) return
890
+ input.checked = allowedIds.has(field.id)
891
+ })
892
+ renderResults()
893
+ }
894
+
895
+ function resetFiltersToDefault() {
896
+ searchInput.value = ''
897
+ caseSensitiveInput.checked = false
898
+ regexSearchInput.checked = false
899
+ typeMatchModeInput.value = 'contains'
900
+ setFieldSelection(new Set(SEARCH_FIELDS.map((field) => field.id)))
901
+ }
902
+
903
+ function renderActiveFilterChips({ selectedIds, hasRegexError }) {
904
+ activeFilterChips.innerHTML = ''
905
+ const chips = []
906
+
907
+ const searchValue = searchInput.value.trim()
908
+
909
+ if (searchValue) chips.push(`search: ${searchValue}`)
910
+ if (typeMatchModeInput.value === 'exact') chips.push('type mode: exact')
911
+ if (caseSensitiveInput.checked) chips.push('case sensitive')
912
+ if (regexSearchInput.checked) chips.push('regex')
913
+ if (hasRegexError) chips.push('regex error')
914
+
915
+ const allIds = SEARCH_FIELDS.map((field) => field.id)
916
+ if (selectedIds.length !== allIds.length) {
917
+ chips.push(`fields: ${selectedIds.join(', ') || 'none'}`)
918
+ }
919
+
920
+ if (chips.length === 0) {
921
+ activeFilterChips.appendChild(el('span', 'empty', 'No active filters'))
922
+ return
923
+ }
924
+
925
+ chips.forEach((chipText) => {
926
+ const chip = el('span', 'chip')
927
+ chip.textContent = chipText
928
+ activeFilterChips.appendChild(chip)
929
+ })
930
+ }
931
+
932
+ function collectFilterState() {
933
+ return {
934
+ search: searchInput.value,
935
+ caseSensitive: caseSensitiveInput.checked,
936
+ regex: regexSearchInput.checked,
937
+ typeMatchMode: typeMatchModeInput.value === 'exact' ? 'exact' : 'contains',
938
+ selectedFields: selectedFieldIds(),
939
+ }
940
+ }
941
+
942
+ function syncFilterStateToUrl() {
943
+ if (isHydratingFromUrl) return
944
+
945
+ const data = collectFilterState()
946
+ const params = new URLSearchParams(window.location.hash.replace(/^#/, ''))
947
+ const serialized = encodeURIComponent(JSON.stringify(data))
948
+ params.set(URL_PARAM_KEY, serialized)
949
+ const nextHash = params.toString()
950
+ if (window.location.hash !== `#${nextHash}`) {
951
+ window.location.hash = nextHash
952
+ }
953
+ }
954
+
955
+ function hydrateFilterStateFromUrl() {
956
+ const params = new URLSearchParams(window.location.hash.replace(/^#/, ''))
957
+ const raw = params.get(URL_PARAM_KEY)
958
+ if (!raw) return
959
+
960
+ try {
961
+ const parsed = JSON.parse(decodeURIComponent(raw))
962
+ isHydratingFromUrl = true
963
+ if (typeof parsed.search === 'string') searchInput.value = parsed.search
964
+ caseSensitiveInput.checked = Boolean(parsed.caseSensitive)
965
+ regexSearchInput.checked = Boolean(parsed.regex)
966
+ if (parsed.typeMatchMode === 'exact' || parsed.typeMatchMode === 'contains') {
967
+ typeMatchModeInput.value = parsed.typeMatchMode
968
+ }
969
+
970
+ if (Array.isArray(parsed.selectedFields)) {
971
+ const allowed = new Set(parsed.selectedFields)
972
+ SEARCH_FIELDS.forEach((field) => {
973
+ const input = document.getElementById(`field-${field.id}`)
974
+ if (!input) return
975
+ input.checked = allowed.has(field.id)
976
+ })
977
+ }
978
+ } catch (error) {
979
+ // Ignore malformed hash state.
980
+ } finally {
981
+ isHydratingFromUrl = false
982
+ }
983
+ }
984
+
697
985
  async function handleFile(file) {
698
986
  const text = await file.text()
699
987
  const parsed = JSON.parse(text)
@@ -731,9 +1019,20 @@
731
1019
  searchInput.addEventListener('input', renderResults)
732
1020
  caseSensitiveInput.addEventListener('change', renderResults)
733
1021
  regexSearchInput.addEventListener('change', renderResults)
1022
+ typeMatchModeInput.addEventListener('change', renderResults)
1023
+
1024
+ selectAllFieldsBtn.addEventListener('click', () =>
1025
+ setFieldSelection(new Set(SEARCH_FIELDS.map((field) => field.id))),
1026
+ )
1027
+ clearAllFieldsBtn.addEventListener('click', () => setFieldSelection(new Set()))
1028
+ schemasOnlyFieldsBtn.addEventListener('click', () => setFieldSelection(SCHEMA_FIELD_IDS))
1029
+ metadataOnlyFieldsBtn.addEventListener('click', () => setFieldSelection(METADATA_FIELD_IDS))
1030
+ resetFiltersBtn.addEventListener('click', resetFiltersToDefault)
734
1031
 
735
1032
  renderFieldCheckboxes()
1033
+ hydrateFilterStateFromUrl()
736
1034
  initializeFromBakedPayload()
1035
+ renderResults()
737
1036
  </script>
738
1037
  </body>
739
1038
  </html>