@emeryld/rrroutes-contract 2.7.11 → 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>
360
- </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>
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" />
367
477
  </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>
372
493
  </div>
373
494
 
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>
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>
504
+ </div>
505
+
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
@@ -790,7 +979,10 @@
790
979
  }
791
980
 
792
981
  function sourceDisplay(source) {
793
- 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}`
794
986
  }
795
987
 
796
988
  function createSourceRow(key, source, engine) {
@@ -841,6 +1033,28 @@
841
1033
  return result
842
1034
  }
843
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
+
844
1058
  function createTreeNode(name = '') {
845
1059
  return {
846
1060
  name,
@@ -928,14 +1142,14 @@
928
1142
  return row
929
1143
  }
930
1144
 
931
- function renderSeparatedSchemas(flatSchema, engine, selectedIds) {
1145
+ function renderSeparatedSchemas(flatSchema, engine, selectedIds, source) {
932
1146
  if (!flatSchema || typeof flatSchema !== 'object') return null
933
1147
  const section = el('div', 'section')
934
1148
  section.appendChild(el('h3', '', 'Schemas (separated by section)'))
935
1149
 
936
1150
  const grouped = splitFlatSchemaBySection(flatSchema)
937
1151
  let hasAnySchemaEntries = false
938
- const limitToMatchedRows = schemaRowsMatchOnlyInput.checked && engine.active
1152
+ const limitToMatchedRows = isPressed(schemaRowsMatchOnlyToggle) && engine.active
939
1153
 
940
1154
  SCHEMA_SECTIONS.forEach((sectionName) => {
941
1155
  const rawEntries = grouped[sectionName]
@@ -956,6 +1170,17 @@
956
1170
  setHighlighted(header, sectionName, engine)
957
1171
  block.appendChild(header)
958
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
+
959
1184
  const tree = buildSchemaTree(entries, sectionName)
960
1185
  block.appendChild(renderTreeNode(tree, engine, true))
961
1186
 
@@ -989,9 +1214,9 @@
989
1214
 
990
1215
  const iconRow = el('div', 'icon-row')
991
1216
  if (cfg.feed) {
992
- const feed = el('span', 'icon-badge')
993
- feed.title = 'Feed'
994
- setHighlighted(feed, 'F', engine)
1217
+ const feed = el('span', 'icon-badge feed')
1218
+ feed.title = 'Feed endpoint'
1219
+ setHighlighted(feed, 'Feed', engine)
995
1220
  iconRow.appendChild(feed)
996
1221
  }
997
1222
  if (cfg.deprecated) {
@@ -1026,30 +1251,11 @@
1026
1251
 
1027
1252
  const sourceByLeaf = state.payload?.sourceByLeaf || {}
1028
1253
  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)
1254
+ if (source?.definition) {
1255
+ overview.appendChild(createSourceRow('definition', source.definition, engine))
1050
1256
  }
1051
1257
 
1052
- const separatedSchemas = renderSeparatedSchemas(flatSchema, engine, selectedIds)
1258
+ const separatedSchemas = renderSeparatedSchemas(flatSchema, engine, selectedIds, source)
1053
1259
  if (separatedSchemas) {
1054
1260
  content.appendChild(separatedSchemas)
1055
1261
  }
@@ -1060,8 +1266,8 @@
1060
1266
 
1061
1267
  function renderResults() {
1062
1268
  const engine = createSearchEngine(searchInput.value.trim(), {
1063
- caseSensitive: caseSensitiveInput.checked,
1064
- regex: regexSearchInput.checked,
1269
+ caseSensitive: isPressed(caseSensitiveToggle),
1270
+ regex: isPressed(regexSearchToggle),
1065
1271
  })
1066
1272
 
1067
1273
  if (engine.error) {
@@ -1094,65 +1300,136 @@
1094
1300
  syncFilterStateToUrl()
1095
1301
  }
1096
1302
 
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
- })
1303
+ function selectedFieldIds() {
1304
+ return SEARCH_FIELDS.filter((field) => state.selectedFieldIds.has(field.id)).map(
1305
+ (field) => field.id,
1306
+ )
1110
1307
  }
1111
1308
 
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)
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)
1117
1338
  })
1118
- 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()
1119
1348
  }
1120
1349
 
1121
1350
  function resetFiltersToDefault() {
1122
1351
  searchInput.value = ''
1123
- caseSensitiveInput.checked = false
1124
- regexSearchInput.checked = false
1352
+ setPressed(caseSensitiveToggle, false)
1353
+ setPressed(regexSearchToggle, false)
1125
1354
  typeMatchModeInput.value = 'contains'
1126
- schemaRowsMatchOnlyInput.checked = false
1127
- 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()
1128
1358
  }
1129
1359
 
1130
1360
  function renderActiveFilterChips({ selectedIds, hasRegexError }) {
1131
1361
  activeFilterChips.innerHTML = ''
1132
- const chips = []
1133
-
1134
1362
  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')
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' })
1142
1411
 
1143
1412
  const allIds = SEARCH_FIELDS.map((field) => field.id)
1144
1413
  if (selectedIds.length !== allIds.length) {
1145
- chips.push(`fields: ${selectedIds.join(', ') || 'none'}`)
1414
+ chipModels.push({
1415
+ text: `fields: ${selectedIds.join(', ') || 'none'}`,
1416
+ onClick: () => setFieldSelection(new Set(allIds)),
1417
+ })
1146
1418
  }
1147
1419
 
1148
- if (chips.length === 0) {
1420
+ if (chipModels.length === 0) {
1149
1421
  activeFilterChips.appendChild(el('span', 'empty', 'No active filters'))
1150
1422
  return
1151
1423
  }
1152
1424
 
1153
- chips.forEach((chipText) => {
1154
- const chip = el('span', 'chip')
1155
- 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
+ }
1156
1433
  activeFilterChips.appendChild(chip)
1157
1434
  })
1158
1435
  }
@@ -1160,10 +1437,10 @@
1160
1437
  function collectFilterState() {
1161
1438
  return {
1162
1439
  search: searchInput.value,
1163
- caseSensitive: caseSensitiveInput.checked,
1164
- regex: regexSearchInput.checked,
1440
+ caseSensitive: isPressed(caseSensitiveToggle),
1441
+ regex: isPressed(regexSearchToggle),
1165
1442
  typeMatchMode: typeMatchModeInput.value === 'exact' ? 'exact' : 'contains',
1166
- schemaRowsMatchOnly: schemaRowsMatchOnlyInput.checked,
1443
+ schemaRowsMatchOnly: isPressed(schemaRowsMatchOnlyToggle),
1167
1444
  selectedFields: selectedFieldIds(),
1168
1445
  }
1169
1446
  }
@@ -1190,20 +1467,15 @@
1190
1467
  const parsed = JSON.parse(decodeURIComponent(raw))
1191
1468
  isHydratingFromUrl = true
1192
1469
  if (typeof parsed.search === 'string') searchInput.value = parsed.search
1193
- caseSensitiveInput.checked = Boolean(parsed.caseSensitive)
1194
- regexSearchInput.checked = Boolean(parsed.regex)
1470
+ setPressed(caseSensitiveToggle, Boolean(parsed.caseSensitive))
1471
+ setPressed(regexSearchToggle, Boolean(parsed.regex))
1195
1472
  if (parsed.typeMatchMode === 'exact' || parsed.typeMatchMode === 'contains') {
1196
1473
  typeMatchModeInput.value = parsed.typeMatchMode
1197
1474
  }
1198
- schemaRowsMatchOnlyInput.checked = Boolean(parsed.schemaRowsMatchOnly)
1475
+ setPressed(schemaRowsMatchOnlyToggle, Boolean(parsed.schemaRowsMatchOnly))
1199
1476
 
1200
1477
  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
- })
1478
+ setFieldSelection(new Set(parsed.selectedFields), { rerender: false })
1207
1479
  }
1208
1480
  } catch (error) {
1209
1481
  // Ignore malformed hash state.
@@ -1247,20 +1519,30 @@
1247
1519
  })
1248
1520
 
1249
1521
  searchInput.addEventListener('input', renderResults)
1250
- caseSensitiveInput.addEventListener('change', renderResults)
1251
- regexSearchInput.addEventListener('change', renderResults)
1522
+ caseSensitiveToggle.addEventListener('click', () => {
1523
+ togglePressed(caseSensitiveToggle)
1524
+ renderResults()
1525
+ })
1526
+ regexSearchToggle.addEventListener('click', () => {
1527
+ togglePressed(regexSearchToggle)
1528
+ renderResults()
1529
+ })
1252
1530
  typeMatchModeInput.addEventListener('change', renderResults)
1253
- schemaRowsMatchOnlyInput.addEventListener('change', renderResults)
1531
+ schemaRowsMatchOnlyToggle.addEventListener('click', () => {
1532
+ togglePressed(schemaRowsMatchOnlyToggle)
1533
+ renderResults()
1534
+ })
1254
1535
 
1255
1536
  selectAllFieldsBtn.addEventListener('click', () =>
1256
1537
  setFieldSelection(new Set(SEARCH_FIELDS.map((field) => field.id))),
1257
1538
  )
1258
1539
  clearAllFieldsBtn.addEventListener('click', () => setFieldSelection(new Set()))
1259
- schemasOnlyFieldsBtn.addEventListener('click', () => setFieldSelection(SCHEMA_FIELD_IDS))
1260
- 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))
1261
1543
  resetFiltersBtn.addEventListener('click', resetFiltersToDefault)
1262
1544
 
1263
- renderFieldCheckboxes()
1545
+ renderFieldChips()
1264
1546
  hydrateFilterStateFromUrl()
1265
1547
  initializeFromBakedPayload()
1266
1548
  renderResults()