@f5-sales-demo/docs-theme 4.4.2 → 4.4.4

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,116 @@
1
+ ---
2
+ import LanguageSelect from 'virtual:starlight/components/LanguageSelect';
3
+ import Search from 'virtual:starlight/components/Search';
4
+ import SiteTitle from 'virtual:starlight/components/SiteTitle';
5
+ import SocialIcons from 'virtual:starlight/components/SocialIcons';
6
+ import ThemeSelect from 'virtual:starlight/components/ThemeSelect';
7
+ import config from 'virtual:starlight/user-config';
8
+ import megaMenuConfig from 'virtual:starlight-mega-menu/config';
9
+ import MegaMenu from '@f5-sales-demo/starlight-mega-menu/components/MegaMenu.tsx';
10
+ import MegaMenuMobile from '@f5-sales-demo/starlight-mega-menu/components/MegaMenuMobile.tsx';
11
+
12
+ /**
13
+ * Render the `Search` component if Pagefind is enabled or the default search component has been overridden.
14
+ */
15
+ const shouldRenderSearch = config.pagefind || config.components.Search !== '@astrojs/starlight/components/Search.astro';
16
+ ---
17
+
18
+ <div class="header sl-mega-menu-header">
19
+ <div class="title-wrapper sl-flex">
20
+ <SiteTitle />
21
+ </div>
22
+ <nav class="smm-desktop-nav">
23
+ <MegaMenu client:only="react" config={megaMenuConfig} />
24
+ </nav>
25
+ <div class="sl-flex print:hidden">
26
+ {shouldRenderSearch && <Search />}
27
+ </div>
28
+ <div class="smm-mobile-toggle">
29
+ <MegaMenuMobile client:only="react" config={megaMenuConfig} />
30
+ </div>
31
+ </div>
32
+
33
+ <style>
34
+ @layer starlight.core {
35
+ .header {
36
+ display: flex;
37
+ gap: var(--sl-nav-gap);
38
+ justify-content: space-between;
39
+ align-items: center;
40
+ height: 100%;
41
+ }
42
+
43
+ .title-wrapper {
44
+ /* Prevent long titles overflowing and covering the search and menu buttons on narrow viewports. */
45
+ overflow: clip;
46
+ /* Avoid clipping focus ring around link inside title wrapper. */
47
+ padding: 0.25rem;
48
+ margin: -0.25rem;
49
+ min-width: 0;
50
+ }
51
+
52
+ .right-group,
53
+ .social-icons {
54
+ gap: 1rem;
55
+ align-items: center;
56
+ }
57
+ .social-icons::after {
58
+ content: '';
59
+ height: 2rem;
60
+ border-inline-end: 1px solid var(--sl-color-gray-5);
61
+ }
62
+
63
+ /* Desktop nav is hidden on small screens */
64
+ .smm-desktop-nav {
65
+ display: none;
66
+ }
67
+
68
+ /* Mobile toggle is visible on small screens */
69
+ .smm-mobile-toggle {
70
+ display: flex;
71
+ align-items: center;
72
+ }
73
+
74
+ @media (min-width: 50rem) {
75
+ :global(:root[data-has-sidebar]) {
76
+ --__sidebar-pad: calc(2 * var(--sl-nav-pad-x));
77
+ }
78
+ :global(:root:not([data-has-toc])) {
79
+ --__toc-width: 0rem;
80
+ }
81
+ .header {
82
+ --__sidebar-width: max(0rem, var(--sl-content-inline-start, 0rem) - var(--sl-nav-pad-x));
83
+ --__main-column-fr: calc(
84
+ (
85
+ 100% + var(--__sidebar-pad, 0rem) - var(--__toc-width, var(--sl-sidebar-width)) -
86
+ (2 * var(--__toc-width, var(--sl-nav-pad-x))) - var(--sl-content-inline-start, 0rem) -
87
+ var(--sl-content-width)
88
+ ) / 2
89
+ );
90
+ display: grid;
91
+ grid-template-columns:
92
+ /* 1 (site title) */
93
+ auto
94
+ /* 2 (mega-menu nav) */
95
+ 1fr
96
+ /* 3 (search box) */
97
+ auto
98
+ /* 4 (right items) */
99
+ auto;
100
+ align-content: center;
101
+ }
102
+
103
+ /* Show desktop nav, hide mobile toggle */
104
+ .smm-desktop-nav {
105
+ display: flex;
106
+ align-items: center;
107
+ justify-content: center;
108
+ min-width: 0;
109
+ }
110
+
111
+ .smm-mobile-toggle {
112
+ display: none;
113
+ }
114
+ }
115
+ }
116
+ </style>
@@ -0,0 +1,6 @@
1
+ ---
2
+ import ProviderSidebarTree from './ProviderSidebarTree.astro';
3
+
4
+ const { sidebar } = Astro.locals.starlightRoute;
5
+ ---
6
+ <nav aria-label="Provider reference"><ProviderSidebarTree entries={sidebar} /></nav>
@@ -0,0 +1,21 @@
1
+ ---
2
+ import type { StarlightRouteData } from '@astrojs/starlight/route-data';
3
+
4
+ interface Props {
5
+ entries: StarlightRouteData['sidebar'];
6
+ }
7
+ const { entries } = Astro.props;
8
+ ---
9
+ <ul>{entries.map(entry => <li>{entry.type === 'link'
10
+ ? <a href={entry.href} aria-current={entry.isCurrent ? 'page' : undefined}>{entry.label}</a>
11
+ : <details open={!entry.collapsed}><summary>{entry.label}</summary><Astro.self entries={entry.entries} /></details>}
12
+ </li>)}</ul>
13
+ <style>
14
+ ul { list-style: none; padding: 0; margin: 0; }
15
+ ul ul { padding-inline-start: .75rem; border-inline-start: 1px solid var(--sl-color-gray-5); }
16
+ li { margin: .3rem 0; overflow-wrap: anywhere; }
17
+ a { display: block; padding: .2rem .4rem; color: var(--sl-color-gray-2); text-decoration: none; font-size: var(--sl-text-sm); }
18
+ a:hover { color: var(--sl-color-white); }
19
+ a[aria-current] { background: var(--sl-color-accent-low); color: var(--sl-color-accent-high); border-inline-start: 2px solid var(--sl-color-accent); }
20
+ summary { cursor: pointer; padding: .25rem; font-weight: 600; font-size: var(--sl-text-sm); }
21
+ </style>
package/config.ts CHANGED
@@ -33,6 +33,7 @@ import { sidebarTranslations } from './src/i18n/translations.ts';
33
33
  import remarkMermaid from './src/plugins/remark-mermaid.mjs';
34
34
  import { providerMegaMenu } from './src/utils/canonical-provider.ts';
35
35
  import { resolveMegaMenuIcon } from './src/utils/resolve-icon.ts';
36
+ import providerSharedAssets from './src/utils/share-provider-assets.mjs';
36
37
  import { buildSubcategorySidebar } from './src/utils/subcategory-sidebar.ts';
37
38
 
38
39
  export type { LocaleConfig } from './src/i18n/locales.ts';
@@ -922,6 +923,7 @@ export function createF5xcDocsConfig(options: F5xcDocsConfigOptions = {}) {
922
923
  ],
923
924
  }),
924
925
  react(),
926
+ ...(canonicalProvider ? [providerSharedAssets()] : []),
925
927
  ...additionalIntegrations,
926
928
  ],
927
929
  });
package/index.ts CHANGED
@@ -24,6 +24,12 @@ export default function f5xcDocsTheme(): StarlightPlugin {
24
24
  ],
25
25
  components: {
26
26
  ...config.components,
27
+ ...(process.env.DOCS_PROFILE === 'canonical-provider'
28
+ ? {
29
+ Header: '@f5-sales-demo/docs-theme/components/ProviderHeader.astro',
30
+ Sidebar: '@f5-sales-demo/docs-theme/components/ProviderSidebar.astro',
31
+ }
32
+ : {}),
27
33
  Banner: '@f5-sales-demo/docs-theme/components/Banner.astro',
28
34
  EditLink: '@f5-sales-demo/docs-theme/components/EditLink.astro',
29
35
  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.4.2",
3
+ "version": "4.4.4",
4
4
  "description": "F5 Distributed Cloud branded Starlight documentation theme",
5
5
  "type": "module",
6
6
  "engines": {
@@ -41,7 +41,9 @@
41
41
  "./src/i18n/translator.ts": "./src/i18n/translator.ts",
42
42
  "./src/i18n/prompt.ts": "./src/i18n/prompt.ts",
43
43
  "./src/utils/localize-ecosystem-href.ts": "./src/utils/localize-ecosystem-href.ts",
44
- "./canonical-route-middleware": "./canonical-route-middleware.ts"
44
+ "./canonical-route-middleware": "./canonical-route-middleware.ts",
45
+ "./components/ProviderSidebar.astro": "./components/ProviderSidebar.astro",
46
+ "./components/ProviderHeader.astro": "./components/ProviderHeader.astro"
45
47
  },
46
48
  "files": [
47
49
  "index.ts",
@@ -0,0 +1,61 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const hash = (value) => createHash('sha256').update(value).digest('hex').slice(0, 24);
7
+ // Input is Astro-generated HTML. Keep each asset's original execution position
8
+ // and SVG attributes; only identical generated bytes share a file.
9
+ export async function shareProviderAssets(root, base = '/') {
10
+ const files = (await readdir(root, { recursive: true })).filter((file) => file.endsWith('.html'));
11
+ const assetRoot = join(root, '_shared');
12
+ const assets = new Map();
13
+ const prefix = `${base.replace(/\/$/, '')}/_shared/`;
14
+ let saved = 0;
15
+ await mkdir(assetRoot, { recursive: true });
16
+ for (const file of files) {
17
+ const path = join(root, file);
18
+ const before = await readFile(path, 'utf8');
19
+ let html = before.replace(/<script\b([^>]*)>([\s\S]*?)<\/script>/g, (original, attrs, body) => {
20
+ if (/\bsrc\s*=/.test(attrs) || !body.trim() || /type=["'](?:application\/|text\/template)/.test(attrs))
21
+ return original;
22
+ const name = `${hash(body)}.js`;
23
+ assets.set(name, body);
24
+ return `<script${attrs} src="${prefix}${name}"></script>`;
25
+ });
26
+ const islands = [];
27
+ html = html.replace(/<astro-island\b[\s\S]*?<\/astro-island>/g, (island) => {
28
+ islands.push(island);
29
+ return `<!--provider-island-${islands.length - 1}-->`;
30
+ });
31
+ html = html.replace(/<svg\b([^>]*)>([\s\S]*?)<\/svg>/g, (original, attrs, body) => {
32
+ // Sprite definitions and interactive SVG content must remain inline.
33
+ if (/<(?:symbol|script|foreignObject)\b/.test(body) || /\bid=/.test(attrs) || /\bon\w+=/.test(attrs))
34
+ return original;
35
+ const name = `${hash(body)}.svg`;
36
+ assets.set(name, `<svg xmlns="http://www.w3.org/2000/svg"><symbol id="icon">${body}</symbol></svg>`);
37
+ return `<svg${attrs}><use href="${prefix}${name}#icon"></use></svg>`;
38
+ });
39
+ html = html.replace(/<!--provider-island-(\d+)-->/g, (_, index) => islands[Number(index)]);
40
+ saved += Buffer.byteLength(before) - Buffer.byteLength(html);
41
+ await writeFile(path, html);
42
+ }
43
+ let assetBytes = 0;
44
+ for (const [name, body] of assets) {
45
+ await writeFile(join(assetRoot, name), body);
46
+ assetBytes += Buffer.byteLength(body);
47
+ }
48
+ return { pages: files.length, assets: assets.size, savedBytes: saved - assetBytes };
49
+ }
50
+
51
+ export default function providerSharedAssets() {
52
+ return {
53
+ name: 'f5-canonical-shared-assets',
54
+ hooks: {
55
+ 'astro:build:done': async ({ dir, logger }) => {
56
+ const result = await shareProviderAssets(fileURLToPath(dir), process.env.DOCS_BASE || '/');
57
+ logger.info(`Shared canonical assets: ${JSON.stringify(result)}`);
58
+ },
59
+ },
60
+ };
61
+ }
@@ -0,0 +1,25 @@
1
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { expect, it } from 'vitest';
5
+ import { shareProviderAssets } from './share-provider-assets.mjs';
6
+
7
+ it('shares identical generated assets, preserving script order and SVG dimensions', async () => {
8
+ const root = await mkdtemp(join(tmpdir(), 'shared-provider-'));
9
+ try {
10
+ const text =
11
+ '<h1>Reference</h1><script>window.example=1;</script><svg width="16" viewBox="0 0 24 24"><path d="M1 2"/></svg><script src="keep.js"></script>';
12
+ await writeFile(join(root, 'one.html'), text);
13
+ await writeFile(join(root, 'two.html'), text);
14
+ const result = await shareProviderAssets(root, '/provider/version/');
15
+ expect(result.assets).toBe(2);
16
+ const html = await readFile(join(root, 'one.html'), 'utf8');
17
+ expect(html).toContain('<h1>Reference</h1>');
18
+ expect(html).toMatch(/<script src="\/provider\/version\/_shared\/[a-f0-9]+\.js"><\/script>/);
19
+ expect(html).toContain('<svg width="16" viewBox="0 0 24 24"><use');
20
+ expect(html).toContain('<script src="keep.js"></script>');
21
+ expect(await readFile(join(root, 'two.html'), 'utf8')).toBe(html);
22
+ } finally {
23
+ await rm(root, { recursive: true, force: true });
24
+ }
25
+ });