@emeryld/rrroutes-contract 2.7.3 → 2.7.4

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.4",
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,13 @@
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
 
128
143
  .kv {
129
- border: 1px solid var(--border);
130
- border-radius: 6px;
131
- padding: 6px 8px;
132
- background: white;
144
+ border-bottom: 1px dashed #e2e8f2;
145
+ padding: 4px 0 6px;
133
146
  }
134
147
 
135
148
  .kv .k {
@@ -148,12 +161,16 @@
148
161
  flex-wrap: wrap;
149
162
  }
150
163
 
164
+ .chips.filters {
165
+ margin-top: 2px;
166
+ }
167
+
151
168
  .chip {
152
- border: 1px solid var(--border);
153
- border-radius: 999px;
154
- padding: 2px 8px;
169
+ border: 1px solid #e0e6f0;
170
+ border-radius: 4px;
171
+ padding: 2px 6px;
155
172
  font-size: 12px;
156
- background: #fff;
173
+ background: #f6f8fc;
157
174
  }
158
175
 
159
176
  .chip.ok {
@@ -171,11 +188,9 @@
171
188
  }
172
189
 
173
190
  .schema-block {
174
- border: 1px solid var(--border);
175
- border-radius: 8px;
176
- padding: 8px;
177
- margin-bottom: 8px;
178
- background: white;
191
+ border-top: 1px solid #e8edf5;
192
+ padding: 8px 0 2px;
193
+ margin-bottom: 6px;
179
194
  }
180
195
 
181
196
  .schema-block:last-child {
@@ -189,29 +204,45 @@
189
204
 
190
205
  .schema-tree details {
191
206
  margin-left: 12px;
192
- border-left: 1px dashed var(--border);
207
+ border-left: 1px solid #e8edf5;
193
208
  padding-left: 8px;
194
209
  }
195
210
 
196
211
  .tree-row {
197
212
  display: grid;
198
- grid-template-columns: 1fr auto auto auto;
213
+ grid-template-columns: 1fr auto;
199
214
  gap: 8px;
200
215
  padding: 3px 0;
201
216
  align-items: center;
202
217
  }
203
218
 
219
+ .schema-tree summary .tree-row {
220
+ display: inline-grid;
221
+ vertical-align: middle;
222
+ }
223
+
224
+ .tree-name {
225
+ display: inline-flex;
226
+ align-items: baseline;
227
+ gap: 2px;
228
+ }
229
+
204
230
  .tree-col-muted {
205
231
  color: var(--muted);
206
232
  font-size: 12px;
207
233
  }
208
234
 
235
+ .required-star {
236
+ color: var(--danger);
237
+ font-weight: 700;
238
+ }
239
+
209
240
  .tree-pill {
210
- border: 1px solid var(--border);
211
- border-radius: 999px;
241
+ border: 1px solid #e0e6f0;
242
+ border-radius: 4px;
212
243
  padding: 1px 6px;
213
244
  font-size: 11px;
214
- background: #f9fbff;
245
+ background: #f2f5fa;
215
246
  }
216
247
  </style>
217
248
  </head>
@@ -229,6 +260,11 @@
229
260
  <input id="searchInput" type="text" placeholder="Type to search..." />
230
261
  </label>
231
262
 
263
+ <label>
264
+ Type filter:
265
+ <input id="typeFilterInput" type="text" placeholder='Type/kind/enum (e.g. "paid")' />
266
+ </label>
267
+
232
268
  <div class="field-row">
233
269
  <label class="field-item">
234
270
  <input id="caseSensitive" type="checkbox" />
@@ -238,9 +274,28 @@
238
274
  <input id="regexSearch" type="checkbox" />
239
275
  <span>regex</span>
240
276
  </label>
277
+ <label class="field-item">
278
+ <input id="hasEnumOnly" type="checkbox" />
279
+ <span>has enum only</span>
280
+ </label>
281
+ <label class="field-item">
282
+ <span>type match</span>
283
+ <select id="typeMatchMode">
284
+ <option value="contains" selected>contains</option>
285
+ <option value="exact">exact</option>
286
+ </select>
287
+ </label>
241
288
  </div>
242
289
 
243
290
  <div id="fieldCheckboxes" class="field-row"></div>
291
+ <div class="field-actions">
292
+ <button id="selectAllFields" type="button">Select all</button>
293
+ <button id="clearAllFields" type="button">Clear all</button>
294
+ <button id="schemasOnlyFields" type="button">Schemas only</button>
295
+ <button id="metadataOnlyFields" type="button">Metadata only</button>
296
+ <button id="resetFilters" type="button">Reset filters</button>
297
+ </div>
298
+ <div id="activeFilterChips" class="chips filters"></div>
244
299
 
245
300
  <div id="status" class="meta">Load a JSON export to begin.</div>
246
301
  </div>
@@ -264,26 +319,60 @@
264
319
  { id: 'tags', label: 'tags', get: (leaf) => leaf.cfg?.tags || [] },
265
320
  { id: 'stability', label: 'stability', get: (leaf) => [leaf.cfg?.stability] },
266
321
  { id: 'docsMeta', label: 'docsMeta', get: (leaf) => [leaf.cfg?.docsMeta] },
267
- { id: 'schemas', label: 'schemas', get: (leaf) => [leaf.cfg?.schemas] },
268
322
  {
269
- id: 'flatSchema',
270
- label: 'flatSchema',
271
- get: (leaf, schemaFlatByLeaf) => [schemaFlatByLeaf?.[leaf.key]],
323
+ id: 'params',
324
+ label: 'params',
325
+ get: (leaf, schemaFlatByLeaf) =>
326
+ schemaSectionToSearchTokens(schemaFlatByLeaf?.[leaf.key], 'params'),
327
+ },
328
+ {
329
+ id: 'query',
330
+ label: 'query',
331
+ get: (leaf, schemaFlatByLeaf) =>
332
+ schemaSectionToSearchTokens(schemaFlatByLeaf?.[leaf.key], 'query'),
333
+ },
334
+ {
335
+ id: 'body',
336
+ label: 'body',
337
+ get: (leaf, schemaFlatByLeaf) =>
338
+ schemaSectionToSearchTokens(schemaFlatByLeaf?.[leaf.key], 'body'),
339
+ },
340
+ {
341
+ id: 'output',
342
+ label: 'output',
343
+ get: (leaf, schemaFlatByLeaf) =>
344
+ schemaSectionToSearchTokens(schemaFlatByLeaf?.[leaf.key], 'output'),
272
345
  },
273
346
  ]
274
347
 
275
348
  const SCHEMA_SECTIONS = ['params', 'query', 'body', 'output']
349
+ const SCHEMA_FIELD_IDS = new Set(SCHEMA_SECTIONS)
350
+ const METADATA_FIELD_IDS = new Set(
351
+ SEARCH_FIELDS.map((field) => field.id).filter((id) => !SCHEMA_FIELD_IDS.has(id)),
352
+ )
276
353
 
277
354
  const state = { payload: null, leaves: [] }
278
355
 
279
356
  const fileInput = document.getElementById('fileInput')
280
357
  const searchInput = document.getElementById('searchInput')
358
+ const typeFilterInput = document.getElementById('typeFilterInput')
281
359
  const caseSensitiveInput = document.getElementById('caseSensitive')
282
360
  const regexSearchInput = document.getElementById('regexSearch')
361
+ const hasEnumOnlyInput = document.getElementById('hasEnumOnly')
362
+ const typeMatchModeInput = document.getElementById('typeMatchMode')
283
363
  const fieldCheckboxes = document.getElementById('fieldCheckboxes')
364
+ const activeFilterChips = document.getElementById('activeFilterChips')
365
+ const selectAllFieldsBtn = document.getElementById('selectAllFields')
366
+ const clearAllFieldsBtn = document.getElementById('clearAllFields')
367
+ const schemasOnlyFieldsBtn = document.getElementById('schemasOnlyFields')
368
+ const metadataOnlyFieldsBtn = document.getElementById('metadataOnlyFields')
369
+ const resetFiltersBtn = document.getElementById('resetFilters')
284
370
  const statusEl = document.getElementById('status')
285
371
  const resultsEl = document.getElementById('results')
286
372
 
373
+ const URL_PARAM_KEY = 'filters'
374
+ let isHydratingFromUrl = false
375
+
287
376
  function escapeHtml(value) {
288
377
  return String(value)
289
378
  .replace(/&/g, '&amp;')
@@ -370,6 +459,65 @@
370
459
  return []
371
460
  }
372
461
 
462
+ function flatSchemaToSearchTokens(flatSchema) {
463
+ if (!flatSchema || typeof flatSchema !== 'object') return []
464
+
465
+ const tokens = new Set()
466
+
467
+ Object.entries(flatSchema).forEach(([path, info]) => {
468
+ if (path) {
469
+ tokens.add(path)
470
+ path.split('.').forEach((segment) => {
471
+ if (segment) tokens.add(segment)
472
+ })
473
+ }
474
+
475
+ if (info && typeof info === 'object') {
476
+ tokens.add(JSON.stringify(info))
477
+ if (info.type) tokens.add(String(info.type))
478
+ if (info.kind) tokens.add(String(info.kind))
479
+ if (info.description) tokens.add(String(info.description))
480
+ if (Array.isArray(info.enumValues)) {
481
+ info.enumValues.forEach((value) => tokens.add(String(value)))
482
+ }
483
+ } else if (info !== null && info !== undefined) {
484
+ tokens.add(String(info))
485
+ }
486
+ })
487
+
488
+ tokens.add(JSON.stringify(flatSchema))
489
+ return Array.from(tokens)
490
+ }
491
+
492
+ function schemaSectionToSearchTokens(flatSchema, sectionName) {
493
+ if (!flatSchema || typeof flatSchema !== 'object') return []
494
+ const sectionEntries = Object.entries(flatSchema).filter(
495
+ ([path]) => path === sectionName || path.startsWith(`${sectionName}.`),
496
+ )
497
+
498
+ if (sectionEntries.length === 0) return []
499
+ return flatSchemaToSearchTokens(Object.fromEntries(sectionEntries))
500
+ }
501
+
502
+ function typeFilterTokensByLeaf(flatSchema) {
503
+ if (!flatSchema || typeof flatSchema !== 'object') return { tokens: [], hasEnum: false }
504
+
505
+ const tokens = new Set()
506
+ let hasEnum = false
507
+
508
+ Object.values(flatSchema).forEach((info) => {
509
+ if (!info || typeof info !== 'object') return
510
+ if (info.type) tokens.add(String(info.type))
511
+ if (info.kind) tokens.add(String(info.kind))
512
+ if (Array.isArray(info.enumValues) && info.enumValues.length > 0) {
513
+ hasEnum = true
514
+ info.enumValues.forEach((value) => tokens.add(String(value)))
515
+ }
516
+ })
517
+
518
+ return { tokens: Array.from(tokens), hasEnum }
519
+ }
520
+
373
521
  function selectedFieldIds() {
374
522
  return SEARCH_FIELDS.filter((field) => {
375
523
  const input = document.getElementById(`field-${field.id}`)
@@ -389,6 +537,42 @@
389
537
  })
390
538
  }
391
539
 
540
+ function matchesTypeFilter(leaf, typeEngine, matchMode, hasEnumOnly) {
541
+ const schemaFlatByLeaf = state.payload?.schemaFlatByLeaf || {}
542
+ const { tokens, hasEnum } = typeFilterTokensByLeaf(schemaFlatByLeaf?.[leaf.key])
543
+
544
+ if (hasEnumOnly && !hasEnum) return false
545
+ if (!typeEngine.active) return true
546
+
547
+ if (matchMode === 'exact') {
548
+ const sourceTokens = typeEngine.caseSensitive
549
+ ? tokens
550
+ : tokens.map((token) => token.toLowerCase())
551
+ const needle = typeEngine.caseSensitive ? typeEngine.query : typeEngine.query.toLowerCase()
552
+ return sourceTokens.includes(needle)
553
+ }
554
+
555
+ return tokens.some((token) => typeEngine.test(token))
556
+ }
557
+
558
+ function createTypeSearchEngine(queryRaw) {
559
+ const query = queryRaw || ''
560
+ const caseSensitive = Boolean(caseSensitiveInput.checked)
561
+ if (!query) return { active: false, query, caseSensitive, test: () => true }
562
+
563
+ const needle = caseSensitive ? query : query.toLowerCase()
564
+ return {
565
+ active: true,
566
+ query,
567
+ caseSensitive,
568
+ test: (text) => {
569
+ const source = String(text ?? '')
570
+ const hay = caseSensitive ? source : source.toLowerCase()
571
+ return hay.includes(needle)
572
+ },
573
+ }
574
+ }
575
+
392
576
  function el(tag, className, text) {
393
577
  const node = document.createElement(tag)
394
578
  if (className) node.className = className
@@ -492,6 +676,29 @@
492
676
  const childKeys = Object.keys(node.children)
493
677
  const hasChildren = childKeys.length > 0
494
678
  const hasInfo = Boolean(node.info)
679
+ const appendNameCell = (row) => {
680
+ const nameWrap = el('span', 'mono tree-name')
681
+ const nameNode = el('span')
682
+ setHighlighted(nameNode, node.name, engine)
683
+ nameWrap.appendChild(nameNode)
684
+
685
+ if (node.info && !node.info.optional) {
686
+ const star = el('span', 'required-star', '*')
687
+ nameWrap.appendChild(star)
688
+ }
689
+
690
+ if (node.info?.nullable) {
691
+ nameWrap.appendChild(el('span', 'tree-col-muted', '-'))
692
+ }
693
+
694
+ row.appendChild(nameWrap)
695
+ }
696
+
697
+ const appendTypeCell = (row, info) => {
698
+ const type = el('span', 'tree-pill')
699
+ setHighlighted(type, info?.type || info?.kind || '—', engine)
700
+ row.appendChild(type)
701
+ }
495
702
 
496
703
  if (isRoot || hasChildren) {
497
704
  const details = el('details')
@@ -499,21 +706,8 @@
499
706
  const summary = el('summary')
500
707
  const row = el('div', 'tree-row')
501
708
 
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
- }
709
+ appendNameCell(row)
710
+ appendTypeCell(row, hasInfo ? node.info : null)
517
711
 
518
712
  summary.appendChild(row)
519
713
  details.appendChild(summary)
@@ -527,15 +721,8 @@
527
721
  }
528
722
 
529
723
  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'))
724
+ appendNameCell(row)
725
+ appendTypeCell(row, node.info)
539
726
 
540
727
  return row
541
728
  }
@@ -655,28 +842,42 @@
655
842
  caseSensitive: caseSensitiveInput.checked,
656
843
  regex: regexSearchInput.checked,
657
844
  })
845
+ const typeEngine = createTypeSearchEngine(typeFilterInput.value.trim())
846
+ const hasEnumOnly = Boolean(hasEnumOnlyInput.checked)
847
+ const typeMatchMode = typeMatchModeInput.value === 'exact' ? 'exact' : 'contains'
658
848
 
659
849
  if (engine.error) {
660
850
  statusEl.textContent = `Invalid regex: ${engine.error}`
661
851
  resultsEl.innerHTML = ''
662
852
  resultsEl.appendChild(el('div', 'empty', 'Fix the regex to continue.'))
853
+ renderActiveFilterChips({
854
+ selectedIds: selectedFieldIds(),
855
+ hasRegexError: true,
856
+ })
663
857
  return
664
858
  }
665
859
 
666
860
  const selectedIds = selectedFieldIds()
667
- const filtered = state.leaves.filter((leaf) => matchesLeaf(leaf, engine, selectedIds))
861
+ const filtered = state.leaves.filter(
862
+ (leaf) =>
863
+ matchesLeaf(leaf, engine, selectedIds) &&
864
+ matchesTypeFilter(leaf, typeEngine, typeMatchMode, hasEnumOnly),
865
+ )
668
866
 
669
867
  statusEl.textContent = `${filtered.length} / ${state.leaves.length} routes matched.`
670
868
  resultsEl.innerHTML = ''
869
+ renderActiveFilterChips({ selectedIds, hasRegexError: false })
671
870
 
672
871
  if (filtered.length === 0) {
673
872
  resultsEl.appendChild(el('div', 'empty', 'No matches.'))
873
+ syncFilterStateToUrl()
674
874
  return
675
875
  }
676
876
 
677
877
  filtered.forEach((leaf) => {
678
878
  resultsEl.appendChild(renderLeaf(leaf, engine))
679
879
  })
880
+ syncFilterStateToUrl()
680
881
  }
681
882
 
682
883
  function renderFieldCheckboxes() {
@@ -694,6 +895,114 @@
694
895
  })
695
896
  }
696
897
 
898
+ function setFieldSelection(allowedIds) {
899
+ SEARCH_FIELDS.forEach((field) => {
900
+ const input = document.getElementById(`field-${field.id}`)
901
+ if (!input) return
902
+ input.checked = allowedIds.has(field.id)
903
+ })
904
+ renderResults()
905
+ }
906
+
907
+ function resetFiltersToDefault() {
908
+ searchInput.value = ''
909
+ typeFilterInput.value = ''
910
+ caseSensitiveInput.checked = false
911
+ regexSearchInput.checked = false
912
+ hasEnumOnlyInput.checked = false
913
+ typeMatchModeInput.value = 'contains'
914
+ setFieldSelection(new Set(SEARCH_FIELDS.map((field) => field.id)))
915
+ }
916
+
917
+ function renderActiveFilterChips({ selectedIds, hasRegexError }) {
918
+ activeFilterChips.innerHTML = ''
919
+ const chips = []
920
+
921
+ const searchValue = searchInput.value.trim()
922
+ const typeValue = typeFilterInput.value.trim()
923
+
924
+ if (searchValue) chips.push(`search: ${searchValue}`)
925
+ if (typeValue) chips.push(`type: ${typeValue}`)
926
+ if (typeMatchModeInput.value === 'exact') chips.push('type mode: exact')
927
+ if (caseSensitiveInput.checked) chips.push('case sensitive')
928
+ if (regexSearchInput.checked) chips.push('regex')
929
+ if (hasEnumOnlyInput.checked) chips.push('has enum only')
930
+ if (hasRegexError) chips.push('regex error')
931
+
932
+ const allIds = SEARCH_FIELDS.map((field) => field.id)
933
+ if (selectedIds.length !== allIds.length) {
934
+ chips.push(`fields: ${selectedIds.join(', ') || 'none'}`)
935
+ }
936
+
937
+ if (chips.length === 0) {
938
+ activeFilterChips.appendChild(el('span', 'empty', 'No active filters'))
939
+ return
940
+ }
941
+
942
+ chips.forEach((chipText) => {
943
+ const chip = el('span', 'chip')
944
+ chip.textContent = chipText
945
+ activeFilterChips.appendChild(chip)
946
+ })
947
+ }
948
+
949
+ function collectFilterState() {
950
+ return {
951
+ search: searchInput.value,
952
+ typeFilter: typeFilterInput.value,
953
+ caseSensitive: caseSensitiveInput.checked,
954
+ regex: regexSearchInput.checked,
955
+ hasEnumOnly: hasEnumOnlyInput.checked,
956
+ typeMatchMode: typeMatchModeInput.value === 'exact' ? 'exact' : 'contains',
957
+ selectedFields: selectedFieldIds(),
958
+ }
959
+ }
960
+
961
+ function syncFilterStateToUrl() {
962
+ if (isHydratingFromUrl) return
963
+
964
+ const data = collectFilterState()
965
+ const params = new URLSearchParams(window.location.hash.replace(/^#/, ''))
966
+ const serialized = encodeURIComponent(JSON.stringify(data))
967
+ params.set(URL_PARAM_KEY, serialized)
968
+ const nextHash = params.toString()
969
+ if (window.location.hash !== `#${nextHash}`) {
970
+ window.location.hash = nextHash
971
+ }
972
+ }
973
+
974
+ function hydrateFilterStateFromUrl() {
975
+ const params = new URLSearchParams(window.location.hash.replace(/^#/, ''))
976
+ const raw = params.get(URL_PARAM_KEY)
977
+ if (!raw) return
978
+
979
+ try {
980
+ const parsed = JSON.parse(decodeURIComponent(raw))
981
+ isHydratingFromUrl = true
982
+ if (typeof parsed.search === 'string') searchInput.value = parsed.search
983
+ if (typeof parsed.typeFilter === 'string') typeFilterInput.value = parsed.typeFilter
984
+ caseSensitiveInput.checked = Boolean(parsed.caseSensitive)
985
+ regexSearchInput.checked = Boolean(parsed.regex)
986
+ hasEnumOnlyInput.checked = Boolean(parsed.hasEnumOnly)
987
+ if (parsed.typeMatchMode === 'exact' || parsed.typeMatchMode === 'contains') {
988
+ typeMatchModeInput.value = parsed.typeMatchMode
989
+ }
990
+
991
+ if (Array.isArray(parsed.selectedFields)) {
992
+ const allowed = new Set(parsed.selectedFields)
993
+ SEARCH_FIELDS.forEach((field) => {
994
+ const input = document.getElementById(`field-${field.id}`)
995
+ if (!input) return
996
+ input.checked = allowed.has(field.id)
997
+ })
998
+ }
999
+ } catch (error) {
1000
+ // Ignore malformed hash state.
1001
+ } finally {
1002
+ isHydratingFromUrl = false
1003
+ }
1004
+ }
1005
+
697
1006
  async function handleFile(file) {
698
1007
  const text = await file.text()
699
1008
  const parsed = JSON.parse(text)
@@ -729,11 +1038,24 @@
729
1038
  })
730
1039
 
731
1040
  searchInput.addEventListener('input', renderResults)
1041
+ typeFilterInput.addEventListener('input', renderResults)
732
1042
  caseSensitiveInput.addEventListener('change', renderResults)
733
1043
  regexSearchInput.addEventListener('change', renderResults)
1044
+ hasEnumOnlyInput.addEventListener('change', renderResults)
1045
+ typeMatchModeInput.addEventListener('change', renderResults)
1046
+
1047
+ selectAllFieldsBtn.addEventListener('click', () =>
1048
+ setFieldSelection(new Set(SEARCH_FIELDS.map((field) => field.id))),
1049
+ )
1050
+ clearAllFieldsBtn.addEventListener('click', () => setFieldSelection(new Set()))
1051
+ schemasOnlyFieldsBtn.addEventListener('click', () => setFieldSelection(SCHEMA_FIELD_IDS))
1052
+ metadataOnlyFieldsBtn.addEventListener('click', () => setFieldSelection(METADATA_FIELD_IDS))
1053
+ resetFiltersBtn.addEventListener('click', resetFiltersToDefault)
734
1054
 
735
1055
  renderFieldCheckboxes()
1056
+ hydrateFilterStateFromUrl()
736
1057
  initializeFromBakedPayload()
1058
+ renderResults()
737
1059
  </script>
738
1060
  </body>
739
1061
  </html>