@gsa-tts/graymatter-ui 0.3.13 → 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 +8 -4
- package/package.json +6 -3
- package/src/components/ApiDocSubNavList.svelte +25 -0
- package/src/components/ApiDocSubNavMenu.svelte +464 -0
- package/src/components/ApiDocSubNavMenuItem.svelte +116 -0
- package/src/components/index.ts +3 -0
- package/src/content/api/endpoints.mdx +174 -0
- package/src/content/api/getting-started.mdx +84 -0
- package/src/content/api/support.mdx +10 -0
- package/src/layouts/BaseLayout.astro +17 -0
- package/src/layouts/DocsLayout.astro +178 -0
- package/src/types/subnav.ts +15 -0
- package/src/utils/copyButtonScript.ts +107 -0
- package/src/utils/generateApiNavData.ts +113 -0
- package/src/utils/index.ts +2 -0
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.
|
|
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",
|
|
@@ -56,8 +59,8 @@
|
|
|
56
59
|
"vite": "^6.3.5",
|
|
57
60
|
"vitest": "^3.0.7",
|
|
58
61
|
"@gsa-tts/graymatter-typescript-config": "0.0.2",
|
|
59
|
-
"@gsa-tts/graymatter-
|
|
60
|
-
"@gsa-tts/graymatter-
|
|
62
|
+
"@gsa-tts/graymatter-vitest-config": "0.0.1",
|
|
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>
|
package/src/components/index.ts
CHANGED
|
@@ -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';
|