@gsa-tts/graymatter-ui 0.3.14 → 0.3.15

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/README.md CHANGED
@@ -190,6 +190,7 @@ import {
190
190
  ### Layout Components
191
191
  - **GlobalAppLayout** - Main application layout wrapper
192
192
  - **BaseLayout** - Base page layout structure
193
+ - **DocsLayout** - Documentation layout with dynamic navigation (Astro only)
193
194
 
194
195
  ### UI Components
195
196
  - **ProfileMenu** - User profile dropdown menu with event-driven actions
@@ -235,15 +236,18 @@ Detailed documentation for specific components is available in the package:
235
236
 
236
237
  - **[NAVIGATION_CONTROL.md](https://github.com/GSA-TTS/usai-gov/blob/main/packages/ui/NAVIGATION_CONTROL.md)** - DesktopSideNav external control system with event-based API
237
238
  - **[PROFILE_MENU.md](https://github.com/GSA-TTS/usai-gov/blob/main/packages/ui/PROFILE_MENU.md)** - ProfileMenu component usage and event handling
239
+ - **[README_DocsLayout.md](https://github.com/GSA-TTS/usai-gov/blob/main/packages/ui/README_DocsLayout.md)** - DocsLayout component with dynamic navigation generation (Astro only)
238
240
 
239
241
  These files are also included with the package in your `node_modules/@gsa-tts/graymatter-ui/` directory. If you don't have access to the private repository, you can find the documentation files locally after installation.
240
242
 
241
243
  ## Framework Support
242
244
 
243
- - **Svelte 5** - Native Svelte components
244
- - **Astro** - Full support with client-side hydration
245
- - **React/Vue/Angular** - Via web components
246
- - **Vanilla JavaScript** - Via web components
245
+ - **Svelte 5** - Native Svelte components (individual components only)
246
+ - **Astro** - Full support with client-side hydration (including DocsLayout)
247
+ - **React/Vue/Angular** - Via web components (individual components only)
248
+ - **Vanilla JavaScript** - Via web components (individual components only)
249
+
250
+ **Note:** The `DocsLayout` component is Astro-specific and cannot be used in other frameworks. For non-Astro applications, use the individual Svelte components and shared utilities instead.
247
251
 
248
252
  ## Troubleshooting
249
253
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gsa-tts/graymatter-ui",
3
- "version": "0.3.14",
3
+ "version": "0.3.15",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -18,6 +18,9 @@
18
18
  "./components": "./src/components/index.ts",
19
19
  "./components/*": "./src/components/*",
20
20
  "./constants": "./src/constants/index.ts",
21
+ "./data": "./src/data/index.ts",
22
+ "./data/*": "./src/data/*",
23
+ "./content/api/*": "./src/content/api/*",
21
24
  "./helpers": "./src/helpers/index.ts",
22
25
  "./helpers/*": "./src/helpers/*",
23
26
  "./utils": "./src/utils/index.ts",
@@ -55,9 +58,9 @@
55
58
  "typescript": "5.7.3",
56
59
  "vite": "^6.3.5",
57
60
  "vitest": "^3.0.7",
58
- "@gsa-tts/graymatter-eslint": "0.0.2",
61
+ "@gsa-tts/graymatter-typescript-config": "0.0.2",
59
62
  "@gsa-tts/graymatter-vitest-config": "0.0.1",
60
- "@gsa-tts/graymatter-typescript-config": "0.0.2"
63
+ "@gsa-tts/graymatter-eslint": "0.0.2"
61
64
  },
62
65
  "dependencies": {
63
66
  "@fontsource-variable/archivo": "^5.2.6",
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import ApiDocSubNavMenuItem from './ApiDocSubNavMenuItem.svelte';
3
+ import Self from './ApiDocSubNavList.svelte';
4
+
5
+ const { items, level = 0, selectedId, onClick, onKeyDown } = $props();
6
+ </script>
7
+
8
+ {#each items as item}
9
+ <ApiDocSubNavMenuItem
10
+ {item}
11
+ {level}
12
+ isSelected={selectedId === item.id}
13
+ {onClick}
14
+ {onKeyDown}
15
+ />
16
+ {#if item.children}
17
+ <Self
18
+ items={item.children}
19
+ level={level + 1}
20
+ {selectedId}
21
+ {onClick}
22
+ {onKeyDown}
23
+ />
24
+ {/if}
25
+ {/each}
@@ -0,0 +1,464 @@
1
+ <script lang="ts">
2
+ import { createEventDispatcher, onMount } from 'svelte';
3
+ import { subNavReady } from '@gsa-tts/graymatter-ui/stores/subNavReadyStore';
4
+ import type { SubNavData, SubNavItem } from '../types/subnav.ts';
5
+ import ApiDocSubNavList from './ApiDocSubNavList.svelte';
6
+
7
+ const { data = null } = $props();
8
+
9
+ const sections = $derived(() =>
10
+ !Array.isArray(data) &&
11
+ data &&
12
+ typeof data === 'object' &&
13
+ 'sections' in data &&
14
+ Array.isArray(
15
+ (data as { sections: { header: string; items: SubNavItem[] }[] }).sections
16
+ )
17
+ ? (data as { sections: { header: string; items: SubNavItem[] }[] })
18
+ .sections
19
+ : null
20
+ );
21
+
22
+ const dispatch = createEventDispatcher<{
23
+ navigate: { item: SubNavItem };
24
+ itemSelect: { item: SubNavItem };
25
+ mobileAnchorNavigate: { hash: string };
26
+ }>();
27
+
28
+ function findFirstItem(data: SubNavData | null): SubNavItem | null {
29
+ if (!data) return null;
30
+
31
+ if (Array.isArray(data)) {
32
+ return data[0] || null;
33
+ } else if (
34
+ data &&
35
+ typeof data === 'object' &&
36
+ 'sections' in data &&
37
+ Array.isArray(
38
+ (data as { sections: { header: string; items: SubNavItem[] }[] })
39
+ .sections
40
+ )
41
+ ) {
42
+ const sections = (
43
+ data as { sections: { header: string; items: SubNavItem[] }[] }
44
+ ).sections;
45
+ if (
46
+ sections.length > 0 &&
47
+ sections[0] &&
48
+ sections[0].items &&
49
+ sections[0].items.length > 0
50
+ ) {
51
+ return sections[0].items[0] || null;
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+
57
+ function findItemByHash(
58
+ data: SubNavData | null,
59
+ hash: string
60
+ ): SubNavItem | null {
61
+ if (!data) return null;
62
+
63
+ function searchInItems(items: SubNavItem[]): SubNavItem | null {
64
+ for (const item of items) {
65
+ if (item.href && item.href.includes('#')) {
66
+ const itemHash = item.href.split('#')[1];
67
+ if (itemHash === hash) {
68
+ return item;
69
+ }
70
+ }
71
+ if (item.children) {
72
+ const found = searchInItems(item.children);
73
+ if (found) return found;
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+
79
+ if (Array.isArray(data)) {
80
+ return searchInItems(data);
81
+ } else if (
82
+ data &&
83
+ typeof data === 'object' &&
84
+ 'sections' in data &&
85
+ Array.isArray(
86
+ (data as { sections: { header: string; items: SubNavItem[] }[] })
87
+ .sections
88
+ )
89
+ ) {
90
+ for (const section of (
91
+ data as { sections: { header: string; items: SubNavItem[] }[] }
92
+ ).sections) {
93
+ const found = searchInItems(section.items);
94
+ if (found) return found;
95
+ }
96
+ }
97
+ return null;
98
+ }
99
+
100
+ function findItemById(
101
+ data: SubNavData | null,
102
+ id: string
103
+ ): SubNavItem | null {
104
+ if (!data) return null;
105
+
106
+ function searchInItems(items: SubNavItem[]): SubNavItem | null {
107
+ for (const item of items) {
108
+ if (item.id === id) {
109
+ return item;
110
+ }
111
+ if (item.children) {
112
+ const found = searchInItems(item.children);
113
+ if (found) return found;
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+
119
+ if (Array.isArray(data)) {
120
+ return searchInItems(data);
121
+ } else if (
122
+ data &&
123
+ typeof data === 'object' &&
124
+ 'sections' in data &&
125
+ Array.isArray(
126
+ (data as { sections: { header: string; items: SubNavItem[] }[] })
127
+ .sections
128
+ )
129
+ ) {
130
+ for (const section of (
131
+ data as { sections: { header: string; items: SubNavItem[] }[] }
132
+ ).sections) {
133
+ const found = searchInItems(section.items);
134
+ if (found) return found;
135
+ }
136
+ }
137
+ return null;
138
+ }
139
+
140
+ // Utility: Find top-level parent of a submenu item
141
+ function findTopLevelParent(
142
+ item: SubNavItem,
143
+ data: SubNavData | null
144
+ ): SubNavItem | null {
145
+ if (!data || !item) return null;
146
+ function search(
147
+ items: SubNavItem[],
148
+ parent: SubNavItem | null
149
+ ): SubNavItem | null {
150
+ for (const it of items) {
151
+ if (it.id === item.id) return parent ?? it;
152
+ if (it.children) {
153
+ const found = search(it.children, parent ?? it);
154
+ if (found) return found;
155
+ }
156
+ }
157
+ return null;
158
+ }
159
+ if (Array.isArray(data)) {
160
+ return search(data, null);
161
+ } else if (
162
+ data &&
163
+ typeof data === 'object' &&
164
+ 'sections' in data &&
165
+ Array.isArray(
166
+ (data as { sections: { header: string; items: SubNavItem[] }[] })
167
+ .sections
168
+ )
169
+ ) {
170
+ for (const section of (
171
+ data as { sections: { header: string; items: SubNavItem[] }[] }
172
+ ).sections) {
173
+ const found = search(section.items, null);
174
+ if (found) return found;
175
+ }
176
+ }
177
+ return null;
178
+ }
179
+
180
+ let currentSelectedId = $state<string | null>(null);
181
+ let hydrated = $state(false);
182
+
183
+ const effectiveSelectedId = $derived(() => {
184
+ if (!hydrated || !data) return null;
185
+
186
+ // If we already have a selectedId, use it
187
+ if (currentSelectedId) return currentSelectedId;
188
+
189
+ // First priority: URL hash
190
+ if (typeof window !== 'undefined' && window.location.hash) {
191
+ const hash = window.location.hash.substring(1);
192
+ const targetItem = findItemByHash(data, hash);
193
+ if (targetItem) {
194
+ return targetItem.id;
195
+ }
196
+ }
197
+
198
+ // Second priority: sessionStorage
199
+ if (typeof window !== 'undefined') {
200
+ const storedId = sessionStorage.getItem('subNav-selectedId');
201
+ if (storedId) {
202
+ try {
203
+ const parsedId = JSON.parse(storedId);
204
+ if (parsedId && parsedId !== 'null' && parsedId !== '') {
205
+ const targetItem = findItemById(data, parsedId);
206
+ if (targetItem) {
207
+ return targetItem.id;
208
+ }
209
+ }
210
+ } catch (e) {
211
+ console.log('[SubNavMenu] Error parsing stored ID:', e);
212
+ }
213
+ }
214
+ }
215
+
216
+ // Third priority: first item
217
+ const firstItem = findFirstItem(data);
218
+ if (firstItem && firstItem.title) {
219
+ return firstItem.id;
220
+ }
221
+
222
+ return null;
223
+ });
224
+
225
+ // Update currentSelectedId when effectiveSelectedId changes
226
+ $effect(() => {
227
+ if (effectiveSelectedId() && effectiveSelectedId() !== currentSelectedId) {
228
+ currentSelectedId = effectiveSelectedId();
229
+ }
230
+ });
231
+
232
+ onMount(() => {
233
+ hydrated = true;
234
+
235
+ if (!data) return;
236
+
237
+ // Handle header updates and scrolling
238
+ const targetItem = currentSelectedId
239
+ ? findItemById(data, currentSelectedId)
240
+ : null;
241
+ if (targetItem) {
242
+ // Always resolve top-level parent from data
243
+ const topLevel = findTopLevelParent(targetItem, data) || targetItem;
244
+ import('@gsa-tts/graymatter-ui/stores/navigationStore')
245
+ .then(({ selectedSubNavItemTitle }) => {
246
+ selectedSubNavItemTitle.set(topLevel.title || '');
247
+ if (typeof window !== 'undefined') {
248
+ sessionStorage.setItem(
249
+ 'subNav-selectedId',
250
+ JSON.stringify(targetItem.id)
251
+ );
252
+ sessionStorage.setItem(
253
+ 'subNav-topLevelId',
254
+ JSON.stringify(topLevel.id)
255
+ );
256
+ sessionStorage.setItem(
257
+ 'subNav-topLevelTitle',
258
+ JSON.stringify(topLevel.title || '')
259
+ );
260
+ }
261
+ })
262
+ .catch();
263
+
264
+ // Handle scrolling if we have a URL hash
265
+ if (targetItem.href && targetItem.href.includes('#')) {
266
+ const hash = targetItem.href.split('#')[1];
267
+ if (hash) {
268
+ setTimeout(() => scrollToAnchorWithOffset(hash), 100);
269
+ }
270
+ }
271
+
272
+ // Always dispatch sectionVisible with top-level parent
273
+ if (topLevel.title && typeof window !== 'undefined') {
274
+ window.dispatchEvent(
275
+ new CustomEvent('sectionVisible', {
276
+ detail: {
277
+ title: topLevel.title,
278
+ id: topLevel.id,
279
+ },
280
+ })
281
+ );
282
+ }
283
+ }
284
+ subNavReady.set(true);
285
+ });
286
+
287
+ function handleItemClick(event: Event, item: SubNavItem) {
288
+ // Always prevent default for anchor links
289
+ if (item.href && item.href.includes('#')) {
290
+ event.preventDefault();
291
+ } else if (!item.href || item.href === '#') {
292
+ event.preventDefault();
293
+ }
294
+ currentSelectedId = item.id;
295
+ const topLevel = findTopLevelParent(item, data) || item;
296
+ import('@gsa-tts/graymatter-ui/stores/navigationStore')
297
+ .then(({ selectedSubNavItemTitle }) => {
298
+ selectedSubNavItemTitle.set(topLevel.title || '');
299
+ if (typeof window !== 'undefined') {
300
+ sessionStorage.setItem('subNav-selectedId', JSON.stringify(item.id));
301
+ sessionStorage.setItem(
302
+ 'subNav-topLevelId',
303
+ JSON.stringify(topLevel.id)
304
+ );
305
+ sessionStorage.setItem(
306
+ 'subNav-topLevelTitle',
307
+ JSON.stringify(topLevel.title || '')
308
+ );
309
+ }
310
+ })
311
+ .catch();
312
+ if (typeof window !== 'undefined') {
313
+ if (item.href && item.href.includes('#')) {
314
+ const hash = item.href.split('#')[1];
315
+ if (hash) {
316
+ window.location.hash = hash;
317
+ if (window.innerWidth > 640) {
318
+ // Desktop: use delay for consistent timing
319
+ setTimeout(() => {
320
+ scrollToAnchorWithOffset(hash);
321
+ }, 350);
322
+ } else {
323
+ // Mobile: immediate scroll without delay
324
+ window.dispatchEvent(
325
+ new CustomEvent('mobileAnchorNavigate', { detail: { hash } })
326
+ );
327
+ }
328
+ }
329
+ }
330
+ // Always use top-level parent for header
331
+ if (topLevel.title) {
332
+ window.dispatchEvent(
333
+ new CustomEvent('sectionVisible', {
334
+ detail: {
335
+ title: topLevel.title,
336
+ id: topLevel.id,
337
+ },
338
+ })
339
+ );
340
+ }
341
+ // Mobile menu logic unchanged
342
+ if (window.innerWidth <= 640 && item.href && item.href !== '#') {
343
+ import('@gsa-tts/graymatter-ui/stores/navigationStore').then(
344
+ ({ navigationStore }) => {
345
+ navigationStore.setExpanded(false);
346
+ }
347
+ );
348
+ }
349
+ if (window.innerWidth <= 640) {
350
+ window.dispatchEvent(new CustomEvent('closeMobileMenu'));
351
+ }
352
+ }
353
+ if (item.href && item.href !== '#') {
354
+ dispatch('navigate', { item });
355
+ } else {
356
+ dispatch('itemSelect', { item });
357
+ }
358
+ }
359
+
360
+ function handleKeyDown(event: KeyboardEvent, item: SubNavItem) {
361
+ if (event.key === 'Enter' || event.key === ' ') {
362
+ event.preventDefault();
363
+ handleItemClick(event, item);
364
+ }
365
+ }
366
+
367
+ // Utility: Scroll to anchor with header offset
368
+ function scrollToAnchorWithOffset(anchorId: string) {
369
+ const mainContent = document.getElementById('main-content');
370
+ if (mainContent) {
371
+ const anchor = mainContent.querySelector(
372
+ `[id="${anchorId}"]`
373
+ ) as HTMLElement;
374
+ if (anchor) {
375
+ // Use instant scrolling without animation
376
+ try {
377
+ // Add temporary scroll margin to account for header
378
+ const originalScrollMargin = anchor.style.scrollMarginTop;
379
+ anchor.style.scrollMarginTop = '56px';
380
+
381
+ anchor.scrollIntoView({
382
+ behavior: 'auto',
383
+ block: 'start',
384
+ inline: 'nearest',
385
+ });
386
+
387
+ // Restore original scroll margin after a delay
388
+ setTimeout(() => {
389
+ anchor.style.scrollMarginTop = originalScrollMargin;
390
+ }, 1000);
391
+ } catch {
392
+ // Fallback to manual calculation
393
+ const headerHeight = 56;
394
+ const rect = anchor.getBoundingClientRect();
395
+ const scrollTop = window.scrollY + rect.top - headerHeight;
396
+ const finalScrollTop = Math.max(0, scrollTop);
397
+ window.scrollTo({ top: finalScrollTop, behavior: 'auto' });
398
+ }
399
+ }
400
+ }
401
+ }
402
+ </script>
403
+
404
+ <div class="sub-menu-container">
405
+ <div class="sub-menu-content">
406
+ {#if data}
407
+ {#if Array.isArray(data)}
408
+ <ApiDocSubNavList
409
+ items={data}
410
+ level={0}
411
+ selectedId={effectiveSelectedId()}
412
+ onClick={handleItemClick}
413
+ onKeyDown={handleKeyDown}
414
+ />
415
+ {/if}
416
+ {#each sections() ?? [] as section: { header: string; items: SubNavItem[] }}
417
+ <div class="sub-menu-header">
418
+ {section.header}
419
+ </div>
420
+ <ApiDocSubNavList
421
+ items={section.items}
422
+ level={0}
423
+ selectedId={effectiveSelectedId()}
424
+ onClick={handleItemClick}
425
+ onKeyDown={handleKeyDown}
426
+ />
427
+ {/each}
428
+ {/if}
429
+ </div>
430
+ </div>
431
+
432
+ <style>
433
+ .sub-menu-container {
434
+ display: flex;
435
+ padding: 0 var(--ai-size-8) var(--ai-size-16) var(--ai-size-8);
436
+ flex-direction: column;
437
+ align-items: flex-start;
438
+ gap: var(--ai-size-32);
439
+ align-self: stretch;
440
+ min-height: 6.25rem;
441
+ }
442
+ .sub-menu-content {
443
+ display: flex;
444
+ flex-direction: column;
445
+ align-items: flex-start;
446
+ gap: var(--ai-size-2);
447
+ align-self: stretch;
448
+ }
449
+ .sub-menu-header {
450
+ display: flex;
451
+ padding: var(--ai-size-8);
452
+ justify-content: flex-start;
453
+ align-items: center;
454
+ gap: var(--ai-size-10);
455
+ align-self: stretch;
456
+ color: var(--ai-color-steel-900);
457
+ font-size: var(--ai-size-12);
458
+ font-style: normal;
459
+ font-weight: 600;
460
+ line-height: 1.2;
461
+ letter-spacing: 0.0075rem;
462
+ text-transform: uppercase;
463
+ }
464
+ </style>
@@ -0,0 +1,116 @@
1
+ <script lang="ts">
2
+ const { item, level = 0, isSelected = false, onClick, onKeyDown } = $props();
3
+ const levelClass = $derived(() => `level-${level}`);
4
+ const hasHref = $derived(() => item.href && item.href !== '#');
5
+ </script>
6
+
7
+ {#if hasHref()}
8
+ <a
9
+ href={item.href}
10
+ class={`menu-item ${levelClass()}`}
11
+ class:selected={isSelected}
12
+ onclick={e => onClick(e, item)}
13
+ >
14
+ <div class="menu-item-content">
15
+ <span class="menu-item-text" class:selected={isSelected}>
16
+ {item.title}
17
+ </span>
18
+ </div>
19
+ </a>
20
+ {:else}
21
+ <div
22
+ class={`menu-item ${levelClass()}${!hasHref() ? ' non-clickable' : ''}`}
23
+ class:selected={isSelected}
24
+ tabindex="0"
25
+ role="button"
26
+ aria-label={`Select ${item.title}`}
27
+ onclick={e => onClick(e, item)}
28
+ onkeydown={e => onKeyDown(e, item)}
29
+ >
30
+ <div class="menu-item-content">
31
+ <span class="menu-item-text" class:selected={isSelected}>
32
+ {item.title}
33
+ </span>
34
+ </div>
35
+ </div>
36
+ {/if}
37
+
38
+ <style>
39
+ .menu-item {
40
+ display: flex;
41
+ height: 2.1625rem; /* 34.6px */
42
+ padding: var(--ai-size-8);
43
+ align-items: center;
44
+ gap: var(--ai-size-4);
45
+ align-self: stretch;
46
+ width: 100%;
47
+ background: none;
48
+ border: none;
49
+ cursor: pointer;
50
+ text-decoration: none;
51
+ color: inherit;
52
+ border-radius: var(--ai-size-4);
53
+ transition: background-color var(--ai-transition-ease-fast);
54
+ }
55
+
56
+ .menu-item:hover {
57
+ background-color: var(--ai-color-steel-200);
58
+ }
59
+
60
+ .menu-item:focus {
61
+ outline: var(--ai-size-2) solid var(--ai-color-blue-600);
62
+ outline-offset: var(--ai-size-2);
63
+ }
64
+
65
+ /* Hide focus outline on click */
66
+ .menu-item:focus:not(:focus-visible) {
67
+ outline: none;
68
+ }
69
+
70
+ .menu-item.selected {
71
+ background-color: var(--ai-color-steel-200);
72
+ }
73
+
74
+ .menu-item.non-clickable {
75
+ cursor: default;
76
+ }
77
+
78
+ .menu-item.non-clickable:hover {
79
+ background-color: transparent;
80
+ }
81
+
82
+ /* Nesting levels with indentation */
83
+ .menu-item.level-0 {
84
+ padding-left: var(--ai-size-8);
85
+ }
86
+
87
+ .menu-item.level-1 {
88
+ padding-left: var(--ai-size-20);
89
+ }
90
+
91
+ .menu-item.level-2 {
92
+ padding-left: var(--ai-size-32);
93
+ }
94
+
95
+ .menu-item-content {
96
+ display: flex;
97
+ align-items: center;
98
+ gap: var(--ai-size-10);
99
+ flex: 1 0 0;
100
+ }
101
+
102
+ .menu-item-text {
103
+ color: var(--ai-color-steel-800);
104
+ font-size: var(--ai-size-14);
105
+ font-style: normal;
106
+ font-weight: 400;
107
+ line-height: 1.3;
108
+ transition: all var(--ai-transition-ease-fast);
109
+ }
110
+
111
+ .menu-item-text.selected,
112
+ .menu-item.selected .menu-item-text {
113
+ color: var(--ai-color-black);
114
+ font-weight: 600;
115
+ }
116
+ </style>
@@ -12,6 +12,9 @@ export { default as MobileBottomNav } from './MobileBottomNav.svelte';
12
12
  export { default as MobileSideMenu } from './MobileSideMenu.svelte';
13
13
  export { default as NavigationInitializer } from './NavigationInitializer.svelte';
14
14
  export { default as UsaSkipNav } from './UsaSkipNav.svelte';
15
+ export { default as ApiDocSubNavMenu } from './ApiDocSubNavMenu.svelte';
16
+ export { default as ApiDocSubNavList } from './ApiDocSubNavList.svelte';
17
+ export { default as ApiDocSubNavMenuItem } from './ApiDocSubNavMenuItem.svelte';
15
18
 
16
19
  // Icon Components
17
20
  export * from './icons/index.js';
@@ -0,0 +1,174 @@
1
+ ---
2
+ title: 'Endpoints'
3
+ description: ''
4
+ sortOrder: 20
5
+ ---
6
+
7
+ API endpoints will be available when you login.
8
+
9
+ ### 1. Models
10
+
11
+ GET /api/v1/models
12
+
13
+ To reach models the documented endpoint is `<base url>/api/v1/models`, which will allow you to retrieve available model information.
14
+
15
+ **Example Request**
16
+
17
+ ```bash
18
+ curl -X 'GET' \
19
+ '<base url>/api/v1/models' \
20
+ -H 'accept: application/json' \
21
+ -H 'Authorization: Bearer <Your API Key>'
22
+ ```
23
+ **Example Response**
24
+
25
+ ```json
26
+ {
27
+ "object": "list",
28
+ "data": [
29
+ {
30
+ "id": "claude_3_5_sonnet",
31
+ "created": 1718841600,
32
+ "object": "model",
33
+ "owned_by": "Anthropic"
34
+ },
35
+ {
36
+ "id": "llama3211b",
37
+ "created": 1727222400,
38
+ "object": "model",
39
+ "owned_by": "Meta"
40
+ },
41
+ {
42
+ "id": "cohere_english_v3",
43
+ "created": 1698883200,
44
+ "object": "model",
45
+ "owned_by": "Cohere"
46
+ }
47
+ ]
48
+ }
49
+ ```
50
+ ### 2. Chat Completions
51
+
52
+ POST /api/v1/chat/completions
53
+
54
+ To reach chat completions the documented endpoint is `<base url>/api/v1/chat/completions`, which will allow you to retrieve chat completion information.
55
+
56
+ **Request Body**
57
+
58
+ * **model**: The model ID (e.g: gemini-2.0-flash, claude_3_haiku)
59
+ * **messages**: An array of of message items consisting of User message, Image Content, Document Content, Assistant Message
60
+ * **max_tokens**: Maximum response length (optional)
61
+ * **temperature**: Response creativity (0.0-2.0, optional)
62
+
63
+ | **Models** | **Range** | **Default value** |
64
+ | :------- | :------: | -------: |
65
+ | Google models | 0.0 - 2.0 | 1.0 |
66
+ | Anthropic models | 0.0 - 1.0 | 0.5 |
67
+ | Meta models | 0.0 - 1.0 | 0.5 |
68
+
69
+ * **top_p**: An alternative to sampling with temperature, called nucleus sampling (optional)
70
+ * **stream**: a boolean indicating whether to send partials responses as available (optional)
71
+
72
+ **Example Request**
73
+
74
+ ```bash
75
+ curl -X 'POST' \
76
+ '<base url>/api/v1/chat/completions' \
77
+ -H 'accept: application/json' \
78
+ -H 'Authorization: Bearer <Your API Key>' \
79
+ -H 'Content-Type: application/json' \
80
+ -d '{
81
+ "messages": [
82
+ {
83
+ "content": "You speak only pirate",
84
+ "role": "system"
85
+ },
86
+ {
87
+ "content": "Hello!",
88
+ "role": "user"
89
+ }
90
+ ],
91
+ "model": "gemini-2.0-flash"
92
+ }'
93
+ ```
94
+
95
+ **Example Response**
96
+
97
+ ```json
98
+ {
99
+ "object": "chat.completion",
100
+ "created": 1748517745,
101
+ "model": "gemini-2.0-flash",
102
+ "choices": [
103
+ {
104
+ "index": 0,
105
+ "message": {
106
+ "role": "assistant",
107
+ "content": "Ahoy there, matey! What brings ye to me waters?\n"
108
+ },
109
+ "finish_reason": "stop"
110
+ }
111
+ ],
112
+ "usage": {
113
+ "prompt_tokens": 14,
114
+ "completion_tokens": 15,
115
+ "total_tokens": 29
116
+ }
117
+ }
118
+
119
+ ```
120
+
121
+ ### 3. Embeddings
122
+
123
+ POST /api/v1/embeddings
124
+
125
+ To reach embeddings the documented endpoint is `<base url>/api/v1/embeddings`, which will allow you to retrieve embeddings information.
126
+
127
+ **Request Body**
128
+
129
+ * **model:** The model ID (e.g: cohere_english_v3)
130
+ * **input:** Input text to embed, encoded as a string or array of strings. Each input must not exceed the max input tokens for the model.
131
+ dimensions: The number of dimensions the resulting output embeddings should have. Only supported in some models (Optional)
132
+ * **input_type:** (Note: this is not part of the OpenAI specification but is useful on some models). Specify the kind of input to allow the model to optimize for specific uses. Options are: "search_document", "search_query", "classification", "clustering", "semantic_similarity" (Optional)
133
+
134
+ **Example Request**
135
+
136
+ ```bash
137
+ curl -X 'POST' \
138
+ '<base url>/api/v1/embeddings' \
139
+ -H 'accept: application/json' \
140
+ -H 'Authorization: Bearer <Your API Key>\
141
+ -H 'Content-Type: application/json' \
142
+ -d '{
143
+ "encodingFormat": "float",
144
+ "input": "A mighty woman with a torch, whose flame / Is the imprisoned lightning",
145
+ "input_type": "search_document",
146
+ "model": "cohere_english_v3"
147
+ }
148
+ ```
149
+ **Example Response**
150
+
151
+ ```json
152
+ {
153
+ "object": "list",
154
+ "data": [
155
+ {
156
+ "object": "embedding",
157
+ "embedding": [
158
+ 0.06933594,
159
+ -0.030883789,
160
+ 0.054351807,
161
+ -0.0018196106,
162
+
163
+ 0.013938904
164
+ ],
165
+ "index": 0
166
+ }
167
+ ],
168
+ "model": "cohere.embed-english-v3",
169
+ "usage": {
170
+ "promptTokens": 12,
171
+ "totalTokens": 12
172
+ }
173
+ }
174
+ ```
@@ -0,0 +1,84 @@
1
+ ---
2
+ title: 'Getting Started'
3
+ description: ''
4
+ sortOrder: 10
5
+ ---
6
+
7
+ ### Introduction
8
+
9
+ USAi API provides programmatic access to AI services for government users and approved partners. It currently supports Chat Completions (model inference) and Embeddings (used for RAG and other applications).
10
+
11
+ Our API has resource-oriented URLs, accepts JSON request bodies, returns JSON responses, and uses HTTP response codes to indicate API errors, authentication messaging, and verbs.
12
+
13
+ ### Models and endpoints
14
+
15
+ USAi API is organized around [REST](http://en.wikipedia.org/wiki/Representational_State_Transfer) and provides access to the large language models (LLMs):
16
+
17
+ **Google AI models**
18
+ - [Gemini 2.5 Flash](https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash)
19
+ - [Gemini 2.5 Pro](https://ai.google.dev/gemini-api/docs/models#gemini-2.5-pro)
20
+
21
+ **Anthropic models**
22
+ - [Claude Haiku 3.5](https://docs.claude.com/en/docs/overview)
23
+ - [Claude Sonnet 3.7](https://docs.claude.com/en/docs/overview)
24
+ - [Claude Sonnet 4](https://docs.claude.com/en/docs/overview)
25
+ - [Claude Opus 4](https://docs.claude.com/en/docs/overview)
26
+
27
+ **Meta models**
28
+ - [Llama 3.2 11B](https://www.llama.com/docs/model-cards-and-prompt-formats/llama3_2/)
29
+ - [Llama 4 Maverick](https://www.llama.com/docs/model-cards-and-prompt-formats/llama4/)
30
+
31
+ The interface is modeled after the [OpenAI Chat Completion API](https://platform.openai.com/docs/guides/text?api-mode=responses). We will continue to add more models over time; we may also remove models if they are found to not meet our standards. You will be notified if a model is removed.
32
+
33
+ Each model we draw on has unique capabilities. Models may also respond differently to the same API request. Below is a summary of each endpoint’s functionality:
34
+
35
+ - **Chat Completions:** The ability to send prompts and receive a response from the LLM models.
36
+ - **Embedding:** Converts input into numeric vectors representing semantic meaning. The vectors are used for tasks like retrieving information from documents, document analysis, and building Retrieval Augmented Generation (RAG) systems.
37
+ - **Models:** A list of models and IDs used to specify LLM models in requests.
38
+
39
+ #### Content types
40
+
41
+ The models USAi API draws on support multiple types of inputs; text, image, and file content types. However, each model has different input capabilities: for example, Claude Sonnet supports Optical Character Recognition (OCR) and recognizes image-only PDFs, while Claude Haiku and Llama currently do not. For additional detail about how we handle these content types, reference [OpenAI’s API documentation](https://platform.openai.com/docs/api-reference/chat/create).
42
+
43
+ ### Authentication
44
+
45
+ All API requests require an API key. Upon authentication, your agency-specific instructions for requesting an API key, and the API endpoint, will be available.
46
+
47
+ ### Limitations
48
+
49
+ #### Rate limitations
50
+
51
+ We currently have a rate limit of 3 calls / second / API key. If you hit a rate limit, you should expect a 429 error code. If you need additional capacity, please contact us at [support@usai.gov](mailto:support@usai.gov). We can work with our infrastructure providers to secure higher model limits.
52
+
53
+ #### Feature limitations
54
+
55
+ Because the underlying platforms have different capabilities and interfaces, not all features of the Chat Completions API are available at this time. Current known limitations include:
56
+
57
+ - Audio
58
+ - Structured output
59
+
60
+ #### Guardrail limitations
61
+
62
+ We do not have guardrails beyond those provided by our model providers; this means that an API user has full control over what inputs and outputs are allowed when using the API. We expect API users to interact with the API ethically and deliberately, and add their own guardrails and system prompts as needed. We recommend implementing system prompts when using the API in situations where you do not know what inputs the model will receive.
63
+
64
+ System prompts should address your needs and concerns. Some of the system prompts in USAi Chat include:
65
+ - You are a helpful assistant that works for a government agency.
66
+ - You help users with general knowledge, problem-solving, coding, and interactive tasks.
67
+ - You maintain a friendly, helpful, professional, and empathetic tone at all times.
68
+ - You want to understand the user's intent, and apply your knowledge and background to formulate the most helpful response possible.
69
+ - Redirect conversations that veer into inappropriate, illegal, or explicit territory.
70
+ - You're not an expert in government policies, security, safety, health, procurement, contracts, or law. Provide general guidance only and advise users to reference appropriate material.
71
+ - Prioritize historical accuracy, scientific inquiry, and objectivity in all responses.
72
+ - Break down complex questions and walk users through solutions step-by-step.
73
+ - Use real-world analogies to simplify complex concepts.
74
+ - When the user's request is unclear, ask for more details to help refine your response.
75
+ - Ask users for feedback on the answer that can help you respond more accurately.
76
+ - Never knowingly make false statements or deceive users.
77
+ - Avoid generating explicit, hateful, dangerous, or illegal content.
78
+ - Protect privacy and do not share personal information about individuals.
79
+ - Redirect users' requests around potentially controversial or polarizing topics quickly.
80
+ - You do not prefer or recommend specific political views, groups, religions, companies, products, or enterprise.
81
+
82
+ ### Code demo
83
+
84
+ To see an example of how to implement features using Python, visit our [HTML example notebook](https://www.usai.gov/assets/files/jupyter_example.html). If you would like to run this file in a Jupyter environment, you can download the <a href="https://www.usai.gov/assets/files/jupyter_example.ipynb" download>notebook ipynb file</a>.
@@ -0,0 +1,10 @@
1
+ ---
2
+ title: 'Support'
3
+ description: ''
4
+ sortOrder: 30
5
+ ---
6
+
7
+ You may encounter issues while working with the USAi API, as it is under active development. We may not be able to fix all issues immediately, but will add them to our backlog for future releases. If you have any questions, hit rate limits, or otherwise need assistance, please contact us at [support@usai.gov](mailto:support@usai.gov).
8
+
9
+
10
+
@@ -0,0 +1,178 @@
1
+ ---
2
+ import GlobalAppLayout from './GlobalAppLayout.astro';
3
+ import ApiDocSubNavMenu from '../components/ApiDocSubNavMenu.svelte';
4
+ import NavigationInitializer from '../components/NavigationInitializer.svelte';
5
+ import { getBaseUrl } from '../helpers';
6
+
7
+ const gtmID = import.meta.env.PUBLIC_GTM_ID;
8
+ const {
9
+ navigationItem = 'api',
10
+ title,
11
+ description,
12
+ openGraphImage,
13
+ showAppIcons = false,
14
+ subNavData = null,
15
+ disableCodeCopyButtons = false,
16
+ postItems = null,
17
+ profileMenuData = null,
18
+ } = Astro.props;
19
+ ---
20
+
21
+ <GlobalAppLayout
22
+ ssrSelectedItem={navigationItem}
23
+ gtmID={gtmID}
24
+ {showAppIcons}
25
+ {profileMenuData}
26
+ logoLinkUrl={getBaseUrl()}
27
+ logoLinkLabel="Homepage"
28
+ title={title}
29
+ {description}
30
+ {openGraphImage}
31
+ >
32
+ <!-- Initialize navigation state and auto-expand submenu for section pages -->
33
+ <NavigationInitializer
34
+ item={navigationItem}
35
+ autoExpand={true}
36
+ client:only="svelte"
37
+ />
38
+
39
+ <!-- Documentation Sub Navigation -->
40
+ {
41
+ subNavData && (
42
+ <ApiDocSubNavMenu
43
+ data={subNavData}
44
+ selectedId={null}
45
+ slot="sub-nav"
46
+ client:load
47
+ />
48
+ )
49
+ }
50
+
51
+ <!-- Main content area -->
52
+ <div class="docs-content">
53
+ <article class="article-body">
54
+ <h1 class="page-title">API Documentation</h1>
55
+
56
+ <!-- Render postItems if provided -->
57
+ {
58
+ postItems &&
59
+ postItems.map((post: any) => (
60
+ <div>
61
+ <h2 class="title" id={post.id}>
62
+ {post.data.title}
63
+ </h2>
64
+ <post.ContentComponent />
65
+ </div>
66
+ ))
67
+ }
68
+ </article>
69
+ </div>
70
+ </GlobalAppLayout>
71
+
72
+ <!-- Add copy button to all code blocks -->
73
+ {
74
+ !disableCodeCopyButtons && (
75
+ <script>
76
+ import {setupCopyButtons} from '../utils/copyButtonScript.js';
77
+ setupCopyButtons();
78
+ </script>
79
+ )
80
+ }
81
+
82
+ <style>
83
+ body {
84
+ color: var(--ai-color-steel-900);
85
+ line-height: var(--ai-font-lineheight-prose);
86
+ }
87
+
88
+ :is(h1, h2, h3, h4, h5, h6) {
89
+ color: var(--ai-color-black);
90
+ }
91
+
92
+ .docs-content {
93
+ padding: 0;
94
+ }
95
+
96
+ .article-body {
97
+ max-width: 84ch;
98
+ }
99
+
100
+ :global(.ai-copy-btn) {
101
+ width: var(--ai-size-32);
102
+ height: var(--ai-size-32);
103
+ position: absolute;
104
+ top: var(--ai-size-8);
105
+ right: var(--ai-size-8);
106
+ background: none;
107
+ border-radius: var(--ai-size-4);
108
+ border: none;
109
+ padding: 0;
110
+ cursor: pointer;
111
+ box-shadow: 0 var(--ai-size-2) var(--ai-size-8) rgba(0, 0, 0, 0.08);
112
+ display: flex;
113
+ align-items: center;
114
+ justify-content: center;
115
+ transition:
116
+ background var(--ai-duration-fast),
117
+ border var(--ai-duration-fast);
118
+ z-index: var(--ai-layer-2);
119
+ overflow: hidden;
120
+ }
121
+ :global(.ai-copy-btn:hover),
122
+ :global(.ai-copy-btn:focus) {
123
+ background: var(--ai-color-steel-800);
124
+ }
125
+ :global(.ai-copy-btn svg) {
126
+ display: block;
127
+ position: relative;
128
+ }
129
+ :global(.ai-code-block-wrapper) {
130
+ position: relative;
131
+ display: block;
132
+ }
133
+ :global(pre) {
134
+ font-family: var(--ai-font-family-monospace);
135
+ font-size: var(--ai-size-13);
136
+ font-weight: var(--ai-font-weight-normal);
137
+ }
138
+
139
+ /* Table styling - only for documentation pages */
140
+ :global(.docs-content table) {
141
+ width: 100%;
142
+ border-collapse: collapse;
143
+ margin: var(--ai-size-24) 0;
144
+ }
145
+
146
+ :global(.docs-content table th),
147
+ :global(.docs-content table td) {
148
+ padding: var(--ai-size-12) var(--ai-size-16);
149
+ text-align: left;
150
+ border-bottom: 1px solid var(--ai-color-neutral-200);
151
+ }
152
+
153
+ :global(.docs-content table th) {
154
+ background-color: var(--ai-color-neutral-50);
155
+ font-weight: var(--ai-font-weight-semibold);
156
+ color: var(--ai-color-black);
157
+ }
158
+
159
+ :global(.docs-content table tbody tr:hover) {
160
+ background-color: var(--ai-color-neutral-25);
161
+ }
162
+
163
+ :global(.docs-content table tbody tr:last-child td) {
164
+ border-bottom: none;
165
+ }
166
+
167
+ @media (--ai-size-breakpoint-tablet) {
168
+ .docs-content {
169
+ padding: 2rem 0;
170
+ }
171
+ }
172
+
173
+ @media (--ai-size-breakpoint-desktop) {
174
+ .docs-content {
175
+ padding: 2rem;
176
+ }
177
+ }
178
+ </style>
@@ -0,0 +1,15 @@
1
+ export interface SubNavItem {
2
+ id: string;
3
+ title: string;
4
+ href?: string;
5
+ children?: SubNavItem[];
6
+ }
7
+
8
+ export interface SubNavSection {
9
+ header: string;
10
+ items: SubNavItem[];
11
+ }
12
+
13
+ export interface SubNavData {
14
+ sections: SubNavSection[];
15
+ }
@@ -0,0 +1,107 @@
1
+ interface CopyButtonConfig {
2
+ buttonClass?: string;
3
+ copyIconClass?: string;
4
+ checkIconClass?: string;
5
+ timeout?: number;
6
+ }
7
+
8
+ export function initializeCopyButtons(config: CopyButtonConfig = {}) {
9
+ const {
10
+ buttonClass = 'ai-copy-btn',
11
+ copyIconClass = 'ai-copy-icon',
12
+ checkIconClass = 'ai-check-icon',
13
+ timeout = 1200,
14
+ } = config;
15
+
16
+ // Function to add a button to a visible code block
17
+ function addCopyButton(pre: HTMLPreElement) {
18
+ const codeBlock = pre.querySelector('code');
19
+ if (!codeBlock || pre.querySelector(`.${buttonClass}`)) return;
20
+
21
+ const wrapper = codeBlock.closest('.ai-code-block-wrapper');
22
+ if (!wrapper) return;
23
+
24
+ const button = document.createElement('button');
25
+ button.className = buttonClass;
26
+ button.type = 'button';
27
+ button.setAttribute('aria-label', 'Copy code to clipboard');
28
+
29
+ button.innerHTML = `
30
+ <!-- Inline Copy Icon -->
31
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
32
+ stroke-width="1.5" stroke="#f2f4f7"
33
+ class="usai-icon ${copyIconClass} size-6"
34
+ aria-hidden="true" focusable="false" role="img"
35
+ style="width:1.25em;height:1.25em;">
36
+ <path stroke-linecap="round" stroke-linejoin="round"
37
+ d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
38
+ </svg>
39
+
40
+ <!-- Inline Check Icon -->
41
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
42
+ stroke-width="1.5" stroke="#f2f4f7"
43
+ class="usai-icon ${checkIconClass} size-6"
44
+ aria-hidden="true" focusable="false" role="img"
45
+ style="display:none; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:1.25em; height:1.25em;">
46
+ <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
47
+ </svg>
48
+ `;
49
+
50
+ button.onclick = () => {
51
+ const lines = codeBlock.querySelectorAll('.line');
52
+ const textToCopy =
53
+ lines.length > 0
54
+ ? Array.from(lines)
55
+ .map(line => line.textContent ?? '')
56
+ .join('\n')
57
+ : (codeBlock.textContent ?? '');
58
+
59
+ navigator.clipboard.writeText(textToCopy);
60
+
61
+ const copyIcon = button.querySelector(
62
+ `.${copyIconClass}`
63
+ ) as SVGElement | null;
64
+ const checkIcon = button.querySelector(
65
+ `.${checkIconClass}`
66
+ ) as SVGElement | null;
67
+
68
+ if (copyIcon && checkIcon) {
69
+ copyIcon.style.display = 'none';
70
+ checkIcon.style.display = '';
71
+ button.setAttribute('aria-label', 'Copied!');
72
+ setTimeout(() => {
73
+ copyIcon.style.display = '';
74
+ checkIcon.style.display = 'none';
75
+ button.setAttribute('aria-label', 'Copy code to clipboard');
76
+ }, timeout);
77
+ }
78
+ };
79
+
80
+ wrapper.appendChild(button);
81
+ }
82
+
83
+ const observer = new IntersectionObserver(
84
+ (entries, obs) => {
85
+ for (const entry of entries) {
86
+ if (entry.isIntersecting) {
87
+ const pre = entry.target as HTMLPreElement;
88
+ addCopyButton(pre);
89
+ obs.unobserve(pre);
90
+ }
91
+ }
92
+ },
93
+ { rootMargin: '200px' }
94
+ );
95
+
96
+ document.querySelectorAll('pre').forEach(pre => {
97
+ observer.observe(pre);
98
+ });
99
+ }
100
+
101
+ export function setupCopyButtons() {
102
+ if (typeof window !== 'undefined') {
103
+ window.addEventListener('DOMContentLoaded', () => {
104
+ initializeCopyButtons();
105
+ });
106
+ }
107
+ }
@@ -0,0 +1,113 @@
1
+ import type { SubNavData } from '../types/subnav.js';
2
+
3
+ // Interface for API documentation metadata
4
+ export interface ApiDocMetadata {
5
+ id: string;
6
+ slug: string;
7
+ title: string;
8
+ description: string;
9
+ sortOrder: number;
10
+ content?: string; // Raw MDX content for heading extraction
11
+ }
12
+
13
+ // Interface for navigation items with recursive children
14
+ interface NavItem {
15
+ id: string;
16
+ title: string;
17
+ href: string;
18
+ children: NavItem[];
19
+ }
20
+
21
+ // Extract headings from MDX content
22
+ function extractHeadings(content: string) {
23
+ const headingRegex = /^(#{1,6})\s+(.+)$/gm;
24
+ const headings: Array<{ level: number; text: string; id: string }> = [];
25
+ let match;
26
+
27
+ while ((match = headingRegex.exec(content)) !== null) {
28
+ const level = match[1]?.length || 0;
29
+ const text = match[2]?.trim() || '';
30
+ // Create a simple ID from the heading text
31
+ const id = text
32
+ .toLowerCase()
33
+ .replace(/[^a-z0-9\s-]/g, '')
34
+ .replace(/\s+/g, '-');
35
+ headings.push({ level, text, id });
36
+ }
37
+
38
+ return headings;
39
+ }
40
+
41
+ // Convert headings to nested navigation structure
42
+ function headingsToNavItems(
43
+ headings: Array<{ level: number; text: string; id: string }>
44
+ ): NavItem[] {
45
+ const items: NavItem[] = [];
46
+ const stack: Array<NavItem & { level: number }> = [];
47
+
48
+ headings.forEach(heading => {
49
+ const item = {
50
+ id: heading.id,
51
+ title: heading.text,
52
+ href: `#${heading.id}`,
53
+ children: [],
54
+ };
55
+
56
+ // Find the correct parent level
57
+ while (stack.length > 0) {
58
+ const lastItem = stack[stack.length - 1];
59
+ if (lastItem && lastItem.level >= heading.level) {
60
+ stack.pop();
61
+ } else {
62
+ break;
63
+ }
64
+ }
65
+
66
+ if (stack.length === 0) {
67
+ // Top level item
68
+ items.push(item);
69
+ } else {
70
+ // Nested item
71
+ const parent = stack[stack.length - 1];
72
+ if (parent) {
73
+ parent.children.push(item);
74
+ }
75
+ }
76
+
77
+ stack.push({ ...item, level: heading.level });
78
+ });
79
+
80
+ return items;
81
+ }
82
+
83
+ // Generate navigation data dynamically from API documentation metadata
84
+ export function generateApiDocumentationNavData(
85
+ apiDocs: ApiDocMetadata[]
86
+ ): SubNavData {
87
+ // Sort by sortOrder
88
+ const sortedDocs = [...apiDocs].sort((a, b) => a.sortOrder - b.sortOrder);
89
+
90
+ // Convert to navigation items with dynamic heading extraction
91
+ const items = sortedDocs.map(doc => {
92
+ // Extract headings from content if provided
93
+ const children = doc.content
94
+ ? headingsToNavItems(extractHeadings(doc.content))
95
+ : [];
96
+
97
+ return {
98
+ id: doc.slug,
99
+ title: doc.title,
100
+ href: `/api/documentation#${doc.slug}`,
101
+ children,
102
+ };
103
+ });
104
+
105
+ return {
106
+ sections: [
107
+ {
108
+ header: 'API Documentation',
109
+ items,
110
+ },
111
+ ],
112
+ };
113
+ }
@@ -1,2 +1,4 @@
1
1
  export * from './getAppUrls.js';
2
2
  export * from './navigationControl.js';
3
+ export * from './copyButtonScript.js';
4
+ export * from './generateApiNavData.js';