stack-site-builder 1.10.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 (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/dev/local-samples-client.js +284 -0
  4. package/dev/local-samples.mjs +594 -0
  5. package/index.d.ts +12 -0
  6. package/index.mjs +124 -0
  7. package/markdown.mjs +394 -0
  8. package/package.json +52 -0
  9. package/src/components/ArrowUpRight.astro +24 -0
  10. package/src/components/ArticleCard.astro +56 -0
  11. package/src/components/ArticleDetail.astro +100 -0
  12. package/src/components/ArticleLink.astro +45 -0
  13. package/src/components/BackToTop.astro +47 -0
  14. package/src/components/BlogIndex.astro +137 -0
  15. package/src/components/Breadcrumb.astro +37 -0
  16. package/src/components/CategoryIndex.astro +115 -0
  17. package/src/components/ChevronDown.astro +23 -0
  18. package/src/components/CodeSamples.astro +141 -0
  19. package/src/components/CollapsibleGrid.astro +50 -0
  20. package/src/components/ConceptCard.astro +59 -0
  21. package/src/components/ConceptDetail.astro +102 -0
  22. package/src/components/ConceptIndex.astro +143 -0
  23. package/src/components/ConceptLink.astro +43 -0
  24. package/src/components/ConceptTools.astro +49 -0
  25. package/src/components/DeckView.astro +772 -0
  26. package/src/components/DetailTabs.astro +219 -0
  27. package/src/components/FilterControls.astro +407 -0
  28. package/src/components/Glossary.astro +264 -0
  29. package/src/components/Home.astro +90 -0
  30. package/src/components/LanguageSwitcher.astro +86 -0
  31. package/src/components/ListControls.astro +182 -0
  32. package/src/components/MermaidLoader.astro +96 -0
  33. package/src/components/PricingSection.astro +100 -0
  34. package/src/components/ProjectFileList.astro +68 -0
  35. package/src/components/ProjectFileTree.astro +95 -0
  36. package/src/components/ProjectRelatedTools.astro +70 -0
  37. package/src/components/ProjectViewer.astro +431 -0
  38. package/src/components/RelatedArticles.astro +52 -0
  39. package/src/components/RelatedConcepts.astro +60 -0
  40. package/src/components/RelatedTools.astro +55 -0
  41. package/src/components/SampleIndex.astro +464 -0
  42. package/src/components/SamplePage.astro +95 -0
  43. package/src/components/SampleProject.astro +56 -0
  44. package/src/components/Slide.astro +36 -0
  45. package/src/components/SlidesIndex.astro +60 -0
  46. package/src/components/StackCard.astro +183 -0
  47. package/src/components/StackDetail.astro +507 -0
  48. package/src/components/StarIcon.astro +21 -0
  49. package/src/components/TagIndex.astro +51 -0
  50. package/src/components/ThemeToggle.astro +106 -0
  51. package/src/components/Toc.astro +39 -0
  52. package/src/components/TocRail.astro +74 -0
  53. package/src/components/ToolAvatar.astro +133 -0
  54. package/src/components/ToolMeta.astro +95 -0
  55. package/src/components/VendorIndex.astro +51 -0
  56. package/src/content.ts +228 -0
  57. package/src/i18n/ui.ts +337 -0
  58. package/src/layouts/BaseLayout.astro +287 -0
  59. package/src/lib/articles.ts +27 -0
  60. package/src/lib/assets.ts +19 -0
  61. package/src/lib/category-tree.ts +64 -0
  62. package/src/lib/concepts.ts +35 -0
  63. package/src/lib/dates.ts +21 -0
  64. package/src/lib/facets.ts +57 -0
  65. package/src/lib/github.ts +186 -0
  66. package/src/lib/inline-md.ts +30 -0
  67. package/src/lib/pricing.ts +22 -0
  68. package/src/lib/project.ts +309 -0
  69. package/src/lib/samples.ts +88 -0
  70. package/src/lib/slides.ts +25 -0
  71. package/src/lib/stacks.ts +129 -0
  72. package/src/lib/toc-rail-client.ts +79 -0
  73. package/src/pages/article/[...id].astro +24 -0
  74. package/src/pages/article/category/[id].astro +17 -0
  75. package/src/pages/article/index.astro +12 -0
  76. package/src/pages/categories/[id].astro +25 -0
  77. package/src/pages/concept/[...id].astro +24 -0
  78. package/src/pages/concept/category/[id].astro +17 -0
  79. package/src/pages/concept/index.astro +12 -0
  80. package/src/pages/glossary.astro +12 -0
  81. package/src/pages/index.astro +9 -0
  82. package/src/pages/ko/article/[...id].astro +24 -0
  83. package/src/pages/ko/article/category/[id].astro +17 -0
  84. package/src/pages/ko/article/index.astro +12 -0
  85. package/src/pages/ko/categories/[id].astro +24 -0
  86. package/src/pages/ko/concept/[...id].astro +24 -0
  87. package/src/pages/ko/concept/category/[id].astro +17 -0
  88. package/src/pages/ko/concept/index.astro +12 -0
  89. package/src/pages/ko/glossary.astro +12 -0
  90. package/src/pages/ko/index.astro +9 -0
  91. package/src/pages/ko/sample/[folder].astro +16 -0
  92. package/src/pages/ko/sample/index.astro +12 -0
  93. package/src/pages/ko/slides/[deck].astro +14 -0
  94. package/src/pages/ko/slides/index.astro +12 -0
  95. package/src/pages/ko/stack/[...id].astro +51 -0
  96. package/src/pages/ko/tags/[tag].astro +18 -0
  97. package/src/pages/ko/vendors/[vendor].astro +18 -0
  98. package/src/pages/sample/[folder].astro +16 -0
  99. package/src/pages/sample/index.astro +12 -0
  100. package/src/pages/slides/[deck].astro +14 -0
  101. package/src/pages/slides/index.astro +12 -0
  102. package/src/pages/stack/[...id].astro +51 -0
  103. package/src/pages/tags/[tag].astro +18 -0
  104. package/src/pages/vendors/[vendor].astro +18 -0
  105. package/src/styles/global.css +2350 -0
@@ -0,0 +1,96 @@
1
+ ---
2
+ // Renders any <pre class="mermaid"> on the page on the client, themed to match
3
+ // the site (colors pulled from our CSS variables), and re-renders when the
4
+ // theme changes. Include once on pages that contain Mermaid diagrams.
5
+ ---
6
+
7
+ <script>
8
+ import mermaid from 'mermaid';
9
+
10
+ // Read the live site palette so Mermaid matches the current theme exactly.
11
+ function palette() {
12
+ const cs = getComputedStyle(document.documentElement);
13
+ const v = (name: string) => cs.getPropertyValue(name).trim();
14
+ return {
15
+ bg: v('--aas-bg'),
16
+ panel: v('--aas-panel'),
17
+ text: v('--aas-text'),
18
+ accent: v('--aas-accent'),
19
+ border: v('--aas-border'),
20
+ muted: v('--aas-muted'),
21
+ };
22
+ }
23
+
24
+ function init() {
25
+ const p = palette();
26
+ mermaid.initialize({
27
+ startOnLoad: false,
28
+ securityLevel: 'strict',
29
+ theme: 'base',
30
+ themeVariables: {
31
+ background: 'transparent',
32
+ fontFamily: 'ui-sans-serif, system-ui, sans-serif',
33
+ primaryColor: p.panel,
34
+ primaryTextColor: p.text,
35
+ primaryBorderColor: p.accent,
36
+ nodeBorder: p.accent,
37
+ mainBkg: p.panel,
38
+ nodeTextColor: p.text,
39
+ lineColor: p.muted,
40
+ edgeLabelBackground: p.bg,
41
+ clusterBkg: 'transparent',
42
+ clusterBorder: p.muted,
43
+ titleColor: p.text,
44
+ secondaryColor: p.bg,
45
+ tertiaryColor: p.bg,
46
+ },
47
+ });
48
+ }
49
+
50
+ async function renderAll() {
51
+ const nodes = Array.from(document.querySelectorAll<HTMLElement>('pre.mermaid'));
52
+ if (nodes.length === 0) return;
53
+ for (const n of nodes) {
54
+ if (!n.dataset.src) n.dataset.src = (n.textContent ?? '').trim();
55
+ n.removeAttribute('data-processed');
56
+ // textContent, not innerHTML: dataset.src is entity-DECODED source, so
57
+ // injecting it as HTML would turn `<…>` in labels into live DOM (the
58
+ // server-side escaping exists precisely to prevent that). Mermaid reads
59
+ // the element's innerHTML and decodes entities itself.
60
+ n.textContent = n.dataset.src ?? '';
61
+ }
62
+ init();
63
+ try {
64
+ await mermaid.run({ nodes });
65
+ } catch {
66
+ /* leave the raw source visible on failure */
67
+ }
68
+ }
69
+
70
+ renderAll();
71
+
72
+ // Allow tab/version switches to re-render diagrams that were hidden (and thus
73
+ // mis-sized) at first paint.
74
+ (window as unknown as { __aasRenderMermaid?: () => void }).__aasRenderMermaid = renderAll;
75
+
76
+ // Re-render diagrams (with fresh palette) when the theme toggles.
77
+ let last = document.documentElement.dataset.theme;
78
+ new MutationObserver(() => {
79
+ const now = document.documentElement.dataset.theme;
80
+ if (now !== last) {
81
+ last = now;
82
+ renderAll();
83
+ }
84
+ }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
85
+
86
+ // A diagram inside a closed <details> renders at zero size; when the reveal
87
+ // opens, re-render so it lays out correctly. `toggle` doesn't bubble — capture.
88
+ document.addEventListener(
89
+ 'toggle',
90
+ (e) => {
91
+ const d = e.target as HTMLElement;
92
+ if (d instanceof HTMLDetailsElement && d.open && d.querySelector('pre.mermaid')) renderAll();
93
+ },
94
+ true,
95
+ );
96
+ </script>
@@ -0,0 +1,100 @@
1
+ ---
2
+ // A "Pricing" tab body: a short intro (`note`) above an optional plan / price /
3
+ // note table, with the source and the as-of date below. Driven by the
4
+ // frontmatter pricing fields.
5
+ //
6
+ // The freshness chip (aging 6m+, stale 1y+) is computed client-side against the
7
+ // visitor's *current* date — not the build date — so a long-lived static build
8
+ // still flags stale prices. The chip ships hidden and is revealed by the script.
9
+ import { useTranslations, type Lang } from '../i18n/ui';
10
+
11
+ interface Tier {
12
+ plan: string;
13
+ price: string;
14
+ note?: string;
15
+ }
16
+ interface Props {
17
+ lang: Lang;
18
+ note?: string;
19
+ tiers?: Tier[];
20
+ source?: string;
21
+ checkedAt?: string;
22
+ }
23
+ const { lang, note, tiers = [], source, checkedAt } = Astro.props;
24
+ const t = useTranslations(lang);
25
+ const hasTable = tiers.length > 0;
26
+ ---
27
+
28
+ {
29
+ (note || hasTable) && (
30
+ <Fragment>
31
+ <h2 id="pricing">
32
+ <a class="aas-anchor" href="#pricing" aria-label="Copy link to section" />
33
+ {t('detail.pricing')}
34
+ </h2>
35
+ {note && <p>{note}</p>}
36
+ {hasTable && (
37
+ <table>
38
+ <thead>
39
+ <tr>
40
+ <th>{t('detail.plan')}</th>
41
+ <th>{t('detail.price')}</th>
42
+ <th>{t('detail.notes')}</th>
43
+ </tr>
44
+ </thead>
45
+ <tbody>
46
+ {tiers.map((tier) => (
47
+ <tr>
48
+ <td>{tier.plan}</td>
49
+ <td>{tier.price}</td>
50
+ <td>{tier.note}</td>
51
+ </tr>
52
+ ))}
53
+ </tbody>
54
+ </table>
55
+ )}
56
+ {(source || checkedAt) && (
57
+ <p class="text-sm text-[var(--aas-muted)]">
58
+ {source && (
59
+ <a href={source} target="_blank" rel="noopener noreferrer">
60
+ {t('detail.source')} &nearr;
61
+ </a>
62
+ )}
63
+ {checkedAt && (
64
+ <Fragment>
65
+ {source ? ' · ' : ''}
66
+ {`${t('detail.asOf')} ${checkedAt}`}
67
+ <span
68
+ data-pricing-freshness
69
+ data-checked={checkedAt}
70
+ data-aging-label={t('pricing.aging')}
71
+ data-aging-hint={t('pricing.agingHint')}
72
+ data-stale-label={t('pricing.stale')}
73
+ data-stale-hint={t('pricing.staleHint')}
74
+ class="ml-2 hidden rounded-md px-2 py-0.5 align-middle text-xs font-medium"
75
+ />
76
+ </Fragment>
77
+ )}
78
+ </p>
79
+ )}
80
+ </Fragment>
81
+ )
82
+ }
83
+
84
+ <script>
85
+ // Flag prices that haven't been re-checked recently, relative to the viewer's
86
+ // current date (so a stale static build still warns). Mirrors stalenessOf().
87
+ const SIX_MONTHS = 182 * 24 * 60 * 60 * 1000;
88
+ const ONE_YEAR = 365 * 24 * 60 * 60 * 1000;
89
+ document.querySelectorAll<HTMLElement>('[data-pricing-freshness]').forEach((el) => {
90
+ const ts = Date.parse(el.dataset.checked || '');
91
+ if (Number.isNaN(ts)) return;
92
+ const age = Date.now() - ts;
93
+ const kind = age > ONE_YEAR ? 'stale' : age > SIX_MONTHS ? 'aging' : null;
94
+ if (!kind) return;
95
+ el.textContent = kind === 'stale' ? el.dataset.staleLabel! : el.dataset.agingLabel!;
96
+ el.title = kind === 'stale' ? el.dataset.staleHint! : el.dataset.agingHint!;
97
+ el.classList.add(kind === 'stale' ? 'aas-stale' : 'aas-aging');
98
+ el.classList.remove('hidden');
99
+ });
100
+ </script>
@@ -0,0 +1,68 @@
1
+ ---
2
+ import { useTranslations, type Lang } from '../i18n/ui';
3
+ import ProjectFileTree, { type DirNode, type TreeNode } from './ProjectFileTree.astro';
4
+
5
+ interface Props {
6
+ lang: Lang;
7
+ folder: string;
8
+ files: { path: string }[];
9
+ class?: string;
10
+ }
11
+
12
+ const { lang, folder, files, class: className } = Astro.props;
13
+ const t = useTranslations(lang);
14
+
15
+ // Group the flat, pre-sorted file list into a folder tree. Each file keeps its
16
+ // original index (the ProjectViewer script selects the file at index 0 by
17
+ // default). A directory node is created the first time one of its files is
18
+ // seen, so folders land where their contents already sort.
19
+ function buildTree(items: { path: string }[]): TreeNode[] {
20
+ const roots: TreeNode[] = [];
21
+ const dirChildren = new Map<string, TreeNode[]>([['', roots]]);
22
+
23
+ items.forEach((file, index) => {
24
+ const parts = file.path.split('/');
25
+ let parentPath = '';
26
+ for (let i = 0; i < parts.length - 1; i++) {
27
+ const dirPath = parentPath ? `${parentPath}/${parts[i]}` : parts[i];
28
+ if (!dirChildren.has(dirPath)) {
29
+ const children: TreeNode[] = [];
30
+ dirChildren.set(dirPath, children);
31
+ const dir: DirNode = { type: 'dir', name: parts[i], path: dirPath, children };
32
+ dirChildren.get(parentPath)!.push(dir);
33
+ }
34
+ parentPath = dirPath;
35
+ }
36
+ dirChildren.get(parentPath)!.push({
37
+ type: 'file',
38
+ name: parts[parts.length - 1],
39
+ path: file.path,
40
+ index,
41
+ });
42
+ });
43
+
44
+ return roots;
45
+ }
46
+
47
+ // Folders before files at every level (stable within each group), so the tree
48
+ // reads like a file explorer.
49
+ function foldersFirst(nodes: TreeNode[]): TreeNode[] {
50
+ for (const n of nodes) if (n.type === 'dir') n.children = foldersFirst(n.children);
51
+ return [...nodes.filter((n) => n.type === 'dir'), ...nodes.filter((n) => n.type === 'file')];
52
+ }
53
+
54
+ const tree = foldersFirst(buildTree(files));
55
+ ---
56
+
57
+ {
58
+ files.length > 0 && (
59
+ <div class:list={['text-sm', className]}>
60
+ <p class="mb-2 text-xs font-semibold tracking-wide text-[var(--aas-muted)] uppercase">
61
+ {t('project.files')}
62
+ </p>
63
+ <div class="rounded-xl border border-[var(--aas-border)] bg-[var(--aas-panel)] p-1.5">
64
+ <ProjectFileTree folder={folder} nodes={tree} />
65
+ </div>
66
+ </div>
67
+ )
68
+ }
@@ -0,0 +1,95 @@
1
+ ---
2
+ // Recursive file-tree node. Folders are native <details> so they fold/expand
3
+ // with no JS; files stay plain buttons carrying the data-* attributes the
4
+ // ProjectViewer script matches on (data-file-path / data-file-folder), so the
5
+ // tree is a drop-in for the old flat list.
6
+ //
7
+ // Indentation is relative: each nested level is one <ul> with a left padding +
8
+ // a faint guide line (border-l), so children visibly hang off their folder.
9
+ import Self from './ProjectFileTree.astro';
10
+
11
+ export interface FileNode {
12
+ type: 'file';
13
+ name: string;
14
+ path: string;
15
+ index: number;
16
+ }
17
+ export interface DirNode {
18
+ type: 'dir';
19
+ name: string;
20
+ path: string;
21
+ children: TreeNode[];
22
+ }
23
+ export type TreeNode = FileNode | DirNode;
24
+
25
+ interface Props {
26
+ folder: string;
27
+ nodes: TreeNode[];
28
+ depth?: number;
29
+ }
30
+
31
+ const { folder, nodes, depth = 0 } = Astro.props;
32
+ ---
33
+
34
+ <ul
35
+ class:list={[
36
+ 'space-y-0.5',
37
+ // Guide line + indent for nested levels; the top level sits flush.
38
+ depth > 0 && 'ml-[0.85rem] border-l border-[var(--aas-border)] pl-1.5',
39
+ ]}
40
+ >
41
+ {
42
+ nodes.map((node) =>
43
+ node.type === 'dir' ? (
44
+ <li>
45
+ <details open class="aas-tree-dir" data-tree-folder={folder} data-tree-dir={node.path}>
46
+ <summary class="flex cursor-pointer list-none items-center gap-1.5 rounded-lg px-2 py-1 text-left font-mono text-xs text-[var(--aas-muted)] select-none hover:bg-[var(--aas-tint)]">
47
+ <svg
48
+ class="aas-tree-caret h-3 w-3 shrink-0 transition-transform"
49
+ viewBox="0 0 12 12"
50
+ aria-hidden="true"
51
+ >
52
+ <path
53
+ d="M4 2.5 7.5 6 4 9.5"
54
+ fill="none"
55
+ stroke="currentColor"
56
+ stroke-width="1.5"
57
+ stroke-linecap="round"
58
+ stroke-linejoin="round"
59
+ />
60
+ </svg>
61
+ <span>{node.name}/</span>
62
+ </summary>
63
+ <Self folder={folder} nodes={node.children} depth={depth + 1} />
64
+ </details>
65
+ </li>
66
+ ) : (
67
+ <li>
68
+ <button
69
+ type="button"
70
+ data-file={node.index}
71
+ data-file-path={node.path}
72
+ data-file-folder={folder}
73
+ aria-current={node.index === 0 ? 'true' : 'false'}
74
+ class="block w-full truncate rounded-lg px-2 py-1 text-left font-mono text-xs text-[var(--aas-muted)] hover:bg-[var(--aas-tint)] aria-[current=true]:bg-[var(--aas-tint)] aria-[current=true]:text-[var(--aas-text)]"
75
+ >
76
+ {node.name}
77
+ </button>
78
+ </li>
79
+ ),
80
+ )
81
+ }
82
+ </ul>
83
+
84
+ <style>
85
+ .aas-tree-dir > summary::-webkit-details-marker {
86
+ display: none;
87
+ }
88
+ .aas-tree-dir > summary {
89
+ list-style: none;
90
+ }
91
+ /* Caret points right when collapsed, rotates down when the folder is open. */
92
+ .aas-tree-dir[open] > summary .aas-tree-caret {
93
+ transform: rotate(90deg);
94
+ }
95
+ </style>
@@ -0,0 +1,70 @@
1
+ ---
2
+ import { getRelativeLocaleUrl } from 'astro:i18n';
3
+ import { categoryMap } from '@aas-data/categories';
4
+ import { useTranslations, type Lang } from '../i18n/ui';
5
+ import type { ToolRef } from '../lib/stacks';
6
+ import ToolAvatar from './ToolAvatar.astro';
7
+
8
+ /** A related tool resolved against the stacks collection — an alias of ToolRef
9
+ * (produced by `toolResolver` in src/lib/stacks.ts). */
10
+ export type RelatedTool = ToolRef;
11
+
12
+ interface Props {
13
+ lang: Lang;
14
+ tools: RelatedTool[];
15
+ }
16
+
17
+ const { lang, tools } = Astro.props;
18
+ const t = useTranslations(lang);
19
+ ---
20
+
21
+ {
22
+ tools.length > 0 && (
23
+ <nav aria-label={t('project.relatedTools')} class="text-sm">
24
+ <p class="mb-2 text-xs font-semibold tracking-wide text-[var(--aas-muted)] uppercase">
25
+ {t('project.relatedTools')}
26
+ </p>
27
+ <ul class="space-y-1.5">
28
+ {tools.map((tool) =>
29
+ tool.registered ? (
30
+ <li>
31
+ <a
32
+ href={getRelativeLocaleUrl(lang, `stack/${tool.slug}/`)}
33
+ class="aas-lift flex items-center gap-2.5 rounded-xl border border-[var(--aas-border)] bg-[var(--aas-panel)] p-2 no-underline"
34
+ >
35
+ <ToolAvatar name={tool.name} logo={tool.logo} logoDark={tool.logoDark} category={tool.category} size={28} />
36
+ <div class="min-w-0">
37
+ <div class="flex items-center gap-1.5">
38
+ <span class="truncate font-semibold text-[var(--aas-text)]">{tool.name}</span>
39
+ {tool.deprecated && (
40
+ <span class="aas-deprecated shrink-0 rounded px-1 py-0.5 text-[0.6rem]">
41
+ {t('meta.deprecated')}
42
+ </span>
43
+ )}
44
+ </div>
45
+ {tool.category && (
46
+ <div class="truncate text-xs text-[var(--aas-muted)]">
47
+ {categoryMap.get(tool.category)?.label[lang]}
48
+ </div>
49
+ )}
50
+ </div>
51
+ </a>
52
+ </li>
53
+ ) : (
54
+ <li>
55
+ <div class="flex items-center gap-2.5 rounded-xl border border-dashed border-[var(--aas-border)] p-2">
56
+ <ToolAvatar name={tool.name} category="" size={28} />
57
+ <div class="min-w-0">
58
+ <span class="truncate font-medium text-[var(--aas-muted)]">{tool.name}</span>
59
+ <div class="truncate text-xs text-[var(--aas-muted)] opacity-70">
60
+ {t('project.unregistered')}
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </li>
65
+ ),
66
+ )}
67
+ </ul>
68
+ </nav>
69
+ )
70
+ }