@emeryld/rrroutes-contract 2.7.10 → 2.7.12

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
@@ -773,12 +962,27 @@
773
962
  function sourceHref(source) {
774
963
  if (!source || !source.file) return undefined
775
964
  const normalizedPath = String(source.file).replace(/\\/g, '/')
776
- const prefix = normalizedPath.startsWith('/') ? 'file://' : 'file:///'
777
- return `${prefix}${encodeURI(normalizedPath)}`
965
+ const platform =
966
+ (navigator.userAgentData && navigator.userAgentData.platform) ||
967
+ navigator.platform ||
968
+ ''
969
+ const isWindows = /win/i.test(platform)
970
+ const vscodePath = isWindows
971
+ ? normalizedPath.replace(/^\/+/, '')
972
+ : normalizedPath.startsWith('/')
973
+ ? normalizedPath
974
+ : `/${normalizedPath}`
975
+ const line = Number.isFinite(source.line) ? source.line : 1
976
+ const column = Number.isFinite(source.column) ? source.column : 1
977
+
978
+ return `vscode://file/${encodeURI(vscodePath)}:${line}:${column}`
778
979
  }
779
980
 
780
981
  function sourceDisplay(source) {
781
- return ''
982
+ if (!source || !source.file) return ''
983
+ const line = Number.isFinite(source.line) ? source.line : 1
984
+ const column = Number.isFinite(source.column) ? source.column : 1
985
+ return `${source.file}:${line}:${column}`
782
986
  }
783
987
 
784
988
  function createSourceRow(key, source, engine) {
@@ -829,6 +1033,28 @@
829
1033
  return result
830
1034
  }
831
1035
 
1036
+ const SCHEMA_SOURCE_KEYS_BY_SECTION = {
1037
+ params: 'paramsSchema',
1038
+ query: 'querySchema',
1039
+ body: 'bodySchema',
1040
+ output: 'outputSchema',
1041
+ }
1042
+
1043
+ function getSchemaSource(source, sectionName) {
1044
+ const schemaKey = SCHEMA_SOURCE_KEYS_BY_SECTION[sectionName]
1045
+ if (!schemaKey || !source?.schemas || typeof source.schemas !== 'object') {
1046
+ return {
1047
+ schemaKey,
1048
+ sourceValue: undefined,
1049
+ }
1050
+ }
1051
+
1052
+ return {
1053
+ schemaKey,
1054
+ sourceValue: source.schemas[schemaKey],
1055
+ }
1056
+ }
1057
+
832
1058
  function createTreeNode(name = '') {
833
1059
  return {
834
1060
  name,
@@ -916,14 +1142,14 @@
916
1142
  return row
917
1143
  }
918
1144
 
919
- function renderSeparatedSchemas(flatSchema, engine, selectedIds) {
1145
+ function renderSeparatedSchemas(flatSchema, engine, selectedIds, source) {
920
1146
  if (!flatSchema || typeof flatSchema !== 'object') return null
921
1147
  const section = el('div', 'section')
922
1148
  section.appendChild(el('h3', '', 'Schemas (separated by section)'))
923
1149
 
924
1150
  const grouped = splitFlatSchemaBySection(flatSchema)
925
1151
  let hasAnySchemaEntries = false
926
- const limitToMatchedRows = schemaRowsMatchOnlyInput.checked && engine.active
1152
+ const limitToMatchedRows = isPressed(schemaRowsMatchOnlyToggle) && engine.active
927
1153
 
928
1154
  SCHEMA_SECTIONS.forEach((sectionName) => {
929
1155
  const rawEntries = grouped[sectionName]
@@ -944,6 +1170,17 @@
944
1170
  setHighlighted(header, sectionName, engine)
945
1171
  block.appendChild(header)
946
1172
 
1173
+ const schemaSource = getSchemaSource(source, sectionName)
1174
+ if (schemaSource.sourceValue) {
1175
+ block.appendChild(
1176
+ createSourceRow(
1177
+ schemaSource.schemaKey || `${sectionName}Schema`,
1178
+ schemaSource.sourceValue,
1179
+ engine,
1180
+ ),
1181
+ )
1182
+ }
1183
+
947
1184
  const tree = buildSchemaTree(entries, sectionName)
948
1185
  block.appendChild(renderTreeNode(tree, engine, true))
949
1186
 
@@ -977,9 +1214,9 @@
977
1214
 
978
1215
  const iconRow = el('div', 'icon-row')
979
1216
  if (cfg.feed) {
980
- const feed = el('span', 'icon-badge')
981
- feed.title = 'Feed'
982
- setHighlighted(feed, 'F', engine)
1217
+ const feed = el('span', 'icon-badge feed')
1218
+ feed.title = 'Feed endpoint'
1219
+ setHighlighted(feed, 'Feed', engine)
983
1220
  iconRow.appendChild(feed)
984
1221
  }
985
1222
  if (cfg.deprecated) {
@@ -1014,30 +1251,11 @@
1014
1251
 
1015
1252
  const sourceByLeaf = state.payload?.sourceByLeaf || {}
1016
1253
  const source = sourceByLeaf[leaf.key]
1017
- if (source) {
1018
- const sourceSection = el('div', 'section')
1019
- sourceSection.appendChild(el('h3', '', 'Source'))
1020
- sourceSection.appendChild(createSourceRow('definition', source.definition, engine))
1021
-
1022
- const schemaSources = source.schemas || {}
1023
- const schemaEntries = Object.entries(schemaSources)
1024
- if (schemaEntries.length > 0) {
1025
- const schemaGrid = el('div', 'grid-2')
1026
- schemaEntries.forEach(([schemaName, schemaSource]) => {
1027
- const display = schemaSource?.sourceName
1028
- ? `${schemaName}: ${schemaSource.sourceName}`
1029
- : schemaSource?.tag
1030
- ? `${schemaName}: ${schemaSource.tag}`
1031
- : schemaName
1032
- schemaGrid.appendChild(createSourceRow(display, schemaSource, engine))
1033
- })
1034
- sourceSection.appendChild(schemaGrid)
1035
- }
1036
-
1037
- content.appendChild(sourceSection)
1254
+ if (source?.definition) {
1255
+ overview.appendChild(createSourceRow('definition', source.definition, engine))
1038
1256
  }
1039
1257
 
1040
- const separatedSchemas = renderSeparatedSchemas(flatSchema, engine, selectedIds)
1258
+ const separatedSchemas = renderSeparatedSchemas(flatSchema, engine, selectedIds, source)
1041
1259
  if (separatedSchemas) {
1042
1260
  content.appendChild(separatedSchemas)
1043
1261
  }
@@ -1048,8 +1266,8 @@
1048
1266
 
1049
1267
  function renderResults() {
1050
1268
  const engine = createSearchEngine(searchInput.value.trim(), {
1051
- caseSensitive: caseSensitiveInput.checked,
1052
- regex: regexSearchInput.checked,
1269
+ caseSensitive: isPressed(caseSensitiveToggle),
1270
+ regex: isPressed(regexSearchToggle),
1053
1271
  })
1054
1272
 
1055
1273
  if (engine.error) {
@@ -1082,65 +1300,136 @@
1082
1300
  syncFilterStateToUrl()
1083
1301
  }
1084
1302
 
1085
- function renderFieldCheckboxes() {
1086
- fieldCheckboxes.innerHTML = ''
1087
- SEARCH_FIELDS.forEach((field) => {
1088
- const label = el('label', 'field-item')
1089
- const input = document.createElement('input')
1090
- input.type = 'checkbox'
1091
- input.id = `field-${field.id}`
1092
- input.checked = true
1093
- input.addEventListener('change', renderResults)
1094
- label.appendChild(input)
1095
- label.appendChild(el('span', '', field.label))
1096
- fieldCheckboxes.appendChild(label)
1097
- })
1303
+ function selectedFieldIds() {
1304
+ return SEARCH_FIELDS.filter((field) => state.selectedFieldIds.has(field.id)).map(
1305
+ (field) => field.id,
1306
+ )
1098
1307
  }
1099
1308
 
1100
- function setFieldSelection(allowedIds) {
1101
- SEARCH_FIELDS.forEach((field) => {
1102
- const input = document.getElementById(`field-${field.id}`)
1103
- if (!input) return
1104
- input.checked = allowedIds.has(field.id)
1309
+ function renderFieldChips() {
1310
+ fieldChipGroups.innerHTML = ''
1311
+ FIELD_GROUPS.forEach((group) => {
1312
+ const groupWrap = el('div', 'scope-group')
1313
+ groupWrap.appendChild(el('div', 'scope-group-title', group.label))
1314
+ const chipsWrap = el('div', 'scope-group-chips')
1315
+
1316
+ group.fieldIds.forEach((fieldId) => {
1317
+ const field = SEARCH_FIELDS.find((item) => item.id === fieldId)
1318
+ if (!field) return
1319
+ const chip = document.createElement('button')
1320
+ chip.type = 'button'
1321
+ chip.className = 'field-chip'
1322
+ chip.textContent = field.label
1323
+ setPressed(chip, state.selectedFieldIds.has(field.id))
1324
+ chip.addEventListener('click', () => {
1325
+ if (state.selectedFieldIds.has(field.id)) {
1326
+ state.selectedFieldIds.delete(field.id)
1327
+ } else {
1328
+ state.selectedFieldIds.add(field.id)
1329
+ }
1330
+ renderFieldChips()
1331
+ renderResults()
1332
+ })
1333
+ chipsWrap.appendChild(chip)
1334
+ })
1335
+
1336
+ groupWrap.appendChild(chipsWrap)
1337
+ fieldChipGroups.appendChild(groupWrap)
1105
1338
  })
1106
- renderResults()
1339
+ }
1340
+
1341
+ function setFieldSelection(allowedIds, options = {}) {
1342
+ const { rerender = true } = options
1343
+ state.selectedFieldIds = new Set(
1344
+ SEARCH_FIELDS.map((field) => field.id).filter((id) => allowedIds.has(id)),
1345
+ )
1346
+ renderFieldChips()
1347
+ if (rerender) renderResults()
1107
1348
  }
1108
1349
 
1109
1350
  function resetFiltersToDefault() {
1110
1351
  searchInput.value = ''
1111
- caseSensitiveInput.checked = false
1112
- regexSearchInput.checked = false
1352
+ setPressed(caseSensitiveToggle, false)
1353
+ setPressed(regexSearchToggle, false)
1113
1354
  typeMatchModeInput.value = 'contains'
1114
- schemaRowsMatchOnlyInput.checked = false
1115
- setFieldSelection(new Set(SEARCH_FIELDS.map((field) => field.id)))
1355
+ setPressed(schemaRowsMatchOnlyToggle, false)
1356
+ setFieldSelection(new Set(SEARCH_FIELDS.map((field) => field.id)), { rerender: false })
1357
+ renderResults()
1116
1358
  }
1117
1359
 
1118
1360
  function renderActiveFilterChips({ selectedIds, hasRegexError }) {
1119
1361
  activeFilterChips.innerHTML = ''
1120
- const chips = []
1121
-
1122
1362
  const searchValue = searchInput.value.trim()
1123
-
1124
- if (searchValue) chips.push(`search: ${searchValue}`)
1125
- if (typeMatchModeInput.value === 'exact') chips.push('type mode: exact')
1126
- if (schemaRowsMatchOnlyInput.checked) chips.push('schema rows: matched only')
1127
- if (caseSensitiveInput.checked) chips.push('case sensitive')
1128
- if (regexSearchInput.checked) chips.push('regex')
1129
- if (hasRegexError) chips.push('regex error')
1363
+ const chipModels = []
1364
+
1365
+ if (searchValue) {
1366
+ chipModels.push({
1367
+ text: `search: ${searchValue}`,
1368
+ onClick: () => {
1369
+ searchInput.value = ''
1370
+ renderResults()
1371
+ },
1372
+ })
1373
+ }
1374
+ if (typeMatchModeInput.value === 'exact') {
1375
+ chipModels.push({
1376
+ text: 'type mode: exact',
1377
+ onClick: () => {
1378
+ typeMatchModeInput.value = 'contains'
1379
+ renderResults()
1380
+ },
1381
+ })
1382
+ }
1383
+ if (isPressed(schemaRowsMatchOnlyToggle)) {
1384
+ chipModels.push({
1385
+ text: 'schema rows: matched only',
1386
+ onClick: () => {
1387
+ setPressed(schemaRowsMatchOnlyToggle, false)
1388
+ renderResults()
1389
+ },
1390
+ })
1391
+ }
1392
+ if (isPressed(caseSensitiveToggle)) {
1393
+ chipModels.push({
1394
+ text: 'case sensitive',
1395
+ onClick: () => {
1396
+ setPressed(caseSensitiveToggle, false)
1397
+ renderResults()
1398
+ },
1399
+ })
1400
+ }
1401
+ if (isPressed(regexSearchToggle)) {
1402
+ chipModels.push({
1403
+ text: 'regex',
1404
+ onClick: () => {
1405
+ setPressed(regexSearchToggle, false)
1406
+ renderResults()
1407
+ },
1408
+ })
1409
+ }
1410
+ if (hasRegexError) chipModels.push({ text: 'regex error' })
1130
1411
 
1131
1412
  const allIds = SEARCH_FIELDS.map((field) => field.id)
1132
1413
  if (selectedIds.length !== allIds.length) {
1133
- chips.push(`fields: ${selectedIds.join(', ') || 'none'}`)
1414
+ chipModels.push({
1415
+ text: `fields: ${selectedIds.join(', ') || 'none'}`,
1416
+ onClick: () => setFieldSelection(new Set(allIds)),
1417
+ })
1134
1418
  }
1135
1419
 
1136
- if (chips.length === 0) {
1420
+ if (chipModels.length === 0) {
1137
1421
  activeFilterChips.appendChild(el('span', 'empty', 'No active filters'))
1138
1422
  return
1139
1423
  }
1140
1424
 
1141
- chips.forEach((chipText) => {
1142
- const chip = el('span', 'chip')
1143
- chip.textContent = chipText
1425
+ chipModels.forEach((chipModel) => {
1426
+ const chip = document.createElement(chipModel.onClick ? 'button' : 'span')
1427
+ chip.className = chipModel.onClick ? 'chip chip-btn' : 'chip'
1428
+ chip.textContent = chipModel.text
1429
+ if (chipModel.onClick) {
1430
+ chip.type = 'button'
1431
+ chip.addEventListener('click', chipModel.onClick)
1432
+ }
1144
1433
  activeFilterChips.appendChild(chip)
1145
1434
  })
1146
1435
  }
@@ -1148,10 +1437,10 @@
1148
1437
  function collectFilterState() {
1149
1438
  return {
1150
1439
  search: searchInput.value,
1151
- caseSensitive: caseSensitiveInput.checked,
1152
- regex: regexSearchInput.checked,
1440
+ caseSensitive: isPressed(caseSensitiveToggle),
1441
+ regex: isPressed(regexSearchToggle),
1153
1442
  typeMatchMode: typeMatchModeInput.value === 'exact' ? 'exact' : 'contains',
1154
- schemaRowsMatchOnly: schemaRowsMatchOnlyInput.checked,
1443
+ schemaRowsMatchOnly: isPressed(schemaRowsMatchOnlyToggle),
1155
1444
  selectedFields: selectedFieldIds(),
1156
1445
  }
1157
1446
  }
@@ -1178,20 +1467,15 @@
1178
1467
  const parsed = JSON.parse(decodeURIComponent(raw))
1179
1468
  isHydratingFromUrl = true
1180
1469
  if (typeof parsed.search === 'string') searchInput.value = parsed.search
1181
- caseSensitiveInput.checked = Boolean(parsed.caseSensitive)
1182
- regexSearchInput.checked = Boolean(parsed.regex)
1470
+ setPressed(caseSensitiveToggle, Boolean(parsed.caseSensitive))
1471
+ setPressed(regexSearchToggle, Boolean(parsed.regex))
1183
1472
  if (parsed.typeMatchMode === 'exact' || parsed.typeMatchMode === 'contains') {
1184
1473
  typeMatchModeInput.value = parsed.typeMatchMode
1185
1474
  }
1186
- schemaRowsMatchOnlyInput.checked = Boolean(parsed.schemaRowsMatchOnly)
1475
+ setPressed(schemaRowsMatchOnlyToggle, Boolean(parsed.schemaRowsMatchOnly))
1187
1476
 
1188
1477
  if (Array.isArray(parsed.selectedFields)) {
1189
- const allowed = new Set(parsed.selectedFields)
1190
- SEARCH_FIELDS.forEach((field) => {
1191
- const input = document.getElementById(`field-${field.id}`)
1192
- if (!input) return
1193
- input.checked = allowed.has(field.id)
1194
- })
1478
+ setFieldSelection(new Set(parsed.selectedFields), { rerender: false })
1195
1479
  }
1196
1480
  } catch (error) {
1197
1481
  // Ignore malformed hash state.
@@ -1235,20 +1519,30 @@
1235
1519
  })
1236
1520
 
1237
1521
  searchInput.addEventListener('input', renderResults)
1238
- caseSensitiveInput.addEventListener('change', renderResults)
1239
- regexSearchInput.addEventListener('change', renderResults)
1522
+ caseSensitiveToggle.addEventListener('click', () => {
1523
+ togglePressed(caseSensitiveToggle)
1524
+ renderResults()
1525
+ })
1526
+ regexSearchToggle.addEventListener('click', () => {
1527
+ togglePressed(regexSearchToggle)
1528
+ renderResults()
1529
+ })
1240
1530
  typeMatchModeInput.addEventListener('change', renderResults)
1241
- schemaRowsMatchOnlyInput.addEventListener('change', renderResults)
1531
+ schemaRowsMatchOnlyToggle.addEventListener('click', () => {
1532
+ togglePressed(schemaRowsMatchOnlyToggle)
1533
+ renderResults()
1534
+ })
1242
1535
 
1243
1536
  selectAllFieldsBtn.addEventListener('click', () =>
1244
1537
  setFieldSelection(new Set(SEARCH_FIELDS.map((field) => field.id))),
1245
1538
  )
1246
1539
  clearAllFieldsBtn.addEventListener('click', () => setFieldSelection(new Set()))
1247
- schemasOnlyFieldsBtn.addEventListener('click', () => setFieldSelection(SCHEMA_FIELD_IDS))
1248
- metadataOnlyFieldsBtn.addEventListener('click', () => setFieldSelection(METADATA_FIELD_IDS))
1540
+ coreFieldsBtn.addEventListener('click', () => setFieldSelection(CORE_FIELD_IDS))
1541
+ schemasOnlyFieldsBtn.addEventListener('click', () => setFieldSelection(SCHEMA_SCOPE_FIELD_IDS))
1542
+ sourceOnlyFieldsBtn.addEventListener('click', () => setFieldSelection(SOURCE_FIELD_IDS))
1249
1543
  resetFiltersBtn.addEventListener('click', resetFiltersToDefault)
1250
1544
 
1251
- renderFieldCheckboxes()
1545
+ renderFieldChips()
1252
1546
  hydrateFilterStateFromUrl()
1253
1547
  initializeFromBakedPayload()
1254
1548
  renderResults()