@f5-sales-demo/docs-theme 3.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +36 -0
  3. package/assets/f5-distributed-cloud.svg +1 -0
  4. package/assets/f5-logo.svg +1 -0
  5. package/assets/github-avatar.png +0 -0
  6. package/astro.config.mjs +3 -0
  7. package/bin/translate.mjs +113 -0
  8. package/components/Banner.astro +51 -0
  9. package/components/EditLink.astro +3 -0
  10. package/components/Footer.astro +47 -0
  11. package/components/Icon.astro +100 -0
  12. package/components/LinkCard.astro +110 -0
  13. package/components/MarkdownContent.astro +7 -0
  14. package/components/Screenshot.astro +31 -0
  15. package/components/SiteTitle.astro +56 -0
  16. package/config.ts +816 -0
  17. package/fonts/font-face.css +79 -0
  18. package/fonts/neusaNextProWide-300.woff2 +0 -0
  19. package/fonts/neusaNextProWide-400.woff2 +0 -0
  20. package/fonts/neusaNextProWide-400i.woff2 +0 -0
  21. package/fonts/neusaNextProWide-500.woff2 +0 -0
  22. package/fonts/neusaNextProWide-700.woff2 +0 -0
  23. package/fonts/neusaNextProWide-700i.woff2 +0 -0
  24. package/fonts/proximaNova-400.woff2 +0 -0
  25. package/fonts/proximaNova-500.woff2 +0 -0
  26. package/fonts/proximaNova-600.woff2 +0 -0
  27. package/fonts/proximaNova-700.woff2 +0 -0
  28. package/index.ts +36 -0
  29. package/package.json +135 -0
  30. package/route-middleware.ts +73 -0
  31. package/src/content.config.ts +10 -0
  32. package/src/i18n/glossary.ts +18 -0
  33. package/src/i18n/locales.test.ts +56 -0
  34. package/src/i18n/locales.ts +7 -0
  35. package/src/i18n/mega-menu-translations.ts +1363 -0
  36. package/src/i18n/prompt.ts +30 -0
  37. package/src/i18n/translations.ts +733 -0
  38. package/src/i18n/translator.ts +120 -0
  39. package/src/plugins/remark-mermaid.mjs +24 -0
  40. package/src/types/icon.ts +6 -0
  41. package/src/utils/localize-ecosystem-href.test.ts +65 -0
  42. package/src/utils/localize-ecosystem-href.ts +34 -0
  43. package/src/utils/resolve-icon.ts +70 -0
  44. package/src/utils/subcategory-sidebar.test.ts +25 -0
  45. package/src/utils/subcategory-sidebar.ts +316 -0
  46. package/styles/custom.css +807 -0
@@ -0,0 +1,79 @@
1
+ @font-face {
2
+ font-family: neusaNextProWide;
3
+ font-weight: 300;
4
+ font-style: normal;
5
+ font-display: swap;
6
+ src: url("./neusaNextProWide-300.woff2") format("woff2");
7
+ }
8
+
9
+ @font-face {
10
+ font-family: neusaNextProWide;
11
+ font-weight: 400;
12
+ font-style: normal;
13
+ font-display: swap;
14
+ src: url("./neusaNextProWide-400.woff2") format("woff2");
15
+ }
16
+
17
+ @font-face {
18
+ font-family: neusaNextProWide;
19
+ font-weight: 500;
20
+ font-style: normal;
21
+ font-display: swap;
22
+ src: url("./neusaNextProWide-500.woff2") format("woff2");
23
+ }
24
+
25
+ @font-face {
26
+ font-family: neusaNextProWide;
27
+ font-weight: 700;
28
+ font-style: normal;
29
+ font-display: swap;
30
+ src: url("./neusaNextProWide-700.woff2") format("woff2");
31
+ }
32
+
33
+ @font-face {
34
+ font-family: neusaNextProWide;
35
+ font-weight: 400;
36
+ font-style: italic;
37
+ font-display: swap;
38
+ src: url("./neusaNextProWide-400i.woff2") format("woff2");
39
+ }
40
+
41
+ @font-face {
42
+ font-family: neusaNextProWide;
43
+ font-weight: 700;
44
+ font-style: italic;
45
+ font-display: swap;
46
+ src: url("./neusaNextProWide-700i.woff2") format("woff2");
47
+ }
48
+
49
+ @font-face {
50
+ font-family: proximaNova;
51
+ font-weight: 400;
52
+ font-style: normal;
53
+ font-display: swap;
54
+ src: url("./proximaNova-400.woff2") format("woff2");
55
+ }
56
+
57
+ @font-face {
58
+ font-family: proximaNova;
59
+ font-weight: 500;
60
+ font-style: normal;
61
+ font-display: swap;
62
+ src: url("./proximaNova-500.woff2") format("woff2");
63
+ }
64
+
65
+ @font-face {
66
+ font-family: proximaNova;
67
+ font-weight: 600;
68
+ font-style: normal;
69
+ font-display: swap;
70
+ src: url("./proximaNova-600.woff2") format("woff2");
71
+ }
72
+
73
+ @font-face {
74
+ font-family: proximaNova;
75
+ font-weight: 700;
76
+ font-style: normal;
77
+ font-display: swap;
78
+ src: url("./proximaNova-700.woff2") format("woff2");
79
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ import type { StarlightPlugin } from '@astrojs/starlight/types';
2
+ import { translations } from './src/i18n/translations.ts';
3
+
4
+ export default function f5xcDocsTheme(): StarlightPlugin {
5
+ return {
6
+ name: '@f5xc-salesdemos/docs-theme',
7
+ hooks: {
8
+ 'i18n:setup'({ injectTranslations }) {
9
+ injectTranslations(translations);
10
+ },
11
+ 'config:setup'({ config, updateConfig, addRouteMiddleware, logger }) {
12
+ addRouteMiddleware({
13
+ entrypoint: '@f5xc-salesdemos/docs-theme/route-middleware',
14
+ order: 'pre',
15
+ });
16
+ updateConfig({
17
+ customCss: [
18
+ ...(config.customCss ?? []),
19
+ '@f5xc-salesdemos/docs-theme/fonts/font-face.css',
20
+ '@f5xc-salesdemos/docs-theme/styles/custom.css',
21
+ ],
22
+ components: {
23
+ ...config.components,
24
+ Banner: '@f5xc-salesdemos/docs-theme/components/Banner.astro',
25
+ EditLink: '@f5xc-salesdemos/docs-theme/components/EditLink.astro',
26
+ Footer: '@f5xc-salesdemos/docs-theme/components/Footer.astro',
27
+ SiteTitle: '@f5xc-salesdemos/docs-theme/components/SiteTitle.astro',
28
+ MarkdownContent:
29
+ process.env.DOCS_MARKDOWN_CONTENT || '@f5xc-salesdemos/docs-theme/components/MarkdownContent.astro',
30
+ },
31
+ });
32
+ logger.info('F5 XC docs theme loaded');
33
+ },
34
+ },
35
+ };
36
+ }
package/package.json ADDED
@@ -0,0 +1,135 @@
1
+ {
2
+ "name": "@f5-sales-demo/docs-theme",
3
+ "_scope_migrated": true,
4
+ "version": "3.9.0",
5
+ "description": "F5 Distributed Cloud branded Starlight documentation theme",
6
+ "type": "module",
7
+ "bin": {
8
+ "docs-translate": "./bin/translate.mjs"
9
+ },
10
+ "license": "MIT",
11
+ "homepage": "https://f5-sales-demo.github.io/docs-theme",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/f5-sales-demo/docs-theme"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/f5-sales-demo/docs-theme/issues"
18
+ },
19
+ "exports": {
20
+ ".": "./index.ts",
21
+ "./fonts/font-face.css": "./fonts/font-face.css",
22
+ "./styles/custom.css": "./styles/custom.css",
23
+ "./components/Banner.astro": "./components/Banner.astro",
24
+ "./components/EditLink.astro": "./components/EditLink.astro",
25
+ "./components/Footer.astro": "./components/Footer.astro",
26
+ "./components/SiteTitle.astro": "./components/SiteTitle.astro",
27
+ "./components/MarkdownContent.astro": "./components/MarkdownContent.astro",
28
+ "./assets/f5-logo.svg": "./assets/f5-logo.svg",
29
+ "./assets/github-avatar.png": "./assets/github-avatar.png",
30
+ "./assets/f5-distributed-cloud.svg": "./assets/f5-distributed-cloud.svg",
31
+ "./config": "./config.ts",
32
+ "./route-middleware": "./route-middleware.ts",
33
+ "./src/plugins/remark-mermaid.mjs": "./src/plugins/remark-mermaid.mjs",
34
+ "./components/Icon.astro": "./components/Icon.astro",
35
+ "./components/LinkCard.astro": "./components/LinkCard.astro",
36
+ "./components/Screenshot.astro": "./components/Screenshot.astro",
37
+ "./src/utils/resolve-icon.ts": "./src/utils/resolve-icon.ts",
38
+ "./src/i18n/locales.ts": "./src/i18n/locales.ts",
39
+ "./src/i18n/translator.ts": "./src/i18n/translator.ts",
40
+ "./src/i18n/prompt.ts": "./src/i18n/prompt.ts",
41
+ "./src/utils/localize-ecosystem-href.ts": "./src/utils/localize-ecosystem-href.ts"
42
+ },
43
+ "files": [
44
+ "index.ts",
45
+ "config.ts",
46
+ "route-middleware.ts",
47
+ "astro.config.mjs",
48
+ "bin/",
49
+ "fonts/",
50
+ "styles/",
51
+ "assets/",
52
+ "components/",
53
+ "src/"
54
+ ],
55
+ "keywords": [
56
+ "starlight",
57
+ "theme",
58
+ "astro",
59
+ "withastro",
60
+ "starlight-theme",
61
+ "f5",
62
+ "f5xc",
63
+ "documentation",
64
+ "docs-theme"
65
+ ],
66
+ "publishConfig": {
67
+ "access": "public"
68
+ },
69
+ "devDependencies": {
70
+ "@playwright/test": "^1.50.0",
71
+ "vitest": "^4.1.9"
72
+ },
73
+ "scripts": {
74
+ "test": "vitest run",
75
+ "test:watch": "vitest",
76
+ "test:visual": "playwright test",
77
+ "test:visual:update": "playwright test --update-snapshots"
78
+ },
79
+ "peerDependencies": {
80
+ "@astrojs/markdown-remark": ">=7.0.0",
81
+ "@astrojs/starlight": ">=0.38.0",
82
+ "@f5-sales-demo/icons-f5-brand": "*",
83
+ "@f5-sales-demo/icons-f5xc": "*",
84
+ "@f5-sales-demo/icons-hashicorp-flight": "*",
85
+ "@iconify-json/carbon": "*",
86
+ "@iconify-json/lucide": "*",
87
+ "@iconify-json/mdi": "*",
88
+ "@iconify-json/ph": "*",
89
+ "@iconify-json/tabler": "*",
90
+ "astro": ">=6.0.0"
91
+ },
92
+ "peerDependenciesMeta": {
93
+ "@iconify-json/lucide": {
94
+ "optional": true
95
+ },
96
+ "@iconify-json/carbon": {
97
+ "optional": true
98
+ },
99
+ "@iconify-json/mdi": {
100
+ "optional": true
101
+ },
102
+ "@iconify-json/ph": {
103
+ "optional": true
104
+ },
105
+ "@iconify-json/tabler": {
106
+ "optional": true
107
+ },
108
+ "@f5-sales-demo/icons-f5-brand": {
109
+ "optional": true
110
+ },
111
+ "@f5-sales-demo/icons-f5xc": {
112
+ "optional": true
113
+ },
114
+ "@f5-sales-demo/icons-hashicorp-flight": {
115
+ "optional": true
116
+ }
117
+ },
118
+ "dependencies": {
119
+ "@anthropic-ai/sdk": "^0.106.0",
120
+ "@astrojs/react": "^6.0.0",
121
+ "@f5-sales-demo/i18n-core": "^1.0.0",
122
+ "@f5-sales-demo/starlight-llms-txt": "1.3.1",
123
+ "@f5-sales-demo/starlight-mega-menu": "^2.0.0",
124
+ "gray-matter": "^4.0.3",
125
+ "remark-code-import": "^1.2.0",
126
+ "starlight-heading-badges": "^0.7.0",
127
+ "starlight-image-zoom": "^0.15.0",
128
+ "starlight-openapi": "^0.25.3",
129
+ "starlight-page-actions": "^0.6.0",
130
+ "starlight-plugin-icons": "^1.1.3",
131
+ "starlight-scroll-to-top": "^1.0.1",
132
+ "starlight-videos": "^0.4.0",
133
+ "unist-util-visit": "^5.0.0"
134
+ }
135
+ }
@@ -0,0 +1,73 @@
1
+ import type { StarlightRouteData } from '@astrojs/starlight/route-data';
2
+ import { defineRouteMiddleware } from '@astrojs/starlight/route-data';
3
+ import { itemLabels, menuLabels } from './src/i18n/mega-menu-translations.ts';
4
+
5
+ const titleTranslations: Record<string, Record<string, string>> = { ...menuLabels, ...itemLabels };
6
+
7
+ type SidebarEntry = StarlightRouteData['sidebar'][number];
8
+
9
+ function isIndexPage(filePath: string | undefined): boolean {
10
+ if (!filePath) return false;
11
+ return /(?:^|[\\/])index\.mdx?$/.test(filePath);
12
+ }
13
+
14
+ function hrefToSlug(href: string): string {
15
+ const base = import.meta.env.BASE_URL.replace(/\/$/, '');
16
+ let s = href;
17
+ if (base && s.startsWith(base)) s = s.slice(base.length);
18
+ s = s.replace(/^\/+|\/+$/g, '');
19
+ if (s.endsWith('.html')) s = s.slice(0, -5);
20
+ if (s === 'index') s = '';
21
+ return s;
22
+ }
23
+
24
+ // Discover all index pages at build time via Vite glob — no astro:content needed
25
+ const indexPaths = Object.keys(
26
+ import.meta.glob(['/src/content/docs/**/index.md', '/src/content/docs/**/index.mdx'], { eager: false }),
27
+ );
28
+
29
+ const indexSlugs = new Set<string>(
30
+ indexPaths.map((p) => {
31
+ const relative = p.replace(/^\/src\/content\/docs\//, '').replace(/\/?index\.mdx?$/, '');
32
+ // Strip locale prefix (e.g., "en/guides" → "guides")
33
+ return relative.replace(/^[a-z]{2}(-[a-z]{2,4})?\/?/, '');
34
+ }),
35
+ );
36
+
37
+ function filterIndexPages(entries: SidebarEntry[], slugs: Set<string>): SidebarEntry[] {
38
+ return entries.reduce<SidebarEntry[]>((acc, entry) => {
39
+ if (entry.type === 'link') {
40
+ const slug = hrefToSlug(entry.href);
41
+ if (!slugs.has(slug)) {
42
+ acc.push(entry);
43
+ }
44
+ } else {
45
+ acc.push({
46
+ ...entry,
47
+ entries: filterIndexPages(entry.entries, slugs),
48
+ });
49
+ }
50
+ return acc;
51
+ }, []);
52
+ }
53
+
54
+ export const onRequest = defineRouteMiddleware(async (context, next) => {
55
+ const route = context.locals.starlightRoute;
56
+ const entry = route.entry;
57
+
58
+ if (isIndexPage(entry.filePath)) {
59
+ // Unconditionally hide TOC on index pages
60
+ route.toc = undefined;
61
+ }
62
+
63
+ if (indexSlugs.size > 0) {
64
+ route.sidebar = filterIndexPages(route.sidebar, indexSlugs);
65
+ }
66
+
67
+ if (route.lang && route.lang !== 'en') {
68
+ const translated = titleTranslations[route.siteTitle]?.[route.lang];
69
+ if (translated) route.siteTitle = translated;
70
+ }
71
+
72
+ await next();
73
+ });
@@ -0,0 +1,10 @@
1
+ import { defineCollection, z } from 'astro:content';
2
+ import { docsLoader } from '@astrojs/starlight/loaders';
3
+ import { docsSchema } from '@astrojs/starlight/schema';
4
+
5
+ export const collections = {
6
+ docs: defineCollection({
7
+ loader: docsLoader(),
8
+ schema: docsSchema({ extend: z.object({ subcategory: z.string().optional() }) }),
9
+ }),
10
+ };
@@ -0,0 +1,18 @@
1
+ import { categoryTitles, itemLabels, menuLabels } from './mega-menu-translations.ts';
2
+
3
+ const allTerms: Record<string, Record<string, string>> = {
4
+ ...menuLabels,
5
+ ...categoryTitles,
6
+ ...itemLabels,
7
+ };
8
+
9
+ export function getGlossary(localeCode: string): Record<string, string> {
10
+ const result: Record<string, string> = {};
11
+ for (const [english, translations] of Object.entries(allTerms)) {
12
+ const translated = translations[localeCode];
13
+ if (translated && translated !== english) {
14
+ result[english] = translated;
15
+ }
16
+ }
17
+ return result;
18
+ }
@@ -0,0 +1,56 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { defaultLocale, f5xcDefaultLocales } from './locales';
3
+
4
+ describe('f5xcDefaultLocales', () => {
5
+ it('contains 13 locales', () => {
6
+ expect(Object.keys(f5xcDefaultLocales)).toHaveLength(13);
7
+ });
8
+
9
+ it('uses slug format for keys', () => {
10
+ for (const key of Object.keys(f5xcDefaultLocales)) {
11
+ expect(key).toMatch(/^[a-z]+(-[a-z]+)?$/);
12
+ }
13
+ });
14
+
15
+ it('has BCP-47 lang property for each locale', () => {
16
+ for (const [_slug, config] of Object.entries(f5xcDefaultLocales)) {
17
+ expect(config.lang).toBeTruthy();
18
+ expect(config.label).toBeTruthy();
19
+ }
20
+ });
21
+
22
+ it('maps pt-br to BCP-47 pt-BR', () => {
23
+ expect(f5xcDefaultLocales['pt-br'].lang).toBe('pt-BR');
24
+ });
25
+
26
+ it('maps zh-cn to BCP-47 zh-CN', () => {
27
+ expect(f5xcDefaultLocales['zh-cn'].lang).toBe('zh-CN');
28
+ });
29
+
30
+ it('maps zh-tw to BCP-47 zh-TW', () => {
31
+ expect(f5xcDefaultLocales['zh-tw'].lang).toBe('zh-TW');
32
+ });
33
+
34
+ it('has RTL dir only for Arabic', () => {
35
+ expect(f5xcDefaultLocales.ar.dir).toBe('rtl');
36
+ expect(f5xcDefaultLocales.en.dir).toBeUndefined();
37
+ expect(f5xcDefaultLocales.fr.dir).toBeUndefined();
38
+ });
39
+
40
+ it('includes all expected locales', () => {
41
+ const expected = ['en', 'fr', 'es', 'de', 'pt-br', 'ja', 'ko', 'zh-cn', 'zh-tw', 'ar', 'it', 'hi', 'th'];
42
+ for (const slug of expected) {
43
+ expect(f5xcDefaultLocales).toHaveProperty(slug);
44
+ }
45
+ });
46
+ });
47
+
48
+ describe('defaultLocale', () => {
49
+ it('is "en"', () => {
50
+ expect(defaultLocale).toBe('en');
51
+ });
52
+
53
+ it('exists in f5xcDefaultLocales', () => {
54
+ expect(f5xcDefaultLocales).toHaveProperty(defaultLocale);
55
+ });
56
+ });
@@ -0,0 +1,7 @@
1
+ import { DEFAULT_LOCALE, type StarlightLocaleConfig, toStarlightLocales } from '@f5xc-salesdemos/i18n-core';
2
+
3
+ export type LocaleConfig = StarlightLocaleConfig;
4
+
5
+ export const f5xcDefaultLocales = toStarlightLocales();
6
+
7
+ export const defaultLocale = DEFAULT_LOCALE;