@emeryld/rrroutes-contract 2.2.2 → 2.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -489,7 +489,7 @@ var CSS_STYLES = `:root {
489
489
 
490
490
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
491
491
 
492
- /* Tag Pastel Palette (Generated via class logic) */
492
+ /* Tag Pastel Palette */
493
493
  --tag-0-bg: rgba(254, 202, 202, 0.2); --tag-0-fg: #fca5a5; /* Red */
494
494
  --tag-1-bg: rgba(253, 230, 138, 0.2); --tag-1-fg: #fcd34d; /* Amber */
495
495
  --tag-2-bg: rgba(187, 247, 208, 0.2); --tag-2-fg: #86efac; /* Green */
@@ -562,6 +562,21 @@ body {
562
562
  box-shadow: 0 10px 30px -10px rgba(0,0,0,0.5);
563
563
  }
564
564
 
565
+ .schema-indent {
566
+ display: inline-block;
567
+ }
568
+
569
+ .schema-branch {
570
+ opacity: 0.6;
571
+ font-family: var(--font-mono);
572
+ margin-right: 2px;
573
+ }
574
+
575
+ .schema-meta code {
576
+ font-family: var(--font-mono);
577
+ font-size: 11px;
578
+ }
579
+
565
580
  /* Top Row: Filters */
566
581
  .filters-row {
567
582
  display: flex;
@@ -573,28 +588,27 @@ body {
573
588
  /* New Wrapper for Search and Methods */
574
589
  .left-column {
575
590
  display: flex;
576
- flex-direction: column; /* Stack Search and Methods vertically */
577
- gap: 16px; /* Gap between search and methods */
578
- flex-basis: 300px; /* Fixed width for the left column */
591
+ flex-direction: column;
592
+ gap: 16px;
593
+ flex-basis: 300px;
579
594
  min-width: 250px;
580
595
  }
581
596
 
582
597
  .search-box {
583
- /* No change to internal search box styling, but remove flex properties */
584
- width: 100%; /* Make search fill the left-column width */
598
+ width: 100%;
585
599
  position: relative;
586
600
  }
587
601
 
588
602
  /* Tag Filter Group */
589
603
  .filter-group.tag-filters-container {
590
- flex: 1; /* Tags take up all remaining space on the right */
604
+ flex: 1;
591
605
  min-width: 200px;
592
606
  }
593
607
 
594
608
  /* Method Filter Group */
595
609
  .filter-group.method-filters-container {
596
- /* Stays inside the left-column, no need for 100% basis */
597
610
  }
611
+
598
612
  .search-input {
599
613
  width: 100%;
600
614
  background: rgba(2, 6, 23, 0.6);
@@ -675,14 +689,21 @@ body {
675
689
  border-color: var(--accent-primary);
676
690
  }
677
691
 
678
- /* Specialized Colored Tag Pills for Filters */
679
- .pill-checkbox.colored-tag input:checked + span {
680
- background: var(--tag-bg);
681
- color: var(--tag-fg);
682
- border-color: var(--tag-fg);
692
+ /* Tag filter pills */
693
+ .tag-filter-pill {
694
+ display: inline-block;
695
+ padding: 3px 8px;
696
+ border-radius: 4px;
697
+ font-size: 11px;
698
+ font-weight: 600;
699
+ background: rgba(30, 41, 59, 0.5);
700
+ color: var(--text-muted);
701
+ border: 1px solid transparent;
702
+ transition: all 0.15s;
683
703
  }
684
- .pill-checkbox.colored-tag span {
685
- /* Subtle hint of color even when unchecked? Optional. kept neutral for now */
704
+ .pill-checkbox.colored-tag input:checked + .tag-filter-pill {
705
+ border-color: var(--accent-primary);
706
+ box-shadow: 0 0 0 1px var(--accent-glow);
686
707
  }
687
708
 
688
709
  /* Group Jump Links */
@@ -762,8 +783,8 @@ body {
762
783
  padding: 12px 16px;
763
784
  display: flex;
764
785
  align-items: center;
765
- gap: 12px; /* Space between method, path, tags */
766
- flex-wrap: wrap; /* Allow wrapping on very small screens if needed, but row pref */
786
+ gap: 12px;
787
+ flex-wrap: wrap;
767
788
  }
768
789
 
769
790
  /* Item 1: Method */
@@ -784,7 +805,7 @@ body {
784
805
  .m-PATCH { background: var(--method-patch-bg); color: var(--method-patch); border: 1px solid rgba(45, 212, 191, 0.3); }
785
806
  .m-DELETE { background: var(--method-delete-bg); color: var(--method-delete); border: 1px solid rgba(248, 113, 113, 0.3); }
786
807
 
787
- /* Item 2: Path (Clickable) */
808
+ /* Item 2: Path */
788
809
  .path-container {
789
810
  font-family: var(--font-mono);
790
811
  font-size: 13px;
@@ -811,7 +832,7 @@ body {
811
832
  }
812
833
  .path-container:hover .copy-icon { opacity: 1; }
813
834
 
814
- /* Item 3: Tags (Aligned right of path) */
835
+ /* Item 3: Tags */
815
836
  .tags-container {
816
837
  display: flex;
817
838
  align-items: center;
@@ -881,6 +902,11 @@ body {
881
902
  }
882
903
  .summary-text { font-size: 14px; color: var(--text-main); line-height: 1.5; }
883
904
 
905
+ .description-text {
906
+ font-size: 12px;
907
+ opacity: 0.7;
908
+ }
909
+
884
910
  /* Tables */
885
911
  .schema-table { width: 100%; border-collapse: collapse; font-size: 12px; }
886
912
  .schema-table th {
@@ -896,6 +922,13 @@ body {
896
922
  .req-true { color: #4ade80; background: rgba(74, 222, 128, 0.1); }
897
923
  .req-false { color: #94a3b8; background: rgba(148, 163, 184, 0.1); }
898
924
 
925
+ .schema-subtitle {
926
+ font-size: 11px;
927
+ font-weight: 600;
928
+ margin-bottom: 4px;
929
+ color: #64748b;
930
+ }
931
+
899
932
  .empty-message { text-align: center; padding: 40px; color: var(--text-muted); }
900
933
  `;
901
934
  var DOCS_JS = `
@@ -906,7 +939,7 @@ var DOCS_JS = `
906
939
  } catch(e) { console.error('Failed to parse docs', e); }
907
940
 
908
941
  // State
909
- let filters = {
942
+ const filters = {
910
943
  search: '',
911
944
  methods: new Set(),
912
945
  tags: new Set()
@@ -925,14 +958,13 @@ var DOCS_JS = `
925
958
  // Initialization
926
959
  function init() {
927
960
  populateFilters();
928
- render();
929
961
  setupGlobalListeners();
962
+ render();
930
963
  }
931
964
 
932
965
  // Tag Coloring Logic
933
966
  function getTagColorClass(tagName) {
934
967
  if (tagName === 'not-implemented') return 'not-implemented';
935
- // Simple string hash to select color index
936
968
  let hash = 0;
937
969
  for (let i = 0; i < tagName.length; i++) {
938
970
  hash = tagName.charCodeAt(i) + ((hash << 5) - hash);
@@ -951,55 +983,87 @@ var DOCS_JS = `
951
983
  }
952
984
 
953
985
  function populateFilters() {
986
+ if (!elMethodFilters || !elTagFilters) return;
987
+
954
988
  // Extract unique values
955
989
  const allMethods = new Set(leaves.map(l => (l.method || 'GET').toUpperCase()));
956
990
  const allTags = new Set(leaves.flatMap(l => (l.cfg && l.cfg.tags) ? l.cfg.tags : []));
957
991
 
958
992
  // Setup Method Checkboxes
959
993
  const sortedMethods = Array.from(allMethods).sort();
960
- elMethodFilters.innerHTML = sortedMethods.map(m => \`
961
- <label class="pill-checkbox">
962
- <input type="checkbox" value="\${m}" checked onchange="updateFilters()">
963
- <span>\${m}</span>
964
- </label>
965
- \`).join('');
994
+ elMethodFilters.innerHTML = sortedMethods.map(m => (
995
+ '<label class="pill-checkbox">' +
996
+ '<input type="checkbox" value="' + escapeAttr(m) + '" checked>' +
997
+ '<span>' + escapeHtml(m) + '</span>' +
998
+ '</label>'
999
+ )).join('');
966
1000
  sortedMethods.forEach(m => filters.methods.add(m));
967
1001
 
968
- // Setup Tag Checkboxes (Left Aligned, Colored)
1002
+ // Attach listeners to method checkboxes
1003
+ elMethodFilters.querySelectorAll('input[type="checkbox"]').forEach(function(cb) {
1004
+ cb.addEventListener('change', updateFilters);
1005
+ });
1006
+
1007
+ // Setup Tag Checkboxes
969
1008
  if (allTags.size > 0) {
970
1009
  elTagFilters.innerHTML = Array.from(allTags).sort().map(t => {
971
1010
  const colorClass = getTagColorClass(t);
972
- const styleVars = 'style="--tag-bg:var(--' + colorClass + '-bg); --tag-fg:var(--' + colorClass + '-fg);"';
973
- return \`
974
- <label class="pill-checkbox colored-tag" \${styleVars}>
975
- <input type="checkbox" value="\${t}" onchange="updateFilters()">
976
- <span>\${t}</span>
977
- </label>
978
- \`;
1011
+ return (
1012
+ '<label class="pill-checkbox colored-tag">' +
1013
+ '<input type="checkbox" value="' + escapeAttr(t) + '">' +
1014
+ '<span class="tag-filter-pill ' + colorClass + '">' + escapeHtml(t) + '</span>' +
1015
+ '</label>'
1016
+ );
979
1017
  }).join('');
1018
+
1019
+ // Attach listeners to tag checkboxes
1020
+ elTagFilters.querySelectorAll('input[type="checkbox"]').forEach(function(cb) {
1021
+ cb.addEventListener('change', updateFilters);
1022
+ });
980
1023
  }
981
1024
  }
982
1025
 
983
- window.updateFilters = function() {
984
- filters.search = elSearch.value.toLowerCase();
1026
+ function updateFilters() {
1027
+ if (elSearch) {
1028
+ filters.search = elSearch.value.toLowerCase();
1029
+ }
1030
+
985
1031
  filters.methods.clear();
986
- elMethodFilters.querySelectorAll('input:checked').forEach(cb => filters.methods.add(cb.value));
1032
+ if (elMethodFilters) {
1033
+ elMethodFilters.querySelectorAll('input[type="checkbox"]:checked').forEach(function(cb) {
1034
+ filters.methods.add(cb.value);
1035
+ });
1036
+ }
1037
+
987
1038
  filters.tags.clear();
988
- elTagFilters.querySelectorAll('input:checked').forEach(cb => filters.tags.add(cb.value));
1039
+ if (elTagFilters) {
1040
+ elTagFilters.querySelectorAll('input[type="checkbox"]:checked').forEach(function(cb) {
1041
+ filters.tags.add(cb.value);
1042
+ });
1043
+ }
1044
+
989
1045
  render();
990
1046
  }
991
1047
 
1048
+ function setupGlobalListeners() {
1049
+ if (elSearch) {
1050
+ elSearch.addEventListener('input', updateFilters);
1051
+ }
1052
+ }
1053
+
992
1054
  // Core Render Logic
993
1055
  function render() {
1056
+ if (!elRouteList || !elOverview) return;
1057
+
994
1058
  // 1. Filter Data
995
- const filtered = leaves.filter(leaf => {
1059
+ const filtered = leaves.filter(function(leaf) {
996
1060
  const m = (leaf.method || 'GET').toUpperCase();
997
1061
  const t = (leaf.cfg && leaf.cfg.tags) ? leaf.cfg.tags : [];
998
1062
  const path = (leaf.path || '').toLowerCase();
999
1063
  const summary = (leaf.cfg && leaf.cfg.summary || '').toLowerCase();
1000
1064
 
1001
1065
  if (!filters.methods.has(m)) return false;
1002
- if (filters.tags.size > 0 && !t.some(tag => filters.tags.has(tag))) return false;
1066
+ if (filters.tags.size > 0 && !t.some(function(tag) { return filters.tags.has(tag); })) return false;
1003
1067
  if (filters.search && !path.includes(filters.search) && !summary.includes(filters.search)) return false;
1004
1068
 
1005
1069
  return true;
@@ -1013,7 +1077,7 @@ var DOCS_JS = `
1013
1077
 
1014
1078
  // 2. Group Data
1015
1079
  const groups = {};
1016
- filtered.forEach(leaf => {
1080
+ filtered.forEach(function(leaf) {
1017
1081
  const gName = (leaf.cfg && leaf.cfg.docsGroup) ? leaf.cfg.docsGroup : 'UNGROUPED';
1018
1082
  if (!groups[gName]) groups[gName] = [];
1019
1083
  groups[gName].push(leaf);
@@ -1021,19 +1085,23 @@ var DOCS_JS = `
1021
1085
 
1022
1086
  const sortedGroupNames = Object.keys(groups).sort(sortGroups);
1023
1087
 
1024
- // 3. Render Overview Chips (Inside Sticky Header)
1025
- // Note: We include a small label "GROUPS:" or similar if needed, or just chips.
1026
- elOverview.innerHTML = '<span class="overview-label">JUMP TO:</span>' + sortedGroupNames.map(gName => \`
1027
- <a href="#group-\${gName}" class="group-chip" onclick="scrollToGroup(event, '\${gName}')">
1028
- \${gName}
1029
- </a>
1030
- \`).join('');
1088
+ // 3. Render Overview Chips
1089
+ elOverview.innerHTML = (
1090
+ '<span class="overview-label">JUMP TO:</span>' +
1091
+ sortedGroupNames.map(function(gName) {
1092
+ return (
1093
+ '<a href="#group-' + escapeAttr(gName) + '" ' +
1094
+ 'class="group-chip" data-group="' + escapeAttr(gName) + '">' +
1095
+ escapeHtml(gName) +
1096
+ '</a>'
1097
+ );
1098
+ }).join('')
1099
+ );
1031
1100
 
1032
1101
  // 4. Render Groups & Cards
1033
- const html = sortedGroupNames.map(gName => {
1102
+ const html = sortedGroupNames.map(function(gName) {
1034
1103
  const routes = groups[gName];
1035
- // Sort Routes: Path ASC, then Method
1036
- routes.sort((a, b) => {
1104
+ routes.sort(function(a, b) {
1037
1105
  const pA = a.path || '';
1038
1106
  const pB = b.path || '';
1039
1107
  if (pA < pB) return -1;
@@ -1041,23 +1109,27 @@ var DOCS_JS = `
1041
1109
  return (a.method || '').localeCompare(b.method || '');
1042
1110
  });
1043
1111
 
1044
- return \`
1045
- <div class="api-group" id="group-\${gName}">
1046
- <div class="group-header">
1047
- <h2 class="group-title">\${gName}</h2>
1048
- <div class="group-actions">
1049
- <button onclick="toggleGroup('\${gName}', true)">Expand All</button>
1050
- <button onclick="toggleGroup('\${gName}', false)">Collapse All</button>
1051
- </div>
1052
- </div>
1053
- <div class="cards-list">
1054
- \${routes.map(renderCard).join('')}
1055
- </div>
1056
- </div>
1057
- \`;
1112
+ return (
1113
+ '<div class="api-group" id="group-' + escapeAttr(gName) + '">' +
1114
+ '<div class="group-header">' +
1115
+ '<h2 class="group-title">' + escapeHtml(gName) + '</h2>' +
1116
+ '<div class="group-actions">' +
1117
+ '<button type="button" data-group="' + escapeAttr(gName) + '" data-action="expand">Expand All</button>' +
1118
+ '<button type="button" data-group="' + escapeAttr(gName) + '" data-action="collapse">Collapse All</button>' +
1119
+ '</div>' +
1120
+ '</div>' +
1121
+ '<div class="cards-list">' +
1122
+ routes.map(renderCard).join('') +
1123
+ '</div>' +
1124
+ '</div>'
1125
+ );
1058
1126
  }).join('');
1059
1127
 
1060
1128
  elRouteList.innerHTML = html;
1129
+
1130
+ // Wire up interactions that depend on rendered DOM
1131
+ wireOverviewInteractions();
1132
+ wireCardInteractions();
1061
1133
  }
1062
1134
 
1063
1135
  // Card Renderer
@@ -1069,35 +1141,33 @@ var DOCS_JS = `
1069
1141
  const description = cfg.description || '';
1070
1142
  const tags = cfg.tags || [];
1071
1143
 
1072
- // Header Construction: Method | Path | Tags | Spacer | Chevron
1073
- const tagBadges = tags.map(t => {
1144
+ const tagBadges = tags.map(function(t) {
1074
1145
  const cClass = getTagColorClass(t);
1075
- return \`<span class="status-badge \${cClass}">\${t}</span>\`;
1146
+ return '<span class="status-badge ' + cClass + '">' + escapeHtml(t) + '</span>';
1076
1147
  }).join('');
1077
1148
 
1078
- const headerHtml = \`
1079
- <div class="card-header">
1080
- <span class="method-badge m-\${method}">\${method}</span>
1081
-
1082
- <div class="path-container" onclick="copyText(event, '\${path}')" title="Click to copy path">
1083
- <span class="path-text">\${path}</span>
1084
- <span class="copy-icon">\u{1F4CB}</span>
1085
- </div>
1086
-
1087
- <div class="tags-container">
1088
- \${tagBadges}
1089
- </div>
1090
-
1091
- <div class="header-spacer"></div>
1092
- <div class="expand-icon">\u25BC</div>
1093
- </div>
1094
- \`;
1095
-
1096
- // Body Construction (Preserved structure)
1097
- let contentHtml = \`<div class="section-block"><div class="summary-text">
1098
- <strong>\${escape(summary)}</strong>
1099
- \${description ? '<br><span style="font-size:12px; opacity:0.7">' + escape(description) + '</span>' : ''}
1100
- </div></div>\`;
1149
+ const headerHtml =
1150
+ '<div class="card-header">' +
1151
+ '<span class="method-badge m-' + escapeAttr(method) + '">' + escapeHtml(method) + '</span>' +
1152
+ '<div class="path-container" data-path="' + escapeAttr(path) + '" title="Click to copy path">' +
1153
+ '<span class="path-text">' + escapeHtml(path) + '</span>' +
1154
+ '<span class="copy-icon">\u{1F4CB}</span>' +
1155
+ '</div>' +
1156
+ '<div class="tags-container">' +
1157
+ tagBadges +
1158
+ '</div>' +
1159
+ '<div class="header-spacer"></div>' +
1160
+ '<div class="expand-icon">\u25BC</div>' +
1161
+ '</div>';
1162
+
1163
+ let contentHtml =
1164
+ '<div class="section-block"><div class="summary-text">' +
1165
+ '<strong>' + escapeHtml(summary) + '</strong>' +
1166
+ (description
1167
+ ? '<br><span class="description-text">' + escapeHtml(description) + '</span>'
1168
+ : ''
1169
+ ) +
1170
+ '</div></div>';
1101
1171
 
1102
1172
  if (cfg.paramsSchema || cfg.querySchema) {
1103
1173
  contentHtml += '<div class="section-block"><div class="section-title">Parameters</div>';
@@ -1107,96 +1177,331 @@ var DOCS_JS = `
1107
1177
  }
1108
1178
 
1109
1179
  if (cfg.bodySchema) {
1110
- contentHtml += \`
1111
- <div class="section-block">
1112
- <div class="section-title">Request Body \${cfg.hasBody ? '' : '(Optional)'}</div>
1113
- \${renderSchemaTable(cfg.bodySchema)}
1114
- </div>
1115
- \`;
1180
+ contentHtml +=
1181
+ '<div class="section-block">' +
1182
+ '<div class="section-title">Request Body ' + (cfg.hasBody ? '' : '(Optional)') + '</div>' +
1183
+ renderSchemaTable(cfg.bodySchema) +
1184
+ '</div>';
1116
1185
  }
1117
1186
 
1118
1187
  if (cfg.outputSchema) {
1119
- contentHtml += \`
1120
- <div class="section-block">
1121
- <div class="section-title">Response Schema</div>
1122
- \${renderSchemaTable(cfg.outputSchema)}
1123
- </div>
1124
- \`;
1188
+ contentHtml +=
1189
+ '<div class="section-block">' +
1190
+ '<div class="section-title">Response Schema</div>' +
1191
+ renderSchemaTable(cfg.outputSchema) +
1192
+ '</div>';
1125
1193
  }
1126
1194
 
1127
- return \`
1128
- <article class="endpoint-card" data-expanded="false" onclick="toggleCard(this, event)">
1129
- \${headerHtml}
1130
- <div class="card-body" onclick="event.stopPropagation()">
1131
- \${contentHtml}
1132
- </div>
1133
- </article>
1134
- \`;
1195
+ return (
1196
+ '<article class="endpoint-card" data-expanded="false">' +
1197
+ headerHtml +
1198
+ '<div class="card-body">' +
1199
+ contentHtml +
1200
+ '</div>' +
1201
+ '</article>'
1202
+ );
1135
1203
  }
1136
1204
 
1137
- function renderSchemaTable(node, title) {
1138
- let rows = '';
1139
- if (node.kind === 'object' && node.properties) {
1140
- Object.keys(node.properties).forEach(key => {
1141
- const prop = node.properties[key];
1142
- const reqClass = prop.optional ? 'req-false' : 'req-true';
1143
- const reqText = prop.optional ? 'OPT' : 'REQ';
1144
- rows += \`
1145
- <tr>
1146
- <td class="col-name">\${key}</td>
1147
- <td class="col-type">\${getTypeLabel(prop)}</td>
1148
- <td><span class="req-badge \${reqClass}">\${reqText}</span></td>
1149
- <td>\${escape(prop.description || '')}</td>
1150
- </tr>
1151
- \`;
1152
- });
1153
- } else {
1154
- rows = \`<tr><td colspan="4">Type: \${getTypeLabel(node)}</td></tr>\`;
1205
+
1206
+ function renderSchemaTable(node, title) {
1207
+ let rows = '';
1208
+
1209
+ if (node.kind === 'object' && node.properties) {
1210
+ rows = renderObjectChildren(node, 0);
1211
+ } else {
1212
+ rows = renderNodeRow('(root)', node, 0);
1213
+ }
1214
+
1215
+ return (
1216
+ (title
1217
+ ? '<div class="schema-subtitle">' + escapeHtml(title) + '</div>'
1218
+ : '') +
1219
+ '<table class="schema-table">' +
1220
+ rows +
1221
+ '</table>'
1222
+ );
1223
+ }
1224
+
1225
+ /**
1226
+ * Render all children of an object node (at a given depth).
1227
+ */
1228
+ function renderObjectChildren(node, depth) {
1229
+ if (!node.properties) return '';
1230
+
1231
+ let rows = '';
1232
+ Object.keys(node.properties).forEach(function (key) {
1233
+ rows += renderNodeRow(key, node.properties[key], depth);
1234
+ });
1235
+ return rows;
1236
+ }
1237
+
1238
+ /**
1239
+ * Render a single row for a property / node, then recursively render nested structure.
1240
+ */
1241
+ function renderNodeRow(name, node, depth) {
1242
+ const reqClass = node.optional ? 'req-false' : 'req-true';
1243
+ const reqText = node.optional ? 'OPT' : 'REQ';
1244
+
1245
+ const indentPx = depth * 16;
1246
+ const nameCell =
1247
+ '<span class="schema-indent" style="padding-left:' +
1248
+ indentPx +
1249
+ 'px;"></span>' +
1250
+ (depth > 0 ? '<span class="schema-branch">\u2514\u2500 </span>' : '') +
1251
+ escapeHtml(name);
1252
+
1253
+ const typeLabel = getTypeLabel(node);
1254
+ const descriptionHtml = renderNodeDescription(node);
1255
+
1256
+ let rows =
1257
+ '<tr>' +
1258
+ '<td class="col-name">' + nameCell + '</td>' +
1259
+ '<td class="col-type">' + escapeHtml(typeLabel) + '</td>' +
1260
+ '<td><span class="req-badge ' + reqClass + '">' + reqText + '</span></td>' +
1261
+ '<td>' + descriptionHtml + '</td>' +
1262
+ '</tr>';
1263
+
1264
+ // 1) Object properties
1265
+ if (node.kind === 'object' && node.properties) {
1266
+ rows += renderObjectChildren(node, depth + 1);
1267
+ }
1268
+
1269
+ // 2) Array element type
1270
+ if (node.kind === 'array' && node.element) {
1271
+ const element = node.element;
1272
+ if (isComplexNode(element)) {
1273
+ rows += renderNodeRow('[items]', element, depth + 1);
1155
1274
  }
1156
- return \`
1157
- \${title ? '<div style="font-size:11px; font-weight:600; margin-bottom:4px; color:#64748b">' + title + '</div>' : ''}
1158
- <table class="schema-table">\${rows}</table>
1159
- \`;
1160
1275
  }
1161
1276
 
1277
+ // 3) Union variants
1278
+ if (node.kind === 'union' && Array.isArray(node.union)) {
1279
+ node.union.forEach(function (variant, index) {
1280
+ rows += renderNodeRow('option ' + (index + 1), variant, depth + 1);
1281
+ });
1282
+ }
1283
+
1284
+ return rows;
1285
+ }
1286
+
1287
+ /**
1288
+ * Decide if a node is \u201Ccomplex\u201D enough to warrant a nested expansion row.
1289
+ */
1290
+ function isComplexNode(node) {
1291
+ return (
1292
+ node.kind === 'object' ||
1293
+ node.kind === 'array' ||
1294
+ node.kind === 'union' ||
1295
+ node.kind === 'record' ||
1296
+ node.kind === 'tuple'
1297
+ );
1298
+ }
1299
+
1300
+ /**
1301
+ * Helper: simple vs complex node, for compact type labels.
1302
+ */
1303
+ function isSimpleNode(node) {
1304
+ return !isComplexNode(node);
1305
+ }
1306
+
1307
+ /**
1308
+ * Type label shown in the "Type" column.
1309
+ */
1310
+ function getTypeLabel(node) {
1311
+ let base;
1312
+
1313
+ switch (node.kind) {
1314
+ case 'object':
1315
+ base = 'object';
1316
+ break;
1317
+
1318
+ case 'array':
1319
+ if (!node.element) {
1320
+ base = 'array';
1321
+ } else if (isSimpleNode(node.element)) {
1322
+ base = 'array<' + getTypeLabel(node.element) + '>';
1323
+ } else {
1324
+ base = 'array<object>';
1325
+ }
1326
+ break;
1327
+
1328
+ case 'union':
1329
+ if (node.union && node.union.length) {
1330
+ const parts = node.union.map(function (u) {
1331
+ return isSimpleNode(u) ? getTypeLabel(u) : 'object';
1332
+ });
1333
+ base = parts.join(' | ');
1334
+ } else {
1335
+ base = 'union';
1336
+ }
1337
+ break;
1338
+
1339
+ case 'literal':
1340
+ base = 'literal';
1341
+ break;
1342
+
1343
+ case 'enum':
1344
+ base = 'enum';
1345
+ break;
1346
+
1347
+ case 'record':
1348
+ base = 'record';
1349
+ break;
1350
+
1351
+ case 'tuple':
1352
+ base = 'tuple';
1353
+ break;
1354
+
1355
+ default:
1356
+ base = node.kind || 'unknown';
1357
+ break;
1358
+ }
1359
+
1360
+ if (node.nullable) {
1361
+ base = base + ' | null';
1362
+ }
1363
+ return base;
1364
+ }
1365
+
1366
+ /**
1367
+ * Description cell content:
1368
+ * - main description text
1369
+ * - for enum: allowed values
1370
+ * - for literal: literal value
1371
+ */
1372
+ function renderNodeDescription(node) {
1373
+ const parts = [];
1374
+
1375
+ if (node.description) {
1376
+ parts.push(escapeHtml(node.description));
1377
+ }
1378
+
1379
+ if (node.kind === 'enum' && node.enumValues && node.enumValues.length) {
1380
+ const values = node.enumValues
1381
+ .map(function (v) {
1382
+ return '<code>' + escapeHtml(String(v)) + '</code>';
1383
+ })
1384
+ .join(' | ');
1385
+ parts.push('<div class="schema-meta">Allowed: ' + values + '</div>');
1386
+ }
1387
+
1388
+ if (node.kind === 'literal' && typeof node.literal !== 'undefined') {
1389
+ const valueStr = escapeHtml(JSON.stringify(node.literal));
1390
+ parts.push(
1391
+ '<div class="schema-meta">Literal: <code>' + valueStr + '</code></div>'
1392
+ );
1393
+ }
1394
+
1395
+ return parts.join('<br>');
1396
+ }
1397
+
1398
+ /**
1399
+ * Escape HTML helper.
1400
+ */
1401
+ function escapeHtml(str) {
1402
+ return String(str)
1403
+ .replace(/&/g, '&amp;')
1404
+ .replace(/</g, '&lt;')
1405
+ .replace(/>/g, '&gt;')
1406
+ .replace(/"/g, '&quot;')
1407
+ .replace(/'/g, '&#39;');
1408
+ }
1409
+
1162
1410
  function getTypeLabel(node) {
1163
1411
  if (!node) return 'any';
1164
- if (node.kind === 'array') return \`\${getTypeLabel(node.element)}[]\`;
1165
- if (node.enumValues) return \`enum(\${node.enumValues.join('|')})\`;
1412
+ if (node.kind === 'array') return getTypeLabel(node.element) + '[]';
1413
+ if (node.enumValues) return 'enum(' + node.enumValues.join('|') + ')';
1166
1414
  return node.kind || 'any';
1167
1415
  }
1168
1416
 
1169
- function escape(str) {
1170
- if (!str) return '';
1171
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1417
+ // Interactions
1418
+
1419
+ function wireOverviewInteractions() {
1420
+ if (!elOverview || !elRouteList) return;
1421
+
1422
+ // group chips
1423
+ elOverview.querySelectorAll('.group-chip').forEach(function(chip) {
1424
+ chip.addEventListener('click', function(e) {
1425
+ e.preventDefault();
1426
+ const gName = chip.getAttribute('data-group');
1427
+ if (gName) scrollToGroup(gName);
1428
+ });
1429
+ });
1430
+
1431
+ // expand/collapse buttons
1432
+ elRouteList.querySelectorAll('.group-actions button').forEach(function(btn) {
1433
+ btn.addEventListener('click', function() {
1434
+ const gName = btn.getAttribute('data-group');
1435
+ const action = btn.getAttribute('data-action');
1436
+ if (!gName || !action) return;
1437
+ toggleGroup(gName, action === 'expand');
1438
+ });
1439
+ });
1172
1440
  }
1173
1441
 
1174
- window.toggleCard = function(card, e) {
1175
- if (e.target.closest('.path-container')) return; // Don't toggle on path click
1442
+ function wireCardInteractions() {
1443
+ if (!elRouteList) return;
1444
+
1445
+ // card expand/collapse
1446
+ elRouteList.querySelectorAll('.endpoint-card').forEach(function(card) {
1447
+ card.addEventListener('click', function(e) {
1448
+ var target = e.target;
1449
+ if (target && target.closest && target.closest('.path-container')) {
1450
+ // handled separately for copy
1451
+ return;
1452
+ }
1453
+ toggleCard(card);
1454
+ });
1455
+ });
1456
+
1457
+ // copy path
1458
+ elRouteList.querySelectorAll('.path-container').forEach(function(pc) {
1459
+ pc.addEventListener('click', function(e) {
1460
+ e.stopPropagation();
1461
+ const text = pc.getAttribute('data-path') || '';
1462
+ copyText(text);
1463
+ });
1464
+ });
1465
+ }
1466
+
1467
+ function toggleCard(card) {
1176
1468
  const isExpanded = card.getAttribute('data-expanded') === 'true';
1177
- card.setAttribute('data-expanded', !isExpanded);
1178
- };
1469
+ card.setAttribute('data-expanded', String(!isExpanded));
1470
+ }
1179
1471
 
1180
- window.toggleGroup = function(gName, expand) {
1472
+ function toggleGroup(gName, expand) {
1181
1473
  const group = document.getElementById('group-' + gName);
1182
1474
  if (!group) return;
1183
- group.querySelectorAll('.endpoint-card').forEach(c => c.setAttribute('data-expanded', expand));
1184
- };
1475
+ group.querySelectorAll('.endpoint-card').forEach(function(c) {
1476
+ c.setAttribute('data-expanded', String(!!expand));
1477
+ });
1478
+ }
1185
1479
 
1186
- window.copyText = function(e, text) {
1187
- e.stopPropagation();
1188
- navigator.clipboard.writeText(text);
1189
- // Could add visual feedback here
1190
- };
1480
+ function copyText(text) {
1481
+ if (!navigator.clipboard || !text) return;
1482
+ navigator.clipboard.writeText(text).catch(function() {
1483
+ // ignore
1484
+ });
1485
+ }
1191
1486
 
1192
- window.scrollToGroup = function(e, gName) {
1193
- e.preventDefault();
1487
+ function scrollToGroup(gName) {
1194
1488
  const el = document.getElementById('group-' + gName);
1195
- if(el) window.scrollTo({ top: el.offsetTop - 220, behavior: 'smooth' });
1489
+ if (el) window.scrollTo({ top: el.offsetTop - 220, behavior: 'smooth' });
1196
1490
  }
1197
1491
 
1198
- function setupGlobalListeners() {
1199
- elSearch.addEventListener('input', updateFilters);
1492
+ // Escaping helpers
1493
+ function escapeHtml(str) {
1494
+ if (!str && str !== 0) return '';
1495
+ return String(str)
1496
+ .replace(/&/g, '&amp;')
1497
+ .replace(/</g, '&lt;')
1498
+ .replace(/>/g, '&gt;')
1499
+ .replace(/"/g, '&quot;')
1500
+ .replace(/'/g, '&#39;');
1501
+ }
1502
+
1503
+ function escapeAttr(str) {
1504
+ return escapeHtml(str);
1200
1505
  }
1201
1506
 
1202
1507
  init();
@@ -1226,12 +1531,9 @@ function renderLeafDocsHTML(leaves, options = {}) {
1226
1531
 
1227
1532
  <div class="controls-container">
1228
1533
  <div class="filters-row">
1229
-
1230
1534
  <div class="left-column">
1231
-
1232
1535
  <div class="filter-group method-filters-container">
1233
1536
  <div class="filter-label">Search</div>
1234
-
1235
1537
  <div class="search-box">
1236
1538
  <span class="search-icon">\u{1F50D}</span>
1237
1539
  <input type="text" id="searchInput" class="search-input" placeholder="Filter endpoints...">