Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions docs/.vitepress/theme/components/NodeKindCatalog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"

type Locale = "en" | "ja" | "zh" | "ko"
type PreviewTheme = "default" | "light" | "dark"
type EngineModule = typeof import("@stack-sh/engine")

const props = defineProps<{ locale: Locale }>()

const kinds = [
"actor",
"client",
"service",
"function",
"worker",
"database",
"cache",
"queue",
"storage",
"external",
] as const

type NodeKind = (typeof kinds)[number]

const labels = {
en: {
copied: "Copied",
copy: "Copy",
copyLabel: "Copy Stack node kind syntax",
error: "The node kind previews could not be rendered.",
loading: "Rendering node kinds locally…",
previewAlt: "Stack node kind preview",
previewTheme: "Preview theme",
themes: { default: "Default", light: "Light", dark: "Dark" },
},
ja: {
copied: "コピー済み",
copy: "コピー",
copyLabel: "Stack node kind syntaxをコピー",
error: "Node kind previewをrenderできませんでした。",
loading: "Node kindをlocalでrenderしています…",
previewAlt: "Stack node kind preview",
previewTheme: "Preview theme",
themes: { default: "Default", light: "Light", dark: "Dark" },
},
zh: {
copied: "已复制",
copy: "复制",
copyLabel: "复制 Stack 节点种类语法",
error: "无法渲染节点种类预览。",
loading: "正在本地渲染节点种类…",
previewAlt: "Stack 节点种类预览",
previewTheme: "预览主题",
themes: { default: "默认", light: "浅色", dark: "深色" },
},
ko: {
copied: "복사됨",
copy: "복사",
copyLabel: "Stack 노드 종류 문법 복사",
error: "노드 종류 미리보기를 렌더링하지 못했습니다.",
loading: "노드 종류를 로컬에서 렌더링하는 중…",
previewAlt: "Stack 노드 종류 미리보기",
previewTheme: "미리보기 테마",
themes: { default: "기본", light: "라이트", dark: "다크" },
},
} as const

let enginePromise: Promise<EngineModule> | undefined

function loadEngine(): Promise<EngineModule> {
enginePromise ??= import("@stack-sh/engine").then(async (engine) => {
await engine.default()
return engine
})
return enginePromise
}

function sourceFor(kind: NodeKind, theme: PreviewTheme) {
return `stack 1.0

diagram "${kind}" {
theme ${theme}

node preview "${kind}" {
kind ${kind}
}
}`
}

const text = computed(() => labels[props.locale])
const previewTheme = ref<PreviewTheme>("default")
const renderedKinds = ref<Partial<Record<NodeKind, string>>>({})
const copiedKind = ref<NodeKind>()
const metadata = ref("")
const loading = ref(true)
const renderingError = ref(false)

let engine: EngineModule | undefined
let copyTimer: number | undefined
let objectUrls: string[] = []
let disposed = false

function releaseObjectUrls() {
for (const url of objectUrls) URL.revokeObjectURL(url)
objectUrls = []
}

function renderKinds() {
if (!engine || disposed) return

releaseObjectUrls()
const nextKinds: Partial<Record<NodeKind, string>> = {}

try {
for (const kind of kinds) {
const result = engine.render(sourceFor(kind, previewTheme.value))

if (!result.svg || result.diagnostics.length > 0) {
throw new Error(`Engine did not render the ${kind} node kind without diagnostics`)
}

const url = URL.createObjectURL(new Blob([result.svg], { type: "image/svg+xml" }))
objectUrls.push(url)
nextKinds[kind] = url
metadata.value = `Engine ${result.metadata.engineVersion} / Theme Catalog ${result.metadata.themeCatalogVersion}`
}

renderedKinds.value = nextKinds
renderingError.value = false
} catch {
releaseObjectUrls()
renderedKinds.value = {}
renderingError.value = true
}
}

async function copySyntax(kind: NodeKind) {
if (!navigator.clipboard) return

try {
await navigator.clipboard.writeText(`kind ${kind}`)
} catch {
return
}

if (disposed) return

copiedKind.value = kind
window.clearTimeout(copyTimer)
copyTimer = window.setTimeout(() => {
copiedKind.value = undefined
}, 1600)
}

onMounted(async () => {
try {
const loadedEngine = await loadEngine()
if (disposed) return
engine = loadedEngine
renderKinds()
} catch {
if (!disposed) renderingError.value = true
} finally {
if (!disposed) loading.value = false
}
})

watch(previewTheme, renderKinds)

onBeforeUnmount(() => {
disposed = true
window.clearTimeout(copyTimer)
releaseObjectUrls()
})
</script>

<template>
<section class="stack-icon-catalog-section">
<div class="stack-icon-catalog-toolbar">
<div class="stack-icon-theme-control" role="group" :aria-label="text.previewTheme">
<button
v-for="theme in ['default', 'light', 'dark'] as const"
:key="theme"
type="button"
:aria-pressed="previewTheme === theme"
@click="previewTheme = theme"
>
{{ text.themes[theme] }}
</button>
</div>
<span v-if="metadata" class="stack-icon-catalog-metadata">{{ metadata }}</span>
</div>

<p v-if="loading" class="stack-icon-catalog-status" role="status">{{ text.loading }}</p>
<p v-else-if="renderingError" class="stack-icon-catalog-status" role="alert">
{{ text.error }}
</p>

<div class="stack-icon-catalog" :aria-busy="loading">
<article
v-for="kind in kinds"
:key="kind"
class="stack-icon-card stack-kind-card"
:data-node-kind="kind"
>
<div class="stack-icon-card__preview">
<img
v-if="renderedKinds[kind]"
:src="renderedKinds[kind]"
:alt="`${text.previewAlt}: ${kind}, ${text.themes[previewTheme]}`"
/>
<span v-else class="stack-icon-card__placeholder" aria-hidden="true" />
</div>
<button
type="button"
class="stack-icon-card__copy"
:aria-label="`${text.copyLabel}: kind ${kind}`"
@click="copySyntax(kind)"
>
<code>kind {{ kind }}</code>
<span aria-live="polite">{{ copiedKind === kind ? text.copied : text.copy }}</span>
</button>
</article>
</div>
</section>
</template>
2 changes: 2 additions & 0 deletions docs/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "@fontsource/ibm-plex-mono/400.css"
import ExampleGallery from "./components/ExampleGallery.vue"
import DocsLayout from "./components/DocsLayout.vue"
import IconCatalog from "./components/IconCatalog.vue"
import NodeKindCatalog from "./components/NodeKindCatalog.vue"
import ProviderCatalog from "./components/ProviderCatalog.vue"
import "./style.css"

Expand All @@ -14,6 +15,7 @@ export default {
enhanceApp({ app }) {
app.component("ExampleGallery", ExampleGallery)
app.component("IconCatalog", IconCatalog)
app.component("NodeKindCatalog", NodeKindCatalog)
app.component("ProviderCatalog", ProviderCatalog)
},
}
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"@fontsource/ibm-plex-mono": "^5.3.0",
"@shikijs/core": "4.4.3",
"@shikijs/engine-javascript": "4.4.3",
"@stack-sh/engine": "0.8.0",
"@stack-sh/engine": "0.9.0",
"@stack-sh/language": "0.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand Down
4 changes: 2 additions & 2 deletions scripts/docs-source.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"repository": "stack-sh/docs",
"revision": "e9847c68865027dd51be9ec18df263d6a9ccbcd1",
"manifestSha256": "c8cd1609ad61b95f5d54d10575ad7715c06ed6c1798067c906e3dfc7282e1d16"
"revision": "16b8ac60219f5051bdb0319048e9b6200d3a614a",
"manifestSha256": "a0e1dc79a73553c6b6cc0bcf7ba8a5b5d134a63eedca3d181e41c1614faa5853"
}
8 changes: 8 additions & 0 deletions scripts/validate-docs-output.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ for (const [page, language] of localePages) {
}
}

for (const locale of ["", "ja/", "zh/", "ko/"]) {
const page = `${locale}language/nodes-and-groups.html`
const html = await readFile(path.join(outputRoot, page), "utf8")
const cards = html.match(/data-node-kind="/g)?.length ?? 0

if (cards !== 10) throw new Error(`${page} contains ${cards} node kind cards instead of 10`)
}

for (const locale of ["", "ja/", "zh/", "ko/"]) {
const page = `${locale}language/themes-and-icons.html`
const html = await readFile(path.join(outputRoot, page), "utf8")
Expand Down
16 changes: 14 additions & 2 deletions scripts/validate-docs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ const providerCatalog = JSON.parse(
await readFile(path.join(docsRoot, ".vitepress/theme/data/provider-catalogs.json"), "utf8"),
)

if (packageMetadata.dependencies["@stack-sh/engine"] !== "0.8.0") {
throw new Error("Documentation must use the exact @stack-sh/engine 0.8.0 release")
if (packageMetadata.dependencies["@stack-sh/engine"] !== "0.9.0") {
throw new Error("Documentation must use the exact @stack-sh/engine 0.9.0 release")
}

const expectedProviderCounts = { aws: 305, gcp: 45, azure: 639, "simple-icons": 62 }
Expand Down Expand Up @@ -232,6 +232,18 @@ for (const term of requiredTerms) {
throw new Error(`English documentation is missing required coverage for ${term}`)
}

for (const locale of ["", ...locales]) {
const page = path.join(docsRoot, locale, "language/nodes-and-groups.md")
const source = await readFile(page, "utf8")
const componentLocale = locale || "en"

if (!source.includes(`<NodeKindCatalog locale="${componentLocale}" />`)) {
throw new Error(
`${componentLocale}/language/nodes-and-groups.md is missing its node kind gallery`,
)
}
}

for (const locale of ["", ...locales]) {
const page = path.join(docsRoot, locale, "language/themes-and-icons.md")
const source = await readFile(page, "utf8")
Expand Down
4 changes: 2 additions & 2 deletions scripts/validate-example-corpus.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ for (const example of catalog.examples) {
providerIcons.length,
`${example.id} ${operation} provider fallback count does not match its source`,
)
assert.equal(result.metadata.engineVersion, "0.8.0")
assert.equal(result.metadata.engineVersion, "0.9.0")
assert.deepEqual(result.metadata.languageVersion, { major: 1, minor: 0 })
}

Expand All @@ -76,5 +76,5 @@ for (const example of catalog.examples) {
}

console.log(
`Validated ${catalog.examples.length} canonical examples with @stack-sh/engine 0.8.0; no SVG files are generated.`,
`Validated ${catalog.examples.length} canonical examples with @stack-sh/engine 0.9.0; no SVG files are generated.`,
)