@gsa-tts/graymatter-ui 0.4.16 → 0.4.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gsa-tts/graymatter-ui",
3
- "version": "0.4.16",
3
+ "version": "0.4.19",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -5,6 +5,13 @@
5
5
  import ApiDocSubNavList from './ApiDocSubNavList.svelte';
6
6
 
7
7
  const { data = null } = $props();
8
+ // TODO: remove legacy /api/documentation path once all consuming apps migrate
9
+ const API_DOC_PATHS = ['/api-documentation', '/api/documentation'];
10
+
11
+ function isApiDocumentationPath(path: string | null | undefined) {
12
+ if (!path) return false;
13
+ return API_DOC_PATHS.some(apiPath => path.includes(apiPath));
14
+ }
8
15
 
9
16
  const sections = $derived(() =>
10
17
  !Array.isArray(data) &&
@@ -180,6 +187,17 @@
180
187
  let currentSelectedId = $state<string | null>(null);
181
188
  let hydrated = $state(false);
182
189
  let pageLoadComplete = $state(false);
190
+ let scrollTimeoutId: ReturnType<typeof setTimeout> | null = null;
191
+
192
+ function queueScrollToAnchor(hash: string, delay: number) {
193
+ if (scrollTimeoutId) {
194
+ clearTimeout(scrollTimeoutId);
195
+ }
196
+ scrollTimeoutId = setTimeout(() => {
197
+ scrollTimeoutId = null;
198
+ scrollToAnchorWithOffset(hash);
199
+ }, delay);
200
+ }
183
201
 
184
202
  const effectiveSelectedId = $derived(() => {
185
203
  if (!hydrated || !data) return null;
@@ -232,8 +250,7 @@
232
250
  // Only do this if we're on the API documentation page (where this component exists)
233
251
  if (data && effectiveSelectedId() && typeof window !== 'undefined') {
234
252
  const currentPath = window.location.pathname;
235
- const isApiDocumentationPage =
236
- currentPath.includes('/api/documentation');
253
+ const isApiDocumentationPage = isApiDocumentationPath(currentPath);
237
254
 
238
255
  if (isApiDocumentationPage) {
239
256
  const restoredItem = findItemById(data, effectiveSelectedId()!);
@@ -259,13 +276,13 @@
259
276
  // Check if we're navigating TO the documentation page (vs refreshing while on it)
260
277
  if (typeof window !== 'undefined') {
261
278
  const currentPath = window.location.pathname;
262
- const isDocumentationPage = currentPath.includes('/api/documentation');
279
+ const isDocumentationPage = isApiDocumentationPath(currentPath);
263
280
 
264
281
  // If we're on the documentation page, check if we came from a different page
265
282
  if (isDocumentationPage) {
266
283
  const referrer = document.referrer;
267
284
  const isFromDifferentPage =
268
- referrer && !referrer.includes('/api/documentation');
285
+ referrer && !isApiDocumentationPath(referrer);
269
286
 
270
287
  // If we came from a different page, clear sessionStorage to reset to initial values
271
288
  if (isFromDifferentPage) {
@@ -290,13 +307,20 @@
290
307
  if (targetItem && targetItem.href && targetItem.href.includes('#')) {
291
308
  const hash = targetItem.href.split('#')[1];
292
309
  if (hash) {
293
- setTimeout(() => scrollToAnchorWithOffset(hash), 100);
310
+ queueScrollToAnchor(hash, 100);
294
311
  }
295
312
  }
296
313
 
297
314
  // Mark page load as complete
298
315
  pageLoadComplete = true;
299
316
  subNavReady.set(true);
317
+
318
+ return () => {
319
+ if (scrollTimeoutId) {
320
+ clearTimeout(scrollTimeoutId);
321
+ scrollTimeoutId = null;
322
+ }
323
+ };
300
324
  });
301
325
 
302
326
  function handleItemClick(event: Event, item: SubNavItem) {
@@ -326,9 +350,7 @@
326
350
  window.location.hash = hash;
327
351
  if (window.innerWidth > 640) {
328
352
  // Desktop: use delay for consistent timing
329
- setTimeout(() => {
330
- scrollToAnchorWithOffset(hash);
331
- }, 350);
353
+ queueScrollToAnchor(hash, 350);
332
354
  } else {
333
355
  // Mobile: immediate scroll without delay
334
356
  window.dispatchEvent(
@@ -376,6 +398,7 @@
376
398
 
377
399
  // Utility: Scroll to anchor with header offset
378
400
  function scrollToAnchorWithOffset(anchorId: string) {
401
+ if (typeof document === 'undefined') return;
379
402
  const mainContent = document.getElementById('main-content');
380
403
  if (mainContent) {
381
404
  const anchor = mainContent.querySelector(
@@ -1,9 +1,14 @@
1
1
  <script lang="ts">
2
2
  /**
3
- * Controls which nav item is selected on initial server-side render (SSR) or first paint.
4
- * - For the Discover page, set ssrSelectedItem="discover".
5
- * - For other pages, set ssrSelectedItem to the corresponding nav key (e.g., "api", "chat", "console").
6
- * - If not provided, the navigation will default to expanded/open on SSR.
3
+ * Initial selection for SSR/first paint.
4
+ * - Discover page: set ssrSelectedItem="discover".
5
+ * - Other pages: set ssrSelectedItem to the nav key (e.g., "api", "chat", "console").
6
+ * - Omit to default to expanded/open on SSR.
7
+ *
8
+ * controlledSelectedItem overrides the nav store after hydration.
9
+ * - Use for web component integrations where bundle/app stores can diverge.
10
+ * - In Svelte/SvelteKit apps, prefer NavigationInitializer and omit
11
+ * controlledSelectedItem to keep store-driven behavior.
7
12
  */
8
13
  import { onMount } from 'svelte';
9
14
  import {
@@ -23,6 +28,7 @@
23
28
 
24
29
  const {
25
30
  ssrSelectedItem = undefined,
31
+ controlledSelectedItem = undefined,
26
32
  onNavToggle = undefined,
27
33
  onNavItemClick = undefined,
28
34
  showAppIcons = true,
@@ -79,8 +85,16 @@
79
85
  return hydrated ? hydratedValue : ssrValue;
80
86
  }
81
87
 
88
+ const selectedForUi = $derived(() => controlledSelectedItem ?? $selectedItem);
89
+ const ssrSelectedForUi = $derived(
90
+ () => controlledSelectedItem ?? ssrSelectedItem
91
+ );
92
+ const ssrSelected = $derived(() => ssrSelectedForUi());
93
+ const ssrIsDiscover = $derived(() => ssrSelected() === 'discover');
94
+ const isMenuDisabledForUi = $derived(() => selectedForUi() === 'discover');
95
+
82
96
  function isNavSelected(key: NavigationItem) {
83
- return ssrOrHydrated($selectedItem === key, ssrSelectedItem === key);
97
+ return ssrOrHydrated(selectedForUi() === key, ssrSelectedForUi() === key);
84
98
  }
85
99
 
86
100
  function isNavHovered(key: string) {
@@ -88,22 +102,16 @@
88
102
  }
89
103
 
90
104
  const sideNavExpanded = $derived(() =>
91
- ssrOrHydrated(
92
- $isExpanded,
93
- ssrSelectedItem ? ssrSelectedItem !== 'discover' : true
94
- )
105
+ ssrOrHydrated($isExpanded, ssrSelected() ? !ssrIsDiscover() : true)
95
106
  );
96
107
  const menuBtnDisabled = $derived(() =>
97
108
  ssrOrHydrated(
98
- $isMenuButtonDisabled,
99
- ssrSelectedItem ? ssrSelectedItem === 'discover' : false
109
+ isMenuDisabledForUi(),
110
+ ssrSelected() ? ssrIsDiscover() : false
100
111
  )
101
112
  );
102
113
  const menuBtnOpen = $derived(() =>
103
- ssrOrHydrated(
104
- $isExpanded,
105
- ssrSelectedItem ? ssrSelectedItem !== 'discover' : true
106
- )
114
+ ssrOrHydrated($isExpanded, ssrSelected() ? !ssrIsDiscover() : true)
107
115
  );
108
116
  const menuBtnHovered = $derived(
109
117
  () => canHover && menuButtonHovered && !menuBtnDisabled()
@@ -138,7 +146,7 @@
138
146
  navigationStore.setExpanded(false);
139
147
  } else {
140
148
  // Switching to desktop: open navigation if on non-discover page
141
- const currentItem = $selectedItem;
149
+ const currentItem = selectedForUi();
142
150
 
143
151
  if (currentItem && currentItem !== 'discover') {
144
152
  navigationStore.setExpanded(true);
@@ -220,7 +228,7 @@
220
228
  }
221
229
 
222
230
  function toggleNav() {
223
- if ($isMenuButtonDisabled) return;
231
+ if (menuBtnDisabled()) return;
224
232
  navigationStore.toggleExpanded();
225
233
  onNavToggle?.({ expanded: $isExpanded });
226
234
  }
@@ -231,7 +239,9 @@
231
239
  sessionStorage.setItem('navigatingToMainNav', 'true');
232
240
  }
233
241
 
234
- navigationStore.setSelectedItem(item);
242
+ if (controlledSelectedItem === undefined) {
243
+ navigationStore.setSelectedItem(item);
244
+ }
235
245
 
236
246
  // Clear header state for main navigation items (including discover)
237
247
  if (item !== 'discover') {
@@ -10,6 +10,7 @@
10
10
  // Props for the web component
11
11
  let {
12
12
  ssrSelectedItem,
13
+ controlledSelectedItem,
13
14
  onNavToggle,
14
15
  onNavItemClick,
15
16
  showAppIcons = true,
@@ -25,6 +26,7 @@
25
26
 
26
27
  <DesktopSideNav
27
28
  {ssrSelectedItem}
29
+ {controlledSelectedItem}
28
30
  {onNavToggle}
29
31
  {onNavItemClick}
30
32
  {showAppIcons}
@@ -14,6 +14,7 @@
14
14
  chatUrl,
15
15
  consoleUrl,
16
16
  discoverUrl,
17
+ controlledSelectedItem = undefined,
17
18
  }: {
18
19
  onNavItemClick?: (data: {
19
20
  item: NavigationItem;
@@ -25,8 +26,11 @@
25
26
  chatUrl?: string;
26
27
  consoleUrl?: string;
27
28
  discoverUrl?: string;
29
+ controlledSelectedItem?: NavigationItem;
28
30
  } = $props();
29
31
 
32
+ const selectedForUi = $derived(() => controlledSelectedItem ?? $selectedItem);
33
+
30
34
  const allNavItems = [
31
35
  {
32
36
  id: 'discover',
@@ -55,7 +59,9 @@
55
59
  sessionStorage.setItem('navigatingToMainNav', 'true');
56
60
  }
57
61
 
58
- navigationStore.setSelectedItem(item);
62
+ if (controlledSelectedItem === undefined) {
63
+ navigationStore.setSelectedItem(item);
64
+ }
59
65
 
60
66
  // Clear header state for main navigation items (including discover)
61
67
  if (item !== 'discover') {
@@ -130,12 +136,12 @@
130
136
  <a
131
137
  href={navItem.href}
132
138
  class={`nav-item ${navItem.id}-item`}
133
- class:active={$selectedItem === navItem.id}
139
+ class:active={selectedForUi() === navItem.id}
134
140
  onclick={() => handleNavItemClick(navItem.id)}
135
141
  part={`${navItem.id}-item`}
136
142
  >
137
143
  <div class="icon">
138
- <navItem.icon isSelected={$selectedItem === navItem.id} />
144
+ <navItem.icon isSelected={selectedForUi() === navItem.id} />
139
145
  </div>
140
146
  <span class="label">{navItem.label}</span>
141
147
  </a>
@@ -16,6 +16,7 @@
16
16
  chatUrl,
17
17
  consoleUrl,
18
18
  discoverUrl,
19
+ controlledSelectedItem,
19
20
  } = $props();
20
21
  </script>
21
22
 
@@ -27,4 +28,5 @@
27
28
  {chatUrl}
28
29
  {consoleUrl}
29
30
  {discoverUrl}
31
+ {controlledSelectedItem}
30
32
  />
@@ -82,7 +82,8 @@ function headingsToNavItems(
82
82
 
83
83
  // Generate navigation data dynamically from API documentation metadata
84
84
  export function generateApiDocumentationNavData(
85
- apiDocs: ApiDocMetadata[]
85
+ apiDocs: ApiDocMetadata[],
86
+ basePath = '/api-documentation'
86
87
  ): SubNavData {
87
88
  // Sort by sortOrder
88
89
  const sortedDocs = [...apiDocs].sort((a, b) => a.sortOrder - b.sortOrder);
@@ -97,7 +98,7 @@ export function generateApiDocumentationNavData(
97
98
  return {
98
99
  id: doc.slug,
99
100
  title: doc.title,
100
- href: `/api/documentation#${doc.slug}`,
101
+ href: `${basePath}#${doc.slug}`,
101
102
  children,
102
103
  };
103
104
  });