@emeryld/rrroutes-contract 2.2.2 → 2.2.3

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 */
@@ -573,28 +573,27 @@ body {
573
573
  /* New Wrapper for Search and Methods */
574
574
  .left-column {
575
575
  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 */
576
+ flex-direction: column;
577
+ gap: 16px;
578
+ flex-basis: 300px;
579
579
  min-width: 250px;
580
580
  }
581
581
 
582
582
  .search-box {
583
- /* No change to internal search box styling, but remove flex properties */
584
- width: 100%; /* Make search fill the left-column width */
583
+ width: 100%;
585
584
  position: relative;
586
585
  }
587
586
 
588
587
  /* Tag Filter Group */
589
588
  .filter-group.tag-filters-container {
590
- flex: 1; /* Tags take up all remaining space on the right */
589
+ flex: 1;
591
590
  min-width: 200px;
592
591
  }
593
592
 
594
593
  /* Method Filter Group */
595
594
  .filter-group.method-filters-container {
596
- /* Stays inside the left-column, no need for 100% basis */
597
595
  }
596
+
598
597
  .search-input {
599
598
  width: 100%;
600
599
  background: rgba(2, 6, 23, 0.6);
@@ -675,14 +674,21 @@ body {
675
674
  border-color: var(--accent-primary);
676
675
  }
677
676
 
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);
677
+ /* Tag filter pills */
678
+ .tag-filter-pill {
679
+ display: inline-block;
680
+ padding: 3px 8px;
681
+ border-radius: 4px;
682
+ font-size: 11px;
683
+ font-weight: 600;
684
+ background: rgba(30, 41, 59, 0.5);
685
+ color: var(--text-muted);
686
+ border: 1px solid transparent;
687
+ transition: all 0.15s;
683
688
  }
684
- .pill-checkbox.colored-tag span {
685
- /* Subtle hint of color even when unchecked? Optional. kept neutral for now */
689
+ .pill-checkbox.colored-tag input:checked + .tag-filter-pill {
690
+ border-color: var(--accent-primary);
691
+ box-shadow: 0 0 0 1px var(--accent-glow);
686
692
  }
687
693
 
688
694
  /* Group Jump Links */
@@ -762,8 +768,8 @@ body {
762
768
  padding: 12px 16px;
763
769
  display: flex;
764
770
  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 */
771
+ gap: 12px;
772
+ flex-wrap: wrap;
767
773
  }
768
774
 
769
775
  /* Item 1: Method */
@@ -784,7 +790,7 @@ body {
784
790
  .m-PATCH { background: var(--method-patch-bg); color: var(--method-patch); border: 1px solid rgba(45, 212, 191, 0.3); }
785
791
  .m-DELETE { background: var(--method-delete-bg); color: var(--method-delete); border: 1px solid rgba(248, 113, 113, 0.3); }
786
792
 
787
- /* Item 2: Path (Clickable) */
793
+ /* Item 2: Path */
788
794
  .path-container {
789
795
  font-family: var(--font-mono);
790
796
  font-size: 13px;
@@ -811,7 +817,7 @@ body {
811
817
  }
812
818
  .path-container:hover .copy-icon { opacity: 1; }
813
819
 
814
- /* Item 3: Tags (Aligned right of path) */
820
+ /* Item 3: Tags */
815
821
  .tags-container {
816
822
  display: flex;
817
823
  align-items: center;
@@ -881,6 +887,11 @@ body {
881
887
  }
882
888
  .summary-text { font-size: 14px; color: var(--text-main); line-height: 1.5; }
883
889
 
890
+ .description-text {
891
+ font-size: 12px;
892
+ opacity: 0.7;
893
+ }
894
+
884
895
  /* Tables */
885
896
  .schema-table { width: 100%; border-collapse: collapse; font-size: 12px; }
886
897
  .schema-table th {
@@ -896,6 +907,13 @@ body {
896
907
  .req-true { color: #4ade80; background: rgba(74, 222, 128, 0.1); }
897
908
  .req-false { color: #94a3b8; background: rgba(148, 163, 184, 0.1); }
898
909
 
910
+ .schema-subtitle {
911
+ font-size: 11px;
912
+ font-weight: 600;
913
+ margin-bottom: 4px;
914
+ color: #64748b;
915
+ }
916
+
899
917
  .empty-message { text-align: center; padding: 40px; color: var(--text-muted); }
900
918
  `;
901
919
  var DOCS_JS = `
@@ -906,7 +924,7 @@ var DOCS_JS = `
906
924
  } catch(e) { console.error('Failed to parse docs', e); }
907
925
 
908
926
  // State
909
- let filters = {
927
+ const filters = {
910
928
  search: '',
911
929
  methods: new Set(),
912
930
  tags: new Set()
@@ -925,14 +943,13 @@ var DOCS_JS = `
925
943
  // Initialization
926
944
  function init() {
927
945
  populateFilters();
928
- render();
929
946
  setupGlobalListeners();
947
+ render();
930
948
  }
931
949
 
932
950
  // Tag Coloring Logic
933
951
  function getTagColorClass(tagName) {
934
952
  if (tagName === 'not-implemented') return 'not-implemented';
935
- // Simple string hash to select color index
936
953
  let hash = 0;
937
954
  for (let i = 0; i < tagName.length; i++) {
938
955
  hash = tagName.charCodeAt(i) + ((hash << 5) - hash);
@@ -951,55 +968,87 @@ var DOCS_JS = `
951
968
  }
952
969
 
953
970
  function populateFilters() {
971
+ if (!elMethodFilters || !elTagFilters) return;
972
+
954
973
  // Extract unique values
955
974
  const allMethods = new Set(leaves.map(l => (l.method || 'GET').toUpperCase()));
956
975
  const allTags = new Set(leaves.flatMap(l => (l.cfg && l.cfg.tags) ? l.cfg.tags : []));
957
976
 
958
977
  // Setup Method Checkboxes
959
978
  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('');
979
+ elMethodFilters.innerHTML = sortedMethods.map(m => (
980
+ '<label class="pill-checkbox">' +
981
+ '<input type="checkbox" value="' + escapeAttr(m) + '" checked>' +
982
+ '<span>' + escapeHtml(m) + '</span>' +
983
+ '</label>'
984
+ )).join('');
966
985
  sortedMethods.forEach(m => filters.methods.add(m));
967
986
 
968
- // Setup Tag Checkboxes (Left Aligned, Colored)
987
+ // Attach listeners to method checkboxes
988
+ elMethodFilters.querySelectorAll('input[type="checkbox"]').forEach(function(cb) {
989
+ cb.addEventListener('change', updateFilters);
990
+ });
991
+
992
+ // Setup Tag Checkboxes
969
993
  if (allTags.size > 0) {
970
994
  elTagFilters.innerHTML = Array.from(allTags).sort().map(t => {
971
995
  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
- \`;
996
+ return (
997
+ '<label class="pill-checkbox colored-tag">' +
998
+ '<input type="checkbox" value="' + escapeAttr(t) + '">' +
999
+ '<span class="tag-filter-pill ' + colorClass + '">' + escapeHtml(t) + '</span>' +
1000
+ '</label>'
1001
+ );
979
1002
  }).join('');
1003
+
1004
+ // Attach listeners to tag checkboxes
1005
+ elTagFilters.querySelectorAll('input[type="checkbox"]').forEach(function(cb) {
1006
+ cb.addEventListener('change', updateFilters);
1007
+ });
980
1008
  }
981
1009
  }
982
1010
 
983
- window.updateFilters = function() {
984
- filters.search = elSearch.value.toLowerCase();
1011
+ function updateFilters() {
1012
+ if (elSearch) {
1013
+ filters.search = elSearch.value.toLowerCase();
1014
+ }
1015
+
985
1016
  filters.methods.clear();
986
- elMethodFilters.querySelectorAll('input:checked').forEach(cb => filters.methods.add(cb.value));
1017
+ if (elMethodFilters) {
1018
+ elMethodFilters.querySelectorAll('input[type="checkbox"]:checked').forEach(function(cb) {
1019
+ filters.methods.add(cb.value);
1020
+ });
1021
+ }
1022
+
987
1023
  filters.tags.clear();
988
- elTagFilters.querySelectorAll('input:checked').forEach(cb => filters.tags.add(cb.value));
1024
+ if (elTagFilters) {
1025
+ elTagFilters.querySelectorAll('input[type="checkbox"]:checked').forEach(function(cb) {
1026
+ filters.tags.add(cb.value);
1027
+ });
1028
+ }
1029
+
989
1030
  render();
990
1031
  }
991
1032
 
1033
+ function setupGlobalListeners() {
1034
+ if (elSearch) {
1035
+ elSearch.addEventListener('input', updateFilters);
1036
+ }
1037
+ }
1038
+
992
1039
  // Core Render Logic
993
1040
  function render() {
1041
+ if (!elRouteList || !elOverview) return;
1042
+
994
1043
  // 1. Filter Data
995
- const filtered = leaves.filter(leaf => {
1044
+ const filtered = leaves.filter(function(leaf) {
996
1045
  const m = (leaf.method || 'GET').toUpperCase();
997
1046
  const t = (leaf.cfg && leaf.cfg.tags) ? leaf.cfg.tags : [];
998
1047
  const path = (leaf.path || '').toLowerCase();
999
1048
  const summary = (leaf.cfg && leaf.cfg.summary || '').toLowerCase();
1000
1049
 
1001
1050
  if (!filters.methods.has(m)) return false;
1002
- if (filters.tags.size > 0 && !t.some(tag => filters.tags.has(tag))) return false;
1051
+ if (filters.tags.size > 0 && !t.some(function(tag) { return filters.tags.has(tag); })) return false;
1003
1052
  if (filters.search && !path.includes(filters.search) && !summary.includes(filters.search)) return false;
1004
1053
 
1005
1054
  return true;
@@ -1013,7 +1062,7 @@ var DOCS_JS = `
1013
1062
 
1014
1063
  // 2. Group Data
1015
1064
  const groups = {};
1016
- filtered.forEach(leaf => {
1065
+ filtered.forEach(function(leaf) {
1017
1066
  const gName = (leaf.cfg && leaf.cfg.docsGroup) ? leaf.cfg.docsGroup : 'UNGROUPED';
1018
1067
  if (!groups[gName]) groups[gName] = [];
1019
1068
  groups[gName].push(leaf);
@@ -1021,19 +1070,23 @@ var DOCS_JS = `
1021
1070
 
1022
1071
  const sortedGroupNames = Object.keys(groups).sort(sortGroups);
1023
1072
 
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('');
1073
+ // 3. Render Overview Chips
1074
+ elOverview.innerHTML = (
1075
+ '<span class="overview-label">JUMP TO:</span>' +
1076
+ sortedGroupNames.map(function(gName) {
1077
+ return (
1078
+ '<a href="#group-' + escapeAttr(gName) + '" ' +
1079
+ 'class="group-chip" data-group="' + escapeAttr(gName) + '">' +
1080
+ escapeHtml(gName) +
1081
+ '</a>'
1082
+ );
1083
+ }).join('')
1084
+ );
1031
1085
 
1032
1086
  // 4. Render Groups & Cards
1033
- const html = sortedGroupNames.map(gName => {
1087
+ const html = sortedGroupNames.map(function(gName) {
1034
1088
  const routes = groups[gName];
1035
- // Sort Routes: Path ASC, then Method
1036
- routes.sort((a, b) => {
1089
+ routes.sort(function(a, b) {
1037
1090
  const pA = a.path || '';
1038
1091
  const pB = b.path || '';
1039
1092
  if (pA < pB) return -1;
@@ -1041,23 +1094,27 @@ var DOCS_JS = `
1041
1094
  return (a.method || '').localeCompare(b.method || '');
1042
1095
  });
1043
1096
 
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
- \`;
1097
+ return (
1098
+ '<div class="api-group" id="group-' + escapeAttr(gName) + '">' +
1099
+ '<div class="group-header">' +
1100
+ '<h2 class="group-title">' + escapeHtml(gName) + '</h2>' +
1101
+ '<div class="group-actions">' +
1102
+ '<button type="button" data-group="' + escapeAttr(gName) + '" data-action="expand">Expand All</button>' +
1103
+ '<button type="button" data-group="' + escapeAttr(gName) + '" data-action="collapse">Collapse All</button>' +
1104
+ '</div>' +
1105
+ '</div>' +
1106
+ '<div class="cards-list">' +
1107
+ routes.map(renderCard).join('') +
1108
+ '</div>' +
1109
+ '</div>'
1110
+ );
1058
1111
  }).join('');
1059
1112
 
1060
1113
  elRouteList.innerHTML = html;
1114
+
1115
+ // Wire up interactions that depend on rendered DOM
1116
+ wireOverviewInteractions();
1117
+ wireCardInteractions();
1061
1118
  }
1062
1119
 
1063
1120
  // Card Renderer
@@ -1069,35 +1126,33 @@ var DOCS_JS = `
1069
1126
  const description = cfg.description || '';
1070
1127
  const tags = cfg.tags || [];
1071
1128
 
1072
- // Header Construction: Method | Path | Tags | Spacer | Chevron
1073
- const tagBadges = tags.map(t => {
1129
+ const tagBadges = tags.map(function(t) {
1074
1130
  const cClass = getTagColorClass(t);
1075
- return \`<span class="status-badge \${cClass}">\${t}</span>\`;
1131
+ return '<span class="status-badge ' + cClass + '">' + escapeHtml(t) + '</span>';
1076
1132
  }).join('');
1077
1133
 
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>\`;
1134
+ const headerHtml =
1135
+ '<div class="card-header">' +
1136
+ '<span class="method-badge m-' + escapeAttr(method) + '">' + escapeHtml(method) + '</span>' +
1137
+ '<div class="path-container" data-path="' + escapeAttr(path) + '" title="Click to copy path">' +
1138
+ '<span class="path-text">' + escapeHtml(path) + '</span>' +
1139
+ '<span class="copy-icon">\u{1F4CB}</span>' +
1140
+ '</div>' +
1141
+ '<div class="tags-container">' +
1142
+ tagBadges +
1143
+ '</div>' +
1144
+ '<div class="header-spacer"></div>' +
1145
+ '<div class="expand-icon">\u25BC</div>' +
1146
+ '</div>';
1147
+
1148
+ let contentHtml =
1149
+ '<div class="section-block"><div class="summary-text">' +
1150
+ '<strong>' + escapeHtml(summary) + '</strong>' +
1151
+ (description
1152
+ ? '<br><span class="description-text">' + escapeHtml(description) + '</span>'
1153
+ : ''
1154
+ ) +
1155
+ '</div></div>';
1101
1156
 
1102
1157
  if (cfg.paramsSchema || cfg.querySchema) {
1103
1158
  contentHtml += '<div class="section-block"><div class="section-title">Parameters</div>';
@@ -1107,96 +1162,150 @@ var DOCS_JS = `
1107
1162
  }
1108
1163
 
1109
1164
  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
- \`;
1165
+ contentHtml +=
1166
+ '<div class="section-block">' +
1167
+ '<div class="section-title">Request Body ' + (cfg.hasBody ? '' : '(Optional)') + '</div>' +
1168
+ renderSchemaTable(cfg.bodySchema) +
1169
+ '</div>';
1116
1170
  }
1117
1171
 
1118
1172
  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
- \`;
1173
+ contentHtml +=
1174
+ '<div class="section-block">' +
1175
+ '<div class="section-title">Response Schema</div>' +
1176
+ renderSchemaTable(cfg.outputSchema) +
1177
+ '</div>';
1125
1178
  }
1126
1179
 
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
- \`;
1180
+ return (
1181
+ '<article class="endpoint-card" data-expanded="false">' +
1182
+ headerHtml +
1183
+ '<div class="card-body">' +
1184
+ contentHtml +
1185
+ '</div>' +
1186
+ '</article>'
1187
+ );
1135
1188
  }
1136
1189
 
1137
1190
  function renderSchemaTable(node, title) {
1138
1191
  let rows = '';
1139
1192
  if (node.kind === 'object' && node.properties) {
1140
- Object.keys(node.properties).forEach(key => {
1193
+ Object.keys(node.properties).forEach(function(key) {
1141
1194
  const prop = node.properties[key];
1142
1195
  const reqClass = prop.optional ? 'req-false' : 'req-true';
1143
1196
  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
- \`;
1197
+ rows +=
1198
+ '<tr>' +
1199
+ '<td class="col-name">' + escapeHtml(key) + '</td>' +
1200
+ '<td class="col-type">' + escapeHtml(getTypeLabel(prop)) + '</td>' +
1201
+ '<td><span class="req-badge ' + reqClass + '">' + reqText + '</span></td>' +
1202
+ '<td>' + escapeHtml(prop.description || '') + '</td>' +
1203
+ '</tr>';
1152
1204
  });
1153
1205
  } else {
1154
- rows = \`<tr><td colspan="4">Type: \${getTypeLabel(node)}</td></tr>\`;
1206
+ rows = '<tr><td colspan="4">Type: ' + escapeHtml(getTypeLabel(node)) + '</td></tr>';
1155
1207
  }
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
- \`;
1208
+ return (
1209
+ (title ? '<div class="schema-subtitle">' + escapeHtml(title) + '</div>' : '') +
1210
+ '<table class="schema-table">' + rows + '</table>'
1211
+ );
1160
1212
  }
1161
1213
 
1162
1214
  function getTypeLabel(node) {
1163
1215
  if (!node) return 'any';
1164
- if (node.kind === 'array') return \`\${getTypeLabel(node.element)}[]\`;
1165
- if (node.enumValues) return \`enum(\${node.enumValues.join('|')})\`;
1216
+ if (node.kind === 'array') return getTypeLabel(node.element) + '[]';
1217
+ if (node.enumValues) return 'enum(' + node.enumValues.join('|') + ')';
1166
1218
  return node.kind || 'any';
1167
1219
  }
1168
1220
 
1169
- function escape(str) {
1170
- if (!str) return '';
1171
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1221
+ // Interactions
1222
+
1223
+ function wireOverviewInteractions() {
1224
+ if (!elOverview || !elRouteList) return;
1225
+
1226
+ // group chips
1227
+ elOverview.querySelectorAll('.group-chip').forEach(function(chip) {
1228
+ chip.addEventListener('click', function(e) {
1229
+ e.preventDefault();
1230
+ const gName = chip.getAttribute('data-group');
1231
+ if (gName) scrollToGroup(gName);
1232
+ });
1233
+ });
1234
+
1235
+ // expand/collapse buttons
1236
+ elRouteList.querySelectorAll('.group-actions button').forEach(function(btn) {
1237
+ btn.addEventListener('click', function() {
1238
+ const gName = btn.getAttribute('data-group');
1239
+ const action = btn.getAttribute('data-action');
1240
+ if (!gName || !action) return;
1241
+ toggleGroup(gName, action === 'expand');
1242
+ });
1243
+ });
1172
1244
  }
1173
1245
 
1174
- window.toggleCard = function(card, e) {
1175
- if (e.target.closest('.path-container')) return; // Don't toggle on path click
1246
+ function wireCardInteractions() {
1247
+ if (!elRouteList) return;
1248
+
1249
+ // card expand/collapse
1250
+ elRouteList.querySelectorAll('.endpoint-card').forEach(function(card) {
1251
+ card.addEventListener('click', function(e) {
1252
+ var target = e.target;
1253
+ if (target && target.closest && target.closest('.path-container')) {
1254
+ // handled separately for copy
1255
+ return;
1256
+ }
1257
+ toggleCard(card);
1258
+ });
1259
+ });
1260
+
1261
+ // copy path
1262
+ elRouteList.querySelectorAll('.path-container').forEach(function(pc) {
1263
+ pc.addEventListener('click', function(e) {
1264
+ e.stopPropagation();
1265
+ const text = pc.getAttribute('data-path') || '';
1266
+ copyText(text);
1267
+ });
1268
+ });
1269
+ }
1270
+
1271
+ function toggleCard(card) {
1176
1272
  const isExpanded = card.getAttribute('data-expanded') === 'true';
1177
- card.setAttribute('data-expanded', !isExpanded);
1178
- };
1273
+ card.setAttribute('data-expanded', String(!isExpanded));
1274
+ }
1179
1275
 
1180
- window.toggleGroup = function(gName, expand) {
1276
+ function toggleGroup(gName, expand) {
1181
1277
  const group = document.getElementById('group-' + gName);
1182
1278
  if (!group) return;
1183
- group.querySelectorAll('.endpoint-card').forEach(c => c.setAttribute('data-expanded', expand));
1184
- };
1279
+ group.querySelectorAll('.endpoint-card').forEach(function(c) {
1280
+ c.setAttribute('data-expanded', String(!!expand));
1281
+ });
1282
+ }
1185
1283
 
1186
- window.copyText = function(e, text) {
1187
- e.stopPropagation();
1188
- navigator.clipboard.writeText(text);
1189
- // Could add visual feedback here
1190
- };
1284
+ function copyText(text) {
1285
+ if (!navigator.clipboard || !text) return;
1286
+ navigator.clipboard.writeText(text).catch(function() {
1287
+ // ignore
1288
+ });
1289
+ }
1191
1290
 
1192
- window.scrollToGroup = function(e, gName) {
1193
- e.preventDefault();
1291
+ function scrollToGroup(gName) {
1194
1292
  const el = document.getElementById('group-' + gName);
1195
- if(el) window.scrollTo({ top: el.offsetTop - 220, behavior: 'smooth' });
1293
+ if (el) window.scrollTo({ top: el.offsetTop - 220, behavior: 'smooth' });
1196
1294
  }
1197
1295
 
1198
- function setupGlobalListeners() {
1199
- elSearch.addEventListener('input', updateFilters);
1296
+ // Escaping helpers
1297
+ function escapeHtml(str) {
1298
+ if (!str && str !== 0) return '';
1299
+ return String(str)
1300
+ .replace(/&/g, '&amp;')
1301
+ .replace(/</g, '&lt;')
1302
+ .replace(/>/g, '&gt;')
1303
+ .replace(/"/g, '&quot;')
1304
+ .replace(/'/g, '&#39;');
1305
+ }
1306
+
1307
+ function escapeAttr(str) {
1308
+ return escapeHtml(str);
1200
1309
  }
1201
1310
 
1202
1311
  init();
@@ -1226,12 +1335,9 @@ function renderLeafDocsHTML(leaves, options = {}) {
1226
1335
 
1227
1336
  <div class="controls-container">
1228
1337
  <div class="filters-row">
1229
-
1230
1338
  <div class="left-column">
1231
-
1232
1339
  <div class="filter-group method-filters-container">
1233
1340
  <div class="filter-label">Search</div>
1234
-
1235
1341
  <div class="search-box">
1236
1342
  <span class="search-icon">\u{1F50D}</span>
1237
1343
  <input type="text" id="searchInput" class="search-input" placeholder="Filter endpoints...">