@f5-sales-demo/docs-theme 4.7.1 → 4.9.0

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.
@@ -0,0 +1,77 @@
1
+ ---
2
+ import MobileMenuFooter from 'virtual:starlight/components/MobileMenuFooter';
3
+ import SidebarPersister from '@astrojs/starlight/components/SidebarPersister.astro';
4
+ import SidebarSublist from '@astrojs/starlight/components/SidebarSublist.astro';
5
+ import SidebarRestorePoint from '@astrojs/starlight/components/SidebarRestorePoint.astro';
6
+
7
+ const { sidebar } = Astro.locals.starlightRoute;
8
+ ---
9
+
10
+ <SidebarPersister>
11
+ <ul class="canada-sidebar">
12
+ {sidebar.map((entry) => {
13
+ if (entry.type === 'link') {
14
+ return <li><a class="home-link" href={entry.href} aria-current={entry.isCurrent ? 'page' : undefined}>{entry.label}</a></li>;
15
+ }
16
+
17
+ const [overview, ...children] = entry.entries;
18
+ if (!overview || overview.type !== 'link') {
19
+ return <li><SidebarSublist sublist={[entry]} /></li>;
20
+ }
21
+ const active = overview.isCurrent || children.some((child) => child.type === 'link' && child.isCurrent);
22
+
23
+ return <li class="stage">
24
+ <a class="stage-link" href={overview.href} aria-current={overview.isCurrent ? 'page' : undefined}>{entry.label}</a>
25
+ <details open={active || !entry.collapsed}>
26
+ <summary aria-label={`Toggle ${entry.label} pages`} title={`Toggle ${entry.label} pages`}><span class="caret" aria-hidden="true"></span></summary>
27
+ <SidebarRestorePoint />
28
+ <SidebarSublist sublist={children} nested />
29
+ </details>
30
+ </li>;
31
+ })}
32
+ </ul>
33
+ </SidebarPersister>
34
+
35
+ <div class="md:sl-hidden"><MobileMenuFooter /></div>
36
+
37
+ <style>
38
+ @layer starlight.core {
39
+ .canada-sidebar { list-style: none; margin: 0; padding: 0; }
40
+ .canada-sidebar > li + li { margin-top: 0.5rem; }
41
+ .stage { position: relative; }
42
+ .home-link, .stage-link {
43
+ display: block;
44
+ border-radius: 0.25rem;
45
+ color: var(--sl-color-gray-2);
46
+ line-height: 1.4;
47
+ padding: 0.3em 2.5rem 0.3em 0.5rem;
48
+ text-decoration: none;
49
+ font-size: var(--sl-text-sm);
50
+ }
51
+ .stage-link { color: var(--sl-color-white); font-weight: 600; }
52
+ .home-link:hover, .home-link:focus, .stage-link:hover, .stage-link:focus { color: var(--sl-color-white); }
53
+ a[aria-current='page'], a[aria-current='page']:hover, a[aria-current='page']:focus {
54
+ color: var(--sl-color-text-invert);
55
+ background-color: var(--sl-color-text-accent);
56
+ font-weight: 600;
57
+ }
58
+ summary {
59
+ position: absolute;
60
+ inset-block-start: 0;
61
+ inset-inline-end: 0;
62
+ display: grid;
63
+ place-items: center;
64
+ width: 2rem;
65
+ height: 2rem;
66
+ cursor: pointer;
67
+ border-radius: 0.25rem;
68
+ list-style: none;
69
+ }
70
+ summary::-webkit-details-marker { display: none; }
71
+ summary:hover, summary:focus-visible { color: var(--sl-color-white); background: var(--sl-color-gray-5); }
72
+ .caret { width: 0.55rem; height: 0.55rem; border-block-start: 2px solid currentColor; border-inline-end: 2px solid currentColor; transform: rotate(45deg); transition: transform 0.2s ease; }
73
+ details[open] .caret { transform: rotate(135deg); }
74
+ :global([dir='rtl']) .caret { transform: rotate(225deg); }
75
+ :global([dir='rtl']) details[open] .caret { transform: rotate(135deg); }
76
+ }
77
+ </style>
package/config.ts CHANGED
@@ -35,7 +35,7 @@ import canonicalLlms from './src/utils/canonical-llms.mjs';
35
35
  import { providerMegaMenu } from './src/utils/canonical-provider.ts';
36
36
  import { menuLocalizationPolicy } from './src/utils/menu-localization.ts';
37
37
  import { stagedProviderSections } from './src/utils/provider-sections.ts';
38
- import { repositoryPublicationProfile } from './src/utils/publication-profiles.ts';
38
+ import { publicationSidebar, repositoryPublicationProfile } from './src/utils/publication-profiles.ts';
39
39
  import { resolveMegaMenuIcon } from './src/utils/resolve-icon.ts';
40
40
  import providerSharedAssets from './src/utils/share-provider-assets.mjs';
41
41
  import { buildSubcategorySidebar } from './src/utils/subcategory-sidebar.ts';
@@ -782,7 +782,7 @@ export function createF5xcDocsConfig(options: F5xcDocsConfigOptions = {}) {
782
782
  starlightMegaMenu({ items: megaMenuItems, mobileLabels }),
783
783
  starlightVideosPlugin(),
784
784
  starlightImageZoom(),
785
- f5xcDocsTheme(),
785
+ f5xcDocsTheme(githubRepository),
786
786
  starlightScrollToTop({
787
787
  showTooltip: true,
788
788
  tooltipText: {
@@ -843,7 +843,9 @@ export function createF5xcDocsConfig(options: F5xcDocsConfigOptions = {}) {
843
843
  ];
844
844
 
845
845
  const contentDir = process.env.CONTENT_DIR || 'src/content/docs';
846
- const subcategorySidebar = canonicalProvider ? [] : buildSubcategorySidebar(contentDir);
846
+ const subcategorySidebar = canonicalProvider
847
+ ? []
848
+ : publicationSidebar(githubRepository, buildSubcategorySidebar(contentDir));
847
849
 
848
850
  // Auto-detect i18n: enable locales only when content has an en/ subdirectory.
849
851
  // Repos that haven't migrated to docs/en/ won't get a broken language selector.
package/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { StarlightPlugin } from '@astrojs/starlight/types';
2
2
  import { translations } from './src/i18n/translations.ts';
3
3
 
4
- export default function f5xcDocsTheme(): StarlightPlugin {
4
+ export default function f5xcDocsTheme(repository = process.env.GITHUB_REPOSITORY || ''): StarlightPlugin {
5
5
  return {
6
6
  name: '@f5-sales-demo/docs-theme',
7
7
  hooks: {
@@ -29,7 +29,11 @@ export default function f5xcDocsTheme(): StarlightPlugin {
29
29
  ? {
30
30
  Sidebar: '@f5-sales-demo/docs-theme/components/ProviderSidebar.astro',
31
31
  }
32
- : {}),
32
+ : repository === 'f5-sales-demo/canada'
33
+ ? {
34
+ Sidebar: '@f5-sales-demo/docs-theme/components/CanadaSidebar.astro',
35
+ }
36
+ : {}),
33
37
  Banner: '@f5-sales-demo/docs-theme/components/Banner.astro',
34
38
  EditLink: '@f5-sales-demo/docs-theme/components/EditLink.astro',
35
39
  Footer: '@f5-sales-demo/docs-theme/components/Footer.astro',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@f5-sales-demo/docs-theme",
3
- "version": "4.7.1",
3
+ "version": "4.9.0",
4
4
  "description": "F5 Distributed Cloud branded Starlight documentation theme",
5
5
  "type": "module",
6
6
  "engines": {
@@ -43,6 +43,7 @@
43
43
  "./src/utils/localize-ecosystem-href.ts": "./src/utils/localize-ecosystem-href.ts",
44
44
  "./canonical-route-middleware": "./canonical-route-middleware.ts",
45
45
  "./components/ProviderSidebar.astro": "./components/ProviderSidebar.astro",
46
+ "./components/CanadaSidebar.astro": "./components/CanadaSidebar.astro",
46
47
  "./components/ProviderHeader.astro": "./components/ProviderHeader.astro",
47
48
  "./components/SharedHeader.astro": "./components/SharedHeader.astro",
48
49
  "./assets/canada-flag.svg": "./assets/canada-flag.svg",
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import f5xcDocsTheme from '../../index';
3
+
4
+ function configuredComponents(repository: string) {
5
+ const updateConfig = vi.fn();
6
+ const setup = f5xcDocsTheme(repository).hooks?.['config:setup'];
7
+ if (!setup) throw new Error('theme setup hook is missing');
8
+ setup({
9
+ config: { components: {} },
10
+ updateConfig,
11
+ addRouteMiddleware: vi.fn(),
12
+ logger: { info: vi.fn() },
13
+ } as never);
14
+ return updateConfig.mock.calls[0][0].components;
15
+ }
16
+
17
+ describe('Canada-only sidebar override', () => {
18
+ it('selects the linked stage sidebar for Canada', () => {
19
+ expect(configuredComponents('f5-sales-demo/canada').Sidebar).toBe(
20
+ '@f5-sales-demo/docs-theme/components/CanadaSidebar.astro',
21
+ );
22
+ });
23
+
24
+ it('leaves other repositories on their existing sidebar', () => {
25
+ expect(configuredComponents('f5-sales-demo/multi-cloud-networking')).not.toHaveProperty('Sidebar');
26
+ });
27
+ });
@@ -1,10 +1,10 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { localizeEcosystemHref } from './localize-ecosystem-href';
3
- import { repositoryPublicationProfile } from './publication-profiles';
3
+ import { publicationSidebar, repositoryPublicationProfile } from './publication-profiles';
4
4
 
5
5
  describe('Canadian publication', () => {
6
6
  it('publishes only English and selects the flag favicon', () => {
7
- expect(repositoryPublicationProfile('f5-sales-demo/canada')).toEqual({
7
+ expect(repositoryPublicationProfile('f5-sales-demo/canada')).toMatchObject({
8
8
  locales: { en: { label: 'English', lang: 'en' } },
9
9
  defaultLocale: 'en',
10
10
  favicon: '/assets/canada-favicon.svg',
@@ -17,3 +17,43 @@ describe('Canadian publication', () => {
17
17
  );
18
18
  });
19
19
  });
20
+
21
+ describe('publication profile sidebar', () => {
22
+ it('orders four linked stages and covers all 14 pages once', () => {
23
+ const sidebar = publicationSidebar('f5-sales-demo/canada', []);
24
+ expect(sidebar?.[0]).toEqual({ slug: 'index' });
25
+ const stages = sidebar?.slice(1) ?? [];
26
+ expect(stages.map((stage) => ('label' in stage ? stage.label : ''))).toEqual([
27
+ 'Design',
28
+ 'Deploy',
29
+ 'Verify',
30
+ 'Operate',
31
+ ]);
32
+ const expected = [
33
+ ['design', 'use-case', 'architecture'],
34
+ ['deploy', 'deployment', 'terraform'],
35
+ ['verify', 'presentation', 'verification'],
36
+ ['operate', 'troubleshooting', 'failover', 'teardown'],
37
+ ];
38
+ const routes = ['index'];
39
+ stages.forEach((stage, index) => {
40
+ expect(stage).toMatchObject({ collapsed: true });
41
+ if (!('items' in stage)) throw new Error('Canada stage must have items');
42
+ const stageRoutes = stage.items.map((item) => {
43
+ if (!('slug' in item)) throw new Error('Canada stage item must be a page');
44
+ return item.slug;
45
+ });
46
+ expect(stageRoutes).toEqual(expected[index]);
47
+ routes.push(...stageRoutes);
48
+ });
49
+ expect(new Set(routes).size).toBe(14);
50
+ expect(routes).toHaveLength(14);
51
+ expect(stages.every((stage) => 'label' in stage && stage.label.split(/\s+/).length <= 3)).toBe(true);
52
+ });
53
+ it('preserves fallback navigation for every repository without a sidebar profile', () => {
54
+ const fallback = [{ label: 'Existing', slug: 'en/existing' }];
55
+ expect(publicationSidebar('f5-sales-demo/multi-cloud-networking', fallback)).toBe(fallback);
56
+ expect(publicationSidebar('f5-sales-demo/docs-theme', undefined)).toBeUndefined();
57
+ expect(publicationSidebar('f5-sales-demo/canada', fallback)).not.toBe(fallback);
58
+ });
59
+ });
@@ -1,15 +1,51 @@
1
+ import type starlight from '@astrojs/starlight';
2
+
3
+ type Sidebar = NonNullable<Parameters<typeof starlight>[0]['sidebar']>;
4
+
5
+ interface PublicationProfile {
6
+ locales: Record<string, { label: string; lang: string }>;
7
+ defaultLocale: string;
8
+ favicon: string;
9
+ sidebar?: Sidebar;
10
+ }
11
+
1
12
  /** Repository publication decisions shared by navigation and site configuration. */
2
- export const publicationProfiles: Record<
3
- string,
4
- { locales: Record<string, { label: string; lang: string }>; defaultLocale: string; favicon: string }
5
- > = {
13
+ export const publicationProfiles: Record<string, PublicationProfile> = {
6
14
  canada: {
7
15
  locales: { en: { label: 'English', lang: 'en' } },
8
16
  defaultLocale: 'en',
9
17
  favicon: '/assets/canada-favicon.svg',
18
+ sidebar: [
19
+ { slug: 'index' },
20
+ {
21
+ label: 'Design',
22
+ collapsed: true,
23
+ items: [{ slug: 'design' }, { slug: 'use-case' }, { slug: 'architecture' }],
24
+ },
25
+ {
26
+ label: 'Deploy',
27
+ collapsed: true,
28
+ items: [{ slug: 'deploy' }, { slug: 'deployment' }, { slug: 'terraform' }],
29
+ },
30
+ {
31
+ label: 'Verify',
32
+ collapsed: true,
33
+ items: [{ slug: 'verify' }, { slug: 'presentation' }, { slug: 'verification' }],
34
+ },
35
+ {
36
+ label: 'Operate',
37
+ collapsed: true,
38
+ items: [{ slug: 'operate' }, { slug: 'troubleshooting' }, { slug: 'failover' }, { slug: 'teardown' }],
39
+ },
40
+ ],
10
41
  },
11
42
  };
12
43
 
13
44
  export function repositoryPublicationProfile(repository: string) {
14
45
  return publicationProfiles[repository.split('/').pop() || ''];
15
46
  }
47
+
48
+ /** Preserve automatic navigation unless this repository explicitly supplies a sidebar. */
49
+ export function publicationSidebar(repository: string, fallback: Sidebar | undefined) {
50
+ return repositoryPublicationProfile(repository)?.sidebar ?? fallback;
51
+ }