@emeryld/rrroutes-contract 2.7.11 → 2.7.13

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.
@@ -50,22 +50,68 @@
50
50
  }
51
51
 
52
52
  .controls {
53
+ display: grid;
54
+ gap: 14px;
55
+ }
56
+
57
+ .primary-row {
53
58
  display: grid;
54
59
  gap: 10px;
60
+ grid-template-columns: minmax(220px, 0.9fr) minmax(300px, 1.4fr) minmax(220px, 1fr);
61
+ align-items: end;
62
+ }
63
+
64
+ .control-block {
65
+ display: grid;
66
+ gap: 6px;
67
+ }
68
+
69
+ .control-label {
70
+ font-size: 12px;
71
+ color: var(--muted);
55
72
  }
56
73
 
57
- .field-row {
74
+ .quick-toggles {
58
75
  display: flex;
59
76
  flex-wrap: wrap;
60
77
  gap: 8px;
78
+ align-items: center;
79
+ }
80
+
81
+ .scope-row {
82
+ border-top: 1px solid var(--border);
83
+ padding-top: 12px;
84
+ display: grid;
85
+ gap: 10px;
61
86
  }
62
87
 
63
- .field-actions {
88
+ .scope-actions {
64
89
  display: flex;
65
90
  flex-wrap: wrap;
66
91
  gap: 8px;
67
92
  }
68
93
 
94
+ .scope-groups {
95
+ display: grid;
96
+ gap: 10px;
97
+ }
98
+
99
+ .scope-group {
100
+ display: grid;
101
+ gap: 6px;
102
+ }
103
+
104
+ .scope-group-title {
105
+ font-size: 12px;
106
+ color: var(--muted);
107
+ }
108
+
109
+ .scope-group-chips {
110
+ display: flex;
111
+ flex-wrap: wrap;
112
+ gap: 6px;
113
+ }
114
+
69
115
  .field-item {
70
116
  display: inline-flex;
71
117
  align-items: center;
@@ -86,7 +132,7 @@
86
132
 
87
133
  button {
88
134
  border: 1px solid var(--border);
89
- border-radius: 4px;
135
+ border-radius: 6px;
90
136
  padding: 7px 10px;
91
137
  font: inherit;
92
138
  background: var(--surface-2);
@@ -190,12 +236,82 @@
190
236
  background: var(--surface-2);
191
237
  }
192
238
 
239
+ .chip-btn {
240
+ cursor: pointer;
241
+ }
242
+
193
243
  .chip.ok {
194
244
  border-color: #b5e4c8;
195
245
  color: var(--ok);
196
246
  background: #effbf4;
197
247
  }
198
248
 
249
+ .pill-toggle {
250
+ border: 1px solid var(--border);
251
+ border-radius: 999px;
252
+ padding: 6px 12px;
253
+ font-size: 12px;
254
+ background: var(--surface-2);
255
+ color: var(--text);
256
+ }
257
+
258
+ .pill-toggle[aria-pressed='true'] {
259
+ border-color: var(--accent);
260
+ background: rgba(167, 100, 211, 0.25);
261
+ color: #f0d8ff;
262
+ }
263
+
264
+ .scope-action {
265
+ font-size: 12px;
266
+ padding: 5px 10px;
267
+ }
268
+
269
+ .field-chip {
270
+ font-size: 12px;
271
+ padding: 5px 10px;
272
+ border-radius: 999px;
273
+ }
274
+
275
+ .field-chip[aria-pressed='true'] {
276
+ border-color: var(--schema-accent);
277
+ background: rgba(251, 189, 35, 0.22);
278
+ color: #ffe7aa;
279
+ }
280
+
281
+ .advanced-panel {
282
+ border-top: 1px solid var(--border);
283
+ padding-top: 10px;
284
+ }
285
+
286
+ .advanced-panel > summary {
287
+ color: var(--text);
288
+ font-size: 13px;
289
+ }
290
+
291
+ .advanced-content {
292
+ margin-top: 10px;
293
+ display: flex;
294
+ flex-wrap: wrap;
295
+ gap: 12px;
296
+ align-items: center;
297
+ }
298
+
299
+ .advanced-content .field-item {
300
+ padding: 0;
301
+ }
302
+
303
+ .active-filters-row {
304
+ border-top: 1px solid var(--border);
305
+ padding-top: 10px;
306
+ display: grid;
307
+ gap: 6px;
308
+ }
309
+
310
+ .active-filters-title {
311
+ color: var(--muted);
312
+ font-size: 12px;
313
+ }
314
+
199
315
  .icon-row {
200
316
  display: flex;
201
317
  gap: 6px;
@@ -214,6 +330,16 @@
214
330
  background: var(--surface-2);
215
331
  }
216
332
 
333
+ .icon-badge.feed {
334
+ width: auto;
335
+ min-width: 46px;
336
+ padding: 2px 8px;
337
+ font-weight: 700;
338
+ border-color: var(--schema-accent);
339
+ color: var(--schema-accent);
340
+ background: rgba(251, 189, 35, 0.15);
341
+ }
342
+
217
343
  .icon-badge.warn {
218
344
  border-color: #f2d5d5;
219
345
  color: #a02424;
@@ -329,6 +455,11 @@
329
455
  }
330
456
 
331
457
  @media (max-width: 720px) {
458
+ .primary-row {
459
+ grid-template-columns: 1fr;
460
+ align-items: stretch;
461
+ }
462
+
332
463
  .grid-3 {
333
464
  grid-template-columns: 1fr;
334
465
  }
@@ -339,47 +470,65 @@
339
470
  <div class="wrap">
340
471
  <h1>Finalized Leaves Viewer</h1>
341
472
  <div class="card controls">
342
- <label>
343
- Load export JSON file:
344
- <input id="fileInput" type="file" accept="application/json,.json" />
345
- </label>
346
-
347
- <label>
348
- Search text:
349
- <input id="searchInput" type="text" placeholder="Type to search..." />
350
- </label>
351
-
352
- <div class="field-row">
353
- <label class="field-item">
354
- <input id="caseSensitive" type="checkbox" />
355
- <span>case sensitive</span>
356
- </label>
357
- <label class="field-item">
358
- <input id="regexSearch" type="checkbox" />
359
- <span>regex</span>
473
+ <div class="primary-row">
474
+ <label class="control-block">
475
+ <span class="control-label">Load export JSON file</span>
476
+ <input id="fileInput" type="file" accept="application/json,.json" />
360
477
  </label>
361
- <label class="field-item">
362
- <span>type match</span>
363
- <select id="typeMatchMode">
364
- <option value="contains" selected>contains</option>
365
- <option value="exact">exact</option>
366
- </select>
367
- </label>
368
- <label class="field-item">
369
- <input id="schemaRowsMatchOnly" type="checkbox" />
370
- <span>schema rows match filter</span>
478
+ <label class="control-block">
479
+ <span class="control-label">Search text</span>
480
+ <input id="searchInput" type="text" placeholder="Type to search..." />
371
481
  </label>
482
+ <div class="control-block">
483
+ <span class="control-label">Quick filters</span>
484
+ <div class="quick-toggles">
485
+ <button id="caseSensitive" class="pill-toggle" type="button" aria-pressed="false">
486
+ Case sensitive
487
+ </button>
488
+ <button id="regexSearch" class="pill-toggle" type="button" aria-pressed="false">
489
+ Regex
490
+ </button>
491
+ </div>
492
+ </div>
493
+ </div>
494
+
495
+ <div class="scope-row">
496
+ <div class="scope-actions">
497
+ <button id="selectAllFields" class="scope-action" type="button">All</button>
498
+ <button id="clearAllFields" class="scope-action" type="button">None</button>
499
+ <button id="coreFields" class="scope-action" type="button">Core</button>
500
+ <button id="schemasOnlyFields" class="scope-action" type="button">Schema</button>
501
+ <button id="sourceOnlyFields" class="scope-action" type="button">Source</button>
502
+ </div>
503
+ <div id="fieldChipGroups" class="scope-groups"></div>
372
504
  </div>
373
505
 
374
- <div id="fieldCheckboxes" class="field-row"></div>
375
- <div class="field-actions">
376
- <button id="selectAllFields" type="button">Select all</button>
377
- <button id="clearAllFields" type="button">Clear all</button>
378
- <button id="schemasOnlyFields" type="button">Schemas only</button>
379
- <button id="metadataOnlyFields" type="button">Metadata only</button>
380
- <button id="resetFilters" type="button">Reset filters</button>
506
+ <details class="advanced-panel">
507
+ <summary>Advanced filters</summary>
508
+ <div class="advanced-content">
509
+ <label class="field-item">
510
+ <span>type match</span>
511
+ <select id="typeMatchMode">
512
+ <option value="contains" selected>contains</option>
513
+ <option value="exact">exact</option>
514
+ </select>
515
+ </label>
516
+ <button
517
+ id="schemaRowsMatchOnly"
518
+ class="pill-toggle"
519
+ type="button"
520
+ aria-pressed="false"
521
+ >
522
+ Schema rows match only
523
+ </button>
524
+ <button id="resetFilters" type="button">Reset filters</button>
525
+ </div>
526
+ </details>
527
+
528
+ <div class="active-filters-row">
529
+ <div class="active-filters-title">Active filters</div>
530
+ <div id="activeFilterChips" class="chips filters"></div>
381
531
  </div>
382
- <div id="activeFilterChips" class="chips filters"></div>
383
532
 
384
533
  <div id="status" class="meta">Load a JSON export to begin.</div>
385
534
  </div>
@@ -461,24 +610,56 @@
461
610
 
462
611
  const SCHEMA_SECTIONS = ['params', 'query', 'body', 'output']
463
612
  const SCHEMA_FIELD_IDS = new Set(SCHEMA_SECTIONS)
464
- const METADATA_FIELD_IDS = new Set(
465
- SEARCH_FIELDS.map((field) => field.id).filter((id) => !SCHEMA_FIELD_IDS.has(id)),
613
+ const SOURCE_FIELD_IDS = new Set(['sourceDefinition', 'sourceSchemas'])
614
+ const SCHEMA_SCOPE_FIELD_IDS = new Set([...SCHEMA_SECTIONS, 'types'])
615
+ const CORE_FIELD_IDS = new Set(
616
+ SEARCH_FIELDS.map((field) => field.id).filter(
617
+ (id) => !SCHEMA_SCOPE_FIELD_IDS.has(id) && !SOURCE_FIELD_IDS.has(id),
618
+ ),
466
619
  )
620
+ const FIELD_GROUPS = [
621
+ {
622
+ id: 'core',
623
+ label: 'Core fields',
624
+ fieldIds: SEARCH_FIELDS.filter((field) => CORE_FIELD_IDS.has(field.id)).map(
625
+ (field) => field.id,
626
+ ),
627
+ },
628
+ {
629
+ id: 'schema',
630
+ label: 'Schema fields',
631
+ fieldIds: SEARCH_FIELDS.filter((field) => SCHEMA_SCOPE_FIELD_IDS.has(field.id)).map(
632
+ (field) => field.id,
633
+ ),
634
+ },
635
+ {
636
+ id: 'source',
637
+ label: 'Source fields',
638
+ fieldIds: SEARCH_FIELDS.filter((field) => SOURCE_FIELD_IDS.has(field.id)).map(
639
+ (field) => field.id,
640
+ ),
641
+ },
642
+ ]
467
643
 
468
- const state = { payload: null, leaves: [] }
644
+ const state = {
645
+ payload: null,
646
+ leaves: [],
647
+ selectedFieldIds: new Set(SEARCH_FIELDS.map((field) => field.id)),
648
+ }
469
649
 
470
650
  const fileInput = document.getElementById('fileInput')
471
651
  const searchInput = document.getElementById('searchInput')
472
- const caseSensitiveInput = document.getElementById('caseSensitive')
473
- const regexSearchInput = document.getElementById('regexSearch')
652
+ const caseSensitiveToggle = document.getElementById('caseSensitive')
653
+ const regexSearchToggle = document.getElementById('regexSearch')
474
654
  const typeMatchModeInput = document.getElementById('typeMatchMode')
475
- const schemaRowsMatchOnlyInput = document.getElementById('schemaRowsMatchOnly')
476
- const fieldCheckboxes = document.getElementById('fieldCheckboxes')
655
+ const schemaRowsMatchOnlyToggle = document.getElementById('schemaRowsMatchOnly')
656
+ const fieldChipGroups = document.getElementById('fieldChipGroups')
477
657
  const activeFilterChips = document.getElementById('activeFilterChips')
478
658
  const selectAllFieldsBtn = document.getElementById('selectAllFields')
479
659
  const clearAllFieldsBtn = document.getElementById('clearAllFields')
660
+ const coreFieldsBtn = document.getElementById('coreFields')
480
661
  const schemasOnlyFieldsBtn = document.getElementById('schemasOnlyFields')
481
- const metadataOnlyFieldsBtn = document.getElementById('metadataOnlyFields')
662
+ const sourceOnlyFieldsBtn = document.getElementById('sourceOnlyFields')
482
663
  const resetFiltersBtn = document.getElementById('resetFilters')
483
664
  const statusEl = document.getElementById('status')
484
665
  const resultsEl = document.getElementById('results')
@@ -499,6 +680,21 @@
499
680
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
500
681
  }
501
682
 
683
+ function isPressed(button) {
684
+ return button?.getAttribute('aria-pressed') === 'true'
685
+ }
686
+
687
+ function setPressed(button, pressed) {
688
+ if (!button) return
689
+ button.setAttribute('aria-pressed', pressed ? 'true' : 'false')
690
+ }
691
+
692
+ function togglePressed(button) {
693
+ const next = !isPressed(button)
694
+ setPressed(button, next)
695
+ return next
696
+ }
697
+
502
698
  function createSearchEngine(queryRaw, options) {
503
699
  const query = queryRaw || ''
504
700
  const caseSensitive = Boolean(options.caseSensitive)
@@ -697,13 +893,6 @@
697
893
  return matched
698
894
  }
699
895
 
700
- function selectedFieldIds() {
701
- return SEARCH_FIELDS.filter((field) => {
702
- const input = document.getElementById(`field-${field.id}`)
703
- return Boolean(input && input.checked)
704
- }).map((field) => field.id)
705
- }
706
-
707
896
  function matchesLeaf(leaf, engine, selectedIds) {
708
897
  if (!engine.active) return true
709
898
  if (selectedIds.length === 0) return false
@@ -789,34 +978,24 @@
789
978
  return `vscode://file/${encodeURI(vscodePath)}:${line}:${column}`
790
979
  }
791
980
 
792
- function sourceDisplay(source) {
793
- return ''
794
- }
795
-
796
- function createSourceRow(key, source, engine) {
981
+ function createSourceRow(label, source, engine) {
797
982
  const box = el('div', 'kv')
798
- const keyNode = el('div', 'k')
799
- const valueNode = el('div', 'v mono')
983
+ const labelNode = el('div', 'v mono')
800
984
 
801
985
  if (!source || !source.file) {
802
- setHighlighted(keyNode, key, engine)
803
- box.appendChild(keyNode)
804
- setHighlighted(valueNode, '—', engine)
805
- box.appendChild(valueNode)
986
+ setHighlighted(labelNode, label, engine)
987
+ box.appendChild(labelNode)
806
988
  return box
807
989
  }
808
990
 
809
991
  const href = sourceHref(source)
810
- const keyLink = document.createElement('a')
811
- keyLink.href = href
812
- keyLink.target = '_blank'
813
- keyLink.rel = 'noopener noreferrer'
814
- keyLink.innerHTML = engine.highlight(key)
815
- keyNode.appendChild(keyLink)
816
-
817
- setHighlighted(valueNode, sourceDisplay(source), engine)
818
- box.appendChild(keyNode)
819
- box.appendChild(valueNode)
992
+ const link = document.createElement('a')
993
+ link.href = href
994
+ link.target = '_blank'
995
+ link.rel = 'noopener noreferrer'
996
+ link.innerHTML = engine.highlight(label)
997
+ labelNode.appendChild(link)
998
+ box.appendChild(labelNode)
820
999
  return box
821
1000
  }
822
1001
 
@@ -841,6 +1020,33 @@
841
1020
  return result
842
1021
  }
843
1022
 
1023
+ const SCHEMA_SOURCE_KEYS_BY_SECTION = {
1024
+ params: 'paramsSchema',
1025
+ query: 'querySchema',
1026
+ body: 'bodySchema',
1027
+ output: 'outputSchema',
1028
+ }
1029
+
1030
+ function getSchemaSource(source, sectionName) {
1031
+ const schemaKey = SCHEMA_SOURCE_KEYS_BY_SECTION[sectionName]
1032
+ if (!schemaKey || !source?.schemas || typeof source.schemas !== 'object') {
1033
+ return {
1034
+ schemaKey,
1035
+ sourceValue: undefined,
1036
+ }
1037
+ }
1038
+
1039
+ return {
1040
+ schemaKey,
1041
+ sourceValue: source.schemas[schemaKey],
1042
+ }
1043
+ }
1044
+
1045
+ function resolveSchemaSourceLabel(schemaKey, sourceValue) {
1046
+ if (!sourceValue || typeof sourceValue !== 'object') return schemaKey || 'schema'
1047
+ return sourceValue.sourceName || sourceValue.tag || schemaKey || 'schema'
1048
+ }
1049
+
844
1050
  function createTreeNode(name = '') {
845
1051
  return {
846
1052
  name,
@@ -928,14 +1134,14 @@
928
1134
  return row
929
1135
  }
930
1136
 
931
- function renderSeparatedSchemas(flatSchema, engine, selectedIds) {
1137
+ function renderSeparatedSchemas(flatSchema, engine, selectedIds, source) {
932
1138
  if (!flatSchema || typeof flatSchema !== 'object') return null
933
1139
  const section = el('div', 'section')
934
1140
  section.appendChild(el('h3', '', 'Schemas (separated by section)'))
935
1141
 
936
1142
  const grouped = splitFlatSchemaBySection(flatSchema)
937
1143
  let hasAnySchemaEntries = false
938
- const limitToMatchedRows = schemaRowsMatchOnlyInput.checked && engine.active
1144
+ const limitToMatchedRows = isPressed(schemaRowsMatchOnlyToggle) && engine.active
939
1145
 
940
1146
  SCHEMA_SECTIONS.forEach((sectionName) => {
941
1147
  const rawEntries = grouped[sectionName]
@@ -956,6 +1162,20 @@
956
1162
  setHighlighted(header, sectionName, engine)
957
1163
  block.appendChild(header)
958
1164
 
1165
+ const schemaSource = getSchemaSource(source, sectionName)
1166
+ if (schemaSource.sourceValue) {
1167
+ block.appendChild(
1168
+ createSourceRow(
1169
+ resolveSchemaSourceLabel(
1170
+ schemaSource.schemaKey || `${sectionName}Schema`,
1171
+ schemaSource.sourceValue,
1172
+ ),
1173
+ schemaSource.sourceValue,
1174
+ engine,
1175
+ ),
1176
+ )
1177
+ }
1178
+
959
1179
  const tree = buildSchemaTree(entries, sectionName)
960
1180
  block.appendChild(renderTreeNode(tree, engine, true))
961
1181
 
@@ -989,9 +1209,9 @@
989
1209
 
990
1210
  const iconRow = el('div', 'icon-row')
991
1211
  if (cfg.feed) {
992
- const feed = el('span', 'icon-badge')
993
- feed.title = 'Feed'
994
- setHighlighted(feed, 'F', engine)
1212
+ const feed = el('span', 'icon-badge feed')
1213
+ feed.title = 'Feed endpoint'
1214
+ setHighlighted(feed, 'Feed', engine)
995
1215
  iconRow.appendChild(feed)
996
1216
  }
997
1217
  if (cfg.deprecated) {
@@ -1026,30 +1246,11 @@
1026
1246
 
1027
1247
  const sourceByLeaf = state.payload?.sourceByLeaf || {}
1028
1248
  const source = sourceByLeaf[leaf.key]
1029
- if (source) {
1030
- const sourceSection = el('div', 'section')
1031
- sourceSection.appendChild(el('h3', '', 'Source'))
1032
- sourceSection.appendChild(createSourceRow('definition', source.definition, engine))
1033
-
1034
- const schemaSources = source.schemas || {}
1035
- const schemaEntries = Object.entries(schemaSources)
1036
- if (schemaEntries.length > 0) {
1037
- const schemaGrid = el('div', 'grid-2')
1038
- schemaEntries.forEach(([schemaName, schemaSource]) => {
1039
- const display = schemaSource?.sourceName
1040
- ? `${schemaName}: ${schemaSource.sourceName}`
1041
- : schemaSource?.tag
1042
- ? `${schemaName}: ${schemaSource.tag}`
1043
- : schemaName
1044
- schemaGrid.appendChild(createSourceRow(display, schemaSource, engine))
1045
- })
1046
- sourceSection.appendChild(schemaGrid)
1047
- }
1048
-
1049
- content.appendChild(sourceSection)
1249
+ if (source?.definition) {
1250
+ overview.appendChild(createSourceRow('definition', source.definition, engine))
1050
1251
  }
1051
1252
 
1052
- const separatedSchemas = renderSeparatedSchemas(flatSchema, engine, selectedIds)
1253
+ const separatedSchemas = renderSeparatedSchemas(flatSchema, engine, selectedIds, source)
1053
1254
  if (separatedSchemas) {
1054
1255
  content.appendChild(separatedSchemas)
1055
1256
  }
@@ -1060,8 +1261,8 @@
1060
1261
 
1061
1262
  function renderResults() {
1062
1263
  const engine = createSearchEngine(searchInput.value.trim(), {
1063
- caseSensitive: caseSensitiveInput.checked,
1064
- regex: regexSearchInput.checked,
1264
+ caseSensitive: isPressed(caseSensitiveToggle),
1265
+ regex: isPressed(regexSearchToggle),
1065
1266
  })
1066
1267
 
1067
1268
  if (engine.error) {
@@ -1094,65 +1295,136 @@
1094
1295
  syncFilterStateToUrl()
1095
1296
  }
1096
1297
 
1097
- function renderFieldCheckboxes() {
1098
- fieldCheckboxes.innerHTML = ''
1099
- SEARCH_FIELDS.forEach((field) => {
1100
- const label = el('label', 'field-item')
1101
- const input = document.createElement('input')
1102
- input.type = 'checkbox'
1103
- input.id = `field-${field.id}`
1104
- input.checked = true
1105
- input.addEventListener('change', renderResults)
1106
- label.appendChild(input)
1107
- label.appendChild(el('span', '', field.label))
1108
- fieldCheckboxes.appendChild(label)
1109
- })
1298
+ function selectedFieldIds() {
1299
+ return SEARCH_FIELDS.filter((field) => state.selectedFieldIds.has(field.id)).map(
1300
+ (field) => field.id,
1301
+ )
1110
1302
  }
1111
1303
 
1112
- function setFieldSelection(allowedIds) {
1113
- SEARCH_FIELDS.forEach((field) => {
1114
- const input = document.getElementById(`field-${field.id}`)
1115
- if (!input) return
1116
- input.checked = allowedIds.has(field.id)
1304
+ function renderFieldChips() {
1305
+ fieldChipGroups.innerHTML = ''
1306
+ FIELD_GROUPS.forEach((group) => {
1307
+ const groupWrap = el('div', 'scope-group')
1308
+ groupWrap.appendChild(el('div', 'scope-group-title', group.label))
1309
+ const chipsWrap = el('div', 'scope-group-chips')
1310
+
1311
+ group.fieldIds.forEach((fieldId) => {
1312
+ const field = SEARCH_FIELDS.find((item) => item.id === fieldId)
1313
+ if (!field) return
1314
+ const chip = document.createElement('button')
1315
+ chip.type = 'button'
1316
+ chip.className = 'field-chip'
1317
+ chip.textContent = field.label
1318
+ setPressed(chip, state.selectedFieldIds.has(field.id))
1319
+ chip.addEventListener('click', () => {
1320
+ if (state.selectedFieldIds.has(field.id)) {
1321
+ state.selectedFieldIds.delete(field.id)
1322
+ } else {
1323
+ state.selectedFieldIds.add(field.id)
1324
+ }
1325
+ renderFieldChips()
1326
+ renderResults()
1327
+ })
1328
+ chipsWrap.appendChild(chip)
1329
+ })
1330
+
1331
+ groupWrap.appendChild(chipsWrap)
1332
+ fieldChipGroups.appendChild(groupWrap)
1117
1333
  })
1118
- renderResults()
1334
+ }
1335
+
1336
+ function setFieldSelection(allowedIds, options = {}) {
1337
+ const { rerender = true } = options
1338
+ state.selectedFieldIds = new Set(
1339
+ SEARCH_FIELDS.map((field) => field.id).filter((id) => allowedIds.has(id)),
1340
+ )
1341
+ renderFieldChips()
1342
+ if (rerender) renderResults()
1119
1343
  }
1120
1344
 
1121
1345
  function resetFiltersToDefault() {
1122
1346
  searchInput.value = ''
1123
- caseSensitiveInput.checked = false
1124
- regexSearchInput.checked = false
1347
+ setPressed(caseSensitiveToggle, false)
1348
+ setPressed(regexSearchToggle, false)
1125
1349
  typeMatchModeInput.value = 'contains'
1126
- schemaRowsMatchOnlyInput.checked = false
1127
- setFieldSelection(new Set(SEARCH_FIELDS.map((field) => field.id)))
1350
+ setPressed(schemaRowsMatchOnlyToggle, false)
1351
+ setFieldSelection(new Set(SEARCH_FIELDS.map((field) => field.id)), { rerender: false })
1352
+ renderResults()
1128
1353
  }
1129
1354
 
1130
1355
  function renderActiveFilterChips({ selectedIds, hasRegexError }) {
1131
1356
  activeFilterChips.innerHTML = ''
1132
- const chips = []
1133
-
1134
1357
  const searchValue = searchInput.value.trim()
1135
-
1136
- if (searchValue) chips.push(`search: ${searchValue}`)
1137
- if (typeMatchModeInput.value === 'exact') chips.push('type mode: exact')
1138
- if (schemaRowsMatchOnlyInput.checked) chips.push('schema rows: matched only')
1139
- if (caseSensitiveInput.checked) chips.push('case sensitive')
1140
- if (regexSearchInput.checked) chips.push('regex')
1141
- if (hasRegexError) chips.push('regex error')
1358
+ const chipModels = []
1359
+
1360
+ if (searchValue) {
1361
+ chipModels.push({
1362
+ text: `search: ${searchValue}`,
1363
+ onClick: () => {
1364
+ searchInput.value = ''
1365
+ renderResults()
1366
+ },
1367
+ })
1368
+ }
1369
+ if (typeMatchModeInput.value === 'exact') {
1370
+ chipModels.push({
1371
+ text: 'type mode: exact',
1372
+ onClick: () => {
1373
+ typeMatchModeInput.value = 'contains'
1374
+ renderResults()
1375
+ },
1376
+ })
1377
+ }
1378
+ if (isPressed(schemaRowsMatchOnlyToggle)) {
1379
+ chipModels.push({
1380
+ text: 'schema rows: matched only',
1381
+ onClick: () => {
1382
+ setPressed(schemaRowsMatchOnlyToggle, false)
1383
+ renderResults()
1384
+ },
1385
+ })
1386
+ }
1387
+ if (isPressed(caseSensitiveToggle)) {
1388
+ chipModels.push({
1389
+ text: 'case sensitive',
1390
+ onClick: () => {
1391
+ setPressed(caseSensitiveToggle, false)
1392
+ renderResults()
1393
+ },
1394
+ })
1395
+ }
1396
+ if (isPressed(regexSearchToggle)) {
1397
+ chipModels.push({
1398
+ text: 'regex',
1399
+ onClick: () => {
1400
+ setPressed(regexSearchToggle, false)
1401
+ renderResults()
1402
+ },
1403
+ })
1404
+ }
1405
+ if (hasRegexError) chipModels.push({ text: 'regex error' })
1142
1406
 
1143
1407
  const allIds = SEARCH_FIELDS.map((field) => field.id)
1144
1408
  if (selectedIds.length !== allIds.length) {
1145
- chips.push(`fields: ${selectedIds.join(', ') || 'none'}`)
1409
+ chipModels.push({
1410
+ text: `fields: ${selectedIds.join(', ') || 'none'}`,
1411
+ onClick: () => setFieldSelection(new Set(allIds)),
1412
+ })
1146
1413
  }
1147
1414
 
1148
- if (chips.length === 0) {
1415
+ if (chipModels.length === 0) {
1149
1416
  activeFilterChips.appendChild(el('span', 'empty', 'No active filters'))
1150
1417
  return
1151
1418
  }
1152
1419
 
1153
- chips.forEach((chipText) => {
1154
- const chip = el('span', 'chip')
1155
- chip.textContent = chipText
1420
+ chipModels.forEach((chipModel) => {
1421
+ const chip = document.createElement(chipModel.onClick ? 'button' : 'span')
1422
+ chip.className = chipModel.onClick ? 'chip chip-btn' : 'chip'
1423
+ chip.textContent = chipModel.text
1424
+ if (chipModel.onClick) {
1425
+ chip.type = 'button'
1426
+ chip.addEventListener('click', chipModel.onClick)
1427
+ }
1156
1428
  activeFilterChips.appendChild(chip)
1157
1429
  })
1158
1430
  }
@@ -1160,10 +1432,10 @@
1160
1432
  function collectFilterState() {
1161
1433
  return {
1162
1434
  search: searchInput.value,
1163
- caseSensitive: caseSensitiveInput.checked,
1164
- regex: regexSearchInput.checked,
1435
+ caseSensitive: isPressed(caseSensitiveToggle),
1436
+ regex: isPressed(regexSearchToggle),
1165
1437
  typeMatchMode: typeMatchModeInput.value === 'exact' ? 'exact' : 'contains',
1166
- schemaRowsMatchOnly: schemaRowsMatchOnlyInput.checked,
1438
+ schemaRowsMatchOnly: isPressed(schemaRowsMatchOnlyToggle),
1167
1439
  selectedFields: selectedFieldIds(),
1168
1440
  }
1169
1441
  }
@@ -1190,20 +1462,15 @@
1190
1462
  const parsed = JSON.parse(decodeURIComponent(raw))
1191
1463
  isHydratingFromUrl = true
1192
1464
  if (typeof parsed.search === 'string') searchInput.value = parsed.search
1193
- caseSensitiveInput.checked = Boolean(parsed.caseSensitive)
1194
- regexSearchInput.checked = Boolean(parsed.regex)
1465
+ setPressed(caseSensitiveToggle, Boolean(parsed.caseSensitive))
1466
+ setPressed(regexSearchToggle, Boolean(parsed.regex))
1195
1467
  if (parsed.typeMatchMode === 'exact' || parsed.typeMatchMode === 'contains') {
1196
1468
  typeMatchModeInput.value = parsed.typeMatchMode
1197
1469
  }
1198
- schemaRowsMatchOnlyInput.checked = Boolean(parsed.schemaRowsMatchOnly)
1470
+ setPressed(schemaRowsMatchOnlyToggle, Boolean(parsed.schemaRowsMatchOnly))
1199
1471
 
1200
1472
  if (Array.isArray(parsed.selectedFields)) {
1201
- const allowed = new Set(parsed.selectedFields)
1202
- SEARCH_FIELDS.forEach((field) => {
1203
- const input = document.getElementById(`field-${field.id}`)
1204
- if (!input) return
1205
- input.checked = allowed.has(field.id)
1206
- })
1473
+ setFieldSelection(new Set(parsed.selectedFields), { rerender: false })
1207
1474
  }
1208
1475
  } catch (error) {
1209
1476
  // Ignore malformed hash state.
@@ -1247,20 +1514,30 @@
1247
1514
  })
1248
1515
 
1249
1516
  searchInput.addEventListener('input', renderResults)
1250
- caseSensitiveInput.addEventListener('change', renderResults)
1251
- regexSearchInput.addEventListener('change', renderResults)
1517
+ caseSensitiveToggle.addEventListener('click', () => {
1518
+ togglePressed(caseSensitiveToggle)
1519
+ renderResults()
1520
+ })
1521
+ regexSearchToggle.addEventListener('click', () => {
1522
+ togglePressed(regexSearchToggle)
1523
+ renderResults()
1524
+ })
1252
1525
  typeMatchModeInput.addEventListener('change', renderResults)
1253
- schemaRowsMatchOnlyInput.addEventListener('change', renderResults)
1526
+ schemaRowsMatchOnlyToggle.addEventListener('click', () => {
1527
+ togglePressed(schemaRowsMatchOnlyToggle)
1528
+ renderResults()
1529
+ })
1254
1530
 
1255
1531
  selectAllFieldsBtn.addEventListener('click', () =>
1256
1532
  setFieldSelection(new Set(SEARCH_FIELDS.map((field) => field.id))),
1257
1533
  )
1258
1534
  clearAllFieldsBtn.addEventListener('click', () => setFieldSelection(new Set()))
1259
- schemasOnlyFieldsBtn.addEventListener('click', () => setFieldSelection(SCHEMA_FIELD_IDS))
1260
- metadataOnlyFieldsBtn.addEventListener('click', () => setFieldSelection(METADATA_FIELD_IDS))
1535
+ coreFieldsBtn.addEventListener('click', () => setFieldSelection(CORE_FIELD_IDS))
1536
+ schemasOnlyFieldsBtn.addEventListener('click', () => setFieldSelection(SCHEMA_SCOPE_FIELD_IDS))
1537
+ sourceOnlyFieldsBtn.addEventListener('click', () => setFieldSelection(SOURCE_FIELD_IDS))
1261
1538
  resetFiltersBtn.addEventListener('click', resetFiltersToDefault)
1262
1539
 
1263
- renderFieldCheckboxes()
1540
+ renderFieldChips()
1264
1541
  hydrateFilterStateFromUrl()
1265
1542
  initializeFromBakedPayload()
1266
1543
  renderResults()