@emeryld/rrroutes-contract 2.7.4 → 2.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@emeryld/rrroutes-contract",
3
3
  "description": "TypeScript contract definitions for RRRoutes",
4
- "version": "2.7.4",
4
+ "version": "2.7.5",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "main": "dist/index.cjs",
@@ -140,6 +140,12 @@
140
140
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
141
141
  }
142
142
 
143
+ .grid-3 {
144
+ display: grid;
145
+ gap: 6px;
146
+ grid-template-columns: repeat(3, minmax(0, 1fr));
147
+ }
148
+
143
149
  .kv {
144
150
  border-bottom: 1px dashed #e2e8f2;
145
151
  padding: 4px 0 6px;
@@ -179,6 +185,30 @@
179
185
  background: #effbf4;
180
186
  }
181
187
 
188
+ .icon-row {
189
+ display: flex;
190
+ gap: 6px;
191
+ margin-top: 2px;
192
+ }
193
+
194
+ .icon-badge {
195
+ display: inline-flex;
196
+ align-items: center;
197
+ justify-content: center;
198
+ width: 18px;
199
+ height: 18px;
200
+ border: 1px solid #e0e6f0;
201
+ border-radius: 4px;
202
+ font-size: 11px;
203
+ background: #f6f8fc;
204
+ }
205
+
206
+ .icon-badge.warn {
207
+ border-color: #f2d5d5;
208
+ color: #a02424;
209
+ background: #fff4f4;
210
+ }
211
+
182
212
  .empty {
183
213
  color: var(--muted);
184
214
  }
@@ -244,6 +274,12 @@
244
274
  font-size: 11px;
245
275
  background: #f2f5fa;
246
276
  }
277
+
278
+ @media (max-width: 720px) {
279
+ .grid-3 {
280
+ grid-template-columns: 1fr;
281
+ }
282
+ }
247
283
  </style>
248
284
  </head>
249
285
  <body>
@@ -260,11 +296,6 @@
260
296
  <input id="searchInput" type="text" placeholder="Type to search..." />
261
297
  </label>
262
298
 
263
- <label>
264
- Type filter:
265
- <input id="typeFilterInput" type="text" placeholder='Type/kind/enum (e.g. "paid")' />
266
- </label>
267
-
268
299
  <div class="field-row">
269
300
  <label class="field-item">
270
301
  <input id="caseSensitive" type="checkbox" />
@@ -274,10 +305,6 @@
274
305
  <input id="regexSearch" type="checkbox" />
275
306
  <span>regex</span>
276
307
  </label>
277
- <label class="field-item">
278
- <input id="hasEnumOnly" type="checkbox" />
279
- <span>has enum only</span>
280
- </label>
281
308
  <label class="field-item">
282
309
  <span>type match</span>
283
310
  <select id="typeMatchMode">
@@ -319,6 +346,11 @@
319
346
  { id: 'tags', label: 'tags', get: (leaf) => leaf.cfg?.tags || [] },
320
347
  { id: 'stability', label: 'stability', get: (leaf) => [leaf.cfg?.stability] },
321
348
  { id: 'docsMeta', label: 'docsMeta', get: (leaf) => [leaf.cfg?.docsMeta] },
349
+ {
350
+ id: 'types',
351
+ label: 'types',
352
+ get: (leaf, schemaFlatByLeaf) => schemaTypeTokens(schemaFlatByLeaf?.[leaf.key]),
353
+ },
322
354
  {
323
355
  id: 'params',
324
356
  label: 'params',
@@ -355,10 +387,8 @@
355
387
 
356
388
  const fileInput = document.getElementById('fileInput')
357
389
  const searchInput = document.getElementById('searchInput')
358
- const typeFilterInput = document.getElementById('typeFilterInput')
359
390
  const caseSensitiveInput = document.getElementById('caseSensitive')
360
391
  const regexSearchInput = document.getElementById('regexSearch')
361
- const hasEnumOnlyInput = document.getElementById('hasEnumOnly')
362
392
  const typeMatchModeInput = document.getElementById('typeMatchMode')
363
393
  const fieldCheckboxes = document.getElementById('fieldCheckboxes')
364
394
  const activeFilterChips = document.getElementById('activeFilterChips')
@@ -394,6 +424,9 @@
394
424
  if (!query) {
395
425
  return {
396
426
  active: false,
427
+ query,
428
+ caseSensitive,
429
+ regex,
397
430
  error: null,
398
431
  test: () => true,
399
432
  highlight: (text) => escapeHtml(text ?? ''),
@@ -406,6 +439,9 @@
406
439
  const rx = new RegExp(query, flags)
407
440
  return {
408
441
  active: true,
442
+ query,
443
+ caseSensitive,
444
+ regex,
409
445
  error: null,
410
446
  test: (text) => {
411
447
  const source = String(text ?? '')
@@ -423,6 +459,9 @@
423
459
  } catch (error) {
424
460
  return {
425
461
  active: true,
462
+ query,
463
+ caseSensitive,
464
+ regex,
426
465
  error: error instanceof Error ? error.message : 'Invalid regex',
427
466
  test: () => false,
428
467
  highlight: (text) => escapeHtml(text ?? ''),
@@ -436,6 +475,9 @@
436
475
 
437
476
  return {
438
477
  active: true,
478
+ query,
479
+ caseSensitive,
480
+ regex,
439
481
  error: null,
440
482
  test: (text) => {
441
483
  const source = String(text ?? '')
@@ -499,23 +541,20 @@
499
541
  return flatSchemaToSearchTokens(Object.fromEntries(sectionEntries))
500
542
  }
501
543
 
502
- function typeFilterTokensByLeaf(flatSchema) {
503
- if (!flatSchema || typeof flatSchema !== 'object') return { tokens: [], hasEnum: false }
544
+ function schemaTypeTokens(flatSchema) {
545
+ if (!flatSchema || typeof flatSchema !== 'object') return []
504
546
 
505
547
  const tokens = new Set()
506
- let hasEnum = false
507
-
508
548
  Object.values(flatSchema).forEach((info) => {
509
549
  if (!info || typeof info !== 'object') return
510
550
  if (info.type) tokens.add(String(info.type))
511
551
  if (info.kind) tokens.add(String(info.kind))
512
- if (Array.isArray(info.enumValues) && info.enumValues.length > 0) {
513
- hasEnum = true
552
+ if (Array.isArray(info.enumValues)) {
514
553
  info.enumValues.forEach((value) => tokens.add(String(value)))
515
554
  }
516
555
  })
517
556
 
518
- return { tokens: Array.from(tokens), hasEnum }
557
+ return Array.from(tokens)
519
558
  }
520
559
 
521
560
  function selectedFieldIds() {
@@ -533,46 +572,17 @@
533
572
  return SEARCH_FIELDS.some((field) => {
534
573
  if (!selectedIds.includes(field.id)) return false
535
574
  const tokens = toTokens(field.get(leaf, schemaFlatByLeaf))
575
+ if (field.id === 'types' && typeMatchModeInput.value === 'exact' && !engine.regex) {
576
+ const needle = engine.caseSensitive ? engine.query : engine.query.toLowerCase()
577
+ const normalized = engine.caseSensitive
578
+ ? tokens
579
+ : tokens.map((token) => String(token).toLowerCase())
580
+ return normalized.includes(needle)
581
+ }
536
582
  return tokens.some((token) => engine.test(token))
537
583
  })
538
584
  }
539
585
 
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
-
576
586
  function el(tag, className, text) {
577
587
  const node = document.createElement(tag)
578
588
  if (className) node.className = className
@@ -728,29 +738,30 @@
728
738
  }
729
739
 
730
740
  function renderSeparatedSchemas(flatSchema, engine) {
741
+ if (!flatSchema || typeof flatSchema !== 'object') return null
731
742
  const section = el('div', 'section')
732
743
  section.appendChild(el('h3', '', 'Schemas (separated by section)'))
733
744
 
734
745
  const grouped = splitFlatSchemaBySection(flatSchema)
746
+ let hasAnySchemaEntries = false
735
747
 
736
748
  SCHEMA_SECTIONS.forEach((sectionName) => {
749
+ const entries = grouped[sectionName]
750
+ if (!entries || Object.keys(entries).length === 0) return
751
+
752
+ hasAnySchemaEntries = true
737
753
  const block = el('div', 'schema-block')
738
754
  const header = el('div', 'schema-header mono')
739
755
  setHighlighted(header, sectionName, engine)
740
756
  block.appendChild(header)
741
757
 
742
- const entries = grouped[sectionName]
743
- if (!entries || Object.keys(entries).length === 0) {
744
- block.appendChild(el('div', 'empty', 'No entries'))
745
- } else {
746
- const tree = buildSchemaTree(entries, sectionName)
747
- block.appendChild(renderTreeNode(tree, engine, true))
748
- }
758
+ const tree = buildSchemaTree(entries, sectionName)
759
+ block.appendChild(renderTreeNode(tree, engine, true))
749
760
 
750
761
  section.appendChild(block)
751
762
  })
752
763
 
753
- return section
764
+ return hasAnySchemaEntries ? section : null
754
765
  }
755
766
 
756
767
  function renderLeaf(leaf, engine) {
@@ -765,60 +776,43 @@
765
776
 
766
777
  const overview = el('div', 'section')
767
778
  overview.appendChild(el('h3', '', 'Overview'))
768
- const grid = el('div', 'grid-2')
769
- grid.appendChild(kv('key', leaf.key, engine))
770
- grid.appendChild(kv('method', leaf.method, engine))
771
- grid.appendChild(kv('path', leaf.path, engine))
772
- grid.appendChild(kv('group', cfg.docsGroup, engine))
773
- grid.appendChild(kv('stability', cfg.stability, engine))
774
- grid.appendChild(kv('feed', cfg.feed ? 'true' : 'false', engine))
775
- grid.appendChild(kv('deprecated', cfg.deprecated ? 'true' : 'false', engine))
776
- grid.appendChild(kv('hidden', cfg.docsHidden ? 'true' : 'false', engine))
777
- overview.appendChild(grid)
778
- content.appendChild(overview)
779
-
780
- const docs = el('div', 'section')
781
- docs.appendChild(el('h3', '', 'Documentation'))
782
- const docGrid = el('div', 'grid-2')
783
- docGrid.appendChild(kv('summary', cfg.summary, engine))
784
- docGrid.appendChild(kv('description', cfg.description, engine))
785
- docs.appendChild(docGrid)
786
-
787
- const tagsRow = el('div', 'chips')
788
- ;(cfg.tags || []).forEach((tag) => {
789
- const chip = el('span', 'chip')
790
- setHighlighted(chip, tag, engine)
791
- tagsRow.appendChild(chip)
792
- })
793
- if ((cfg.tags || []).length === 0) {
794
- tagsRow.appendChild(el('span', 'empty', 'No tags'))
779
+ const topGrid = el('div', 'grid-3')
780
+ topGrid.appendChild(kv('group', cfg.docsGroup, engine))
781
+ topGrid.appendChild(
782
+ kv('tags', cfg.tags && cfg.tags.length > 0 ? cfg.tags.join(', ') : undefined, engine),
783
+ )
784
+ topGrid.appendChild(kv('stability', cfg.stability, engine))
785
+ overview.appendChild(topGrid)
786
+ overview.appendChild(kv('summary', cfg.summary, engine))
787
+ overview.appendChild(kv('description', cfg.description, engine))
788
+
789
+ const iconRow = el('div', 'icon-row')
790
+ if (cfg.feed) {
791
+ const feed = el('span', 'icon-badge')
792
+ feed.title = 'Feed'
793
+ setHighlighted(feed, 'F', engine)
794
+ iconRow.appendChild(feed)
795
795
  }
796
- docs.appendChild(tagsRow)
797
-
798
- if (cfg.docsMeta && Object.keys(cfg.docsMeta).length > 0) {
799
- Object.entries(cfg.docsMeta).forEach(([k, v]) => {
800
- docs.appendChild(kv(`meta.${k}`, typeof v === 'string' ? v : JSON.stringify(v), engine))
801
- })
796
+ if (cfg.deprecated) {
797
+ const deprecated = el('span', 'icon-badge warn')
798
+ deprecated.title = 'Deprecated'
799
+ setHighlighted(deprecated, 'D', engine)
800
+ iconRow.appendChild(deprecated)
802
801
  }
803
-
804
- content.appendChild(docs)
805
-
806
- const schemas = el('div', 'section')
807
- schemas.appendChild(el('h3', '', 'Schema Summaries'))
808
- const schemaGrid = el('div', 'grid-2')
809
- const schemaObj = cfg.schemas || {}
810
- schemaGrid.appendChild(renderSchemaSummary('params', schemaObj.params, engine))
811
- schemaGrid.appendChild(renderSchemaSummary('query', schemaObj.query, engine))
812
- schemaGrid.appendChild(renderSchemaSummary('body', schemaObj.body, engine))
813
- schemaGrid.appendChild(renderSchemaSummary('output', schemaObj.output, engine))
814
- schemaGrid.appendChild(renderSchemaSummary('outputMeta', schemaObj.outputMeta, engine))
815
- schemaGrid.appendChild(renderSchemaSummary('queryExtension', schemaObj.queryExtension, engine))
816
- schemas.appendChild(schemaGrid)
817
- content.appendChild(schemas)
802
+ if (cfg.docsHidden) {
803
+ const hidden = el('span', 'icon-badge warn')
804
+ hidden.title = 'Hidden'
805
+ setHighlighted(hidden, 'H', engine)
806
+ iconRow.appendChild(hidden)
807
+ }
808
+ if (iconRow.childNodes.length > 0) {
809
+ overview.appendChild(iconRow)
810
+ }
811
+ content.appendChild(overview)
818
812
 
819
813
  const files = el('div', 'section')
820
- files.appendChild(el('h3', '', 'Body Files'))
821
814
  if (Array.isArray(cfg.bodyFiles) && cfg.bodyFiles.length > 0) {
815
+ files.appendChild(el('h3', '', 'Body Files'))
822
816
  const chips = el('div', 'chips')
823
817
  cfg.bodyFiles.forEach((file) => {
824
818
  const chip = el('span', 'chip ok')
@@ -826,12 +820,13 @@
826
820
  chips.appendChild(chip)
827
821
  })
828
822
  files.appendChild(chips)
829
- } else {
830
- files.appendChild(el('div', 'empty', 'No file upload fields.'))
823
+ content.appendChild(files)
831
824
  }
832
- content.appendChild(files)
833
825
 
834
- content.appendChild(renderSeparatedSchemas(flatSchema, engine))
826
+ const separatedSchemas = renderSeparatedSchemas(flatSchema, engine)
827
+ if (separatedSchemas) {
828
+ content.appendChild(separatedSchemas)
829
+ }
835
830
 
836
831
  details.appendChild(content)
837
832
  return details
@@ -842,9 +837,6 @@
842
837
  caseSensitive: caseSensitiveInput.checked,
843
838
  regex: regexSearchInput.checked,
844
839
  })
845
- const typeEngine = createTypeSearchEngine(typeFilterInput.value.trim())
846
- const hasEnumOnly = Boolean(hasEnumOnlyInput.checked)
847
- const typeMatchMode = typeMatchModeInput.value === 'exact' ? 'exact' : 'contains'
848
840
 
849
841
  if (engine.error) {
850
842
  statusEl.textContent = `Invalid regex: ${engine.error}`
@@ -858,11 +850,7 @@
858
850
  }
859
851
 
860
852
  const selectedIds = selectedFieldIds()
861
- const filtered = state.leaves.filter(
862
- (leaf) =>
863
- matchesLeaf(leaf, engine, selectedIds) &&
864
- matchesTypeFilter(leaf, typeEngine, typeMatchMode, hasEnumOnly),
865
- )
853
+ const filtered = state.leaves.filter((leaf) => matchesLeaf(leaf, engine, selectedIds))
866
854
 
867
855
  statusEl.textContent = `${filtered.length} / ${state.leaves.length} routes matched.`
868
856
  resultsEl.innerHTML = ''
@@ -906,10 +894,8 @@
906
894
 
907
895
  function resetFiltersToDefault() {
908
896
  searchInput.value = ''
909
- typeFilterInput.value = ''
910
897
  caseSensitiveInput.checked = false
911
898
  regexSearchInput.checked = false
912
- hasEnumOnlyInput.checked = false
913
899
  typeMatchModeInput.value = 'contains'
914
900
  setFieldSelection(new Set(SEARCH_FIELDS.map((field) => field.id)))
915
901
  }
@@ -919,14 +905,11 @@
919
905
  const chips = []
920
906
 
921
907
  const searchValue = searchInput.value.trim()
922
- const typeValue = typeFilterInput.value.trim()
923
908
 
924
909
  if (searchValue) chips.push(`search: ${searchValue}`)
925
- if (typeValue) chips.push(`type: ${typeValue}`)
926
910
  if (typeMatchModeInput.value === 'exact') chips.push('type mode: exact')
927
911
  if (caseSensitiveInput.checked) chips.push('case sensitive')
928
912
  if (regexSearchInput.checked) chips.push('regex')
929
- if (hasEnumOnlyInput.checked) chips.push('has enum only')
930
913
  if (hasRegexError) chips.push('regex error')
931
914
 
932
915
  const allIds = SEARCH_FIELDS.map((field) => field.id)
@@ -949,10 +932,8 @@
949
932
  function collectFilterState() {
950
933
  return {
951
934
  search: searchInput.value,
952
- typeFilter: typeFilterInput.value,
953
935
  caseSensitive: caseSensitiveInput.checked,
954
936
  regex: regexSearchInput.checked,
955
- hasEnumOnly: hasEnumOnlyInput.checked,
956
937
  typeMatchMode: typeMatchModeInput.value === 'exact' ? 'exact' : 'contains',
957
938
  selectedFields: selectedFieldIds(),
958
939
  }
@@ -980,10 +961,8 @@
980
961
  const parsed = JSON.parse(decodeURIComponent(raw))
981
962
  isHydratingFromUrl = true
982
963
  if (typeof parsed.search === 'string') searchInput.value = parsed.search
983
- if (typeof parsed.typeFilter === 'string') typeFilterInput.value = parsed.typeFilter
984
964
  caseSensitiveInput.checked = Boolean(parsed.caseSensitive)
985
965
  regexSearchInput.checked = Boolean(parsed.regex)
986
- hasEnumOnlyInput.checked = Boolean(parsed.hasEnumOnly)
987
966
  if (parsed.typeMatchMode === 'exact' || parsed.typeMatchMode === 'contains') {
988
967
  typeMatchModeInput.value = parsed.typeMatchMode
989
968
  }
@@ -1038,10 +1017,8 @@
1038
1017
  })
1039
1018
 
1040
1019
  searchInput.addEventListener('input', renderResults)
1041
- typeFilterInput.addEventListener('input', renderResults)
1042
1020
  caseSensitiveInput.addEventListener('change', renderResults)
1043
1021
  regexSearchInput.addEventListener('change', renderResults)
1044
- hasEnumOnlyInput.addEventListener('change', renderResults)
1045
1022
  typeMatchModeInput.addEventListener('change', renderResults)
1046
1023
 
1047
1024
  selectAllFieldsBtn.addEventListener('click', () =>