diff --git a/.gitignore b/.gitignore index d477200..db2955a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ lerna-debug.log* node_modules dist dist-ssr +docs/.vitepress/cache *.local .wrangler .dev.vars* diff --git a/docs/.vitepress/theme/components/IconCatalog.vue b/docs/.vitepress/theme/components/IconCatalog.vue new file mode 100644 index 0000000..0f6b0dd --- /dev/null +++ b/docs/.vitepress/theme/components/IconCatalog.vue @@ -0,0 +1,220 @@ + + + diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index 58e793d..195f747 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -2,6 +2,12 @@ import DefaultTheme from "vitepress/theme" import "@fontsource-variable/ibm-plex-sans" import "@fontsource/ibm-plex-mono/400.css" +import IconCatalog from "./components/IconCatalog.vue" import "./style.css" -export default DefaultTheme +export default { + extends: DefaultTheme, + enhanceApp({ app }) { + app.component("IconCatalog", IconCatalog) + }, +} diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css index 3223b18..ab439cc 100644 --- a/docs/.vitepress/theme/style.css +++ b/docs/.vitepress/theme/style.css @@ -140,10 +140,161 @@ body { border-radius: 0.25rem; } +.stack-icon-catalog-section { + margin: 1.5rem 0; +} + +.stack-icon-catalog-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; +} + +.stack-icon-theme-control { + display: inline-flex; + padding: 0.1875rem; + border: 1px solid var(--vp-c-divider); + border-radius: 0.375rem; +} + +.stack-icon-theme-control button { + padding: 0.35rem 0.75rem; + color: var(--vp-c-text-2); + font-size: 0.8125rem; + font-weight: 600; + line-height: 1.25rem; + border-radius: 0.25rem; +} + +.stack-icon-theme-control button[aria-pressed="true"] { + color: var(--vp-button-brand-text); + background: var(--vp-button-brand-bg); +} + +.stack-icon-theme-control button:focus-visible, +.stack-icon-card__copy:focus-visible { + outline: 2px solid var(--vp-c-brand-1); + outline-offset: 2px; +} + +.stack-icon-catalog-metadata, +.stack-icon-catalog-status { + color: var(--vp-c-text-2); + font-size: 0.8125rem; +} + +.stack-icon-catalog-status { + margin: 0.75rem 0 0; +} + +.stack-icon-catalog { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.75rem; + margin-top: 0.75rem; +} + +.stack-icon-card { + min-width: 0; + overflow: hidden; + background: var(--vp-c-bg); + border: 1px solid var(--vp-c-divider); + border-radius: 0.375rem; +} + +.stack-icon-card__preview { + display: grid; + min-height: 9rem; + padding: 0.75rem; + place-items: center; + background: var(--vp-c-bg-soft); + border-bottom: 1px solid var(--vp-c-divider); +} + +.stack-icon-card__preview img { + display: block; + width: 100%; + height: 8rem; + margin: 0; + object-fit: contain; +} + +.stack-icon-card__placeholder { + width: 72%; + height: 6rem; + background: var(--vp-c-divider); + border-radius: 0.25rem; + opacity: 0.5; +} + +.stack-icon-card__identity { + display: grid; + gap: 0.25rem; + padding: 1rem 1rem 0; +} + +.stack-icon-card__identity code { + width: fit-content; + color: var(--vp-c-brand-1); + font-weight: 600; +} + +.stack-icon-card__identity span { + min-width: 0; + color: var(--vp-c-text-2); + font-size: 0.875rem; + line-height: 1.5; +} + +.stack-icon-card__copy { + display: flex; + align-items: center; + justify-content: space-between; + width: calc(100% - 2rem); + gap: 0.75rem; + margin: 0.75rem 1rem 1rem; + padding: 0.5rem 0.625rem; + color: var(--vp-c-text-2); + text-align: left; + background: var(--vp-code-block-bg); + border: 1px solid var(--vp-c-divider); + border-radius: 0.25rem; +} + +.vp-doc .stack-icon-card__copy code { + min-width: 0; + padding: 0; + overflow-wrap: anywhere; + background: transparent; + font-size: 0.75rem; + white-space: nowrap; +} + +.vp-doc .stack-icon-card__copy span { + color: var(--vp-c-brand-1); + font-size: 0.75rem; + font-weight: 600; + white-space: nowrap; +} + +.stack-icon-card__copy:hover { + border-color: var(--vp-c-brand-1); +} + @media (max-width: 639px) { .vp-doc table { display: block; } + + .stack-icon-catalog { + grid-template-columns: 1fr; + } + + .stack-icon-catalog-toolbar { + align-items: flex-start; + flex-direction: column; + } } @media (prefers-reduced-motion: reduce) { diff --git a/docs/ja/language/themes-and-icons.md b/docs/ja/language/themes-and-icons.md index c5cce97..cc898c9 100644 --- a/docs/ja/language/themes-and-icons.md +++ b/docs/ja/language/themes-and-icons.md @@ -60,15 +60,46 @@ Theme identifierは通常のStack identifier syntaxを使います。一度登 ```stack stack 1.0 -diagram "Icon fallback" { - node primary "Primary database" { - kind database - icon "postgresql" +diagram "Explicit icon" { + node gateway "Public API" { + kind service + icon "api" } } ``` -Iconはnodeを装飾するだけで、`database` kindやaccessible labelを変えません。Effective themeに`postgresql`がなければ、`kind-database`でrenderを続け、warning `STK5001`を出します。Themeごとにartworkを変えられますが、同じidentifierは同じlogical subjectを表します。 +Iconはnodeを装飾するだけで、`service` kind、identity、label、accessible descriptionを変えません。Themeごとに色やartworkを調整できますが、同じidentifierは同じlogical subjectを表します。 + +## First-party icon catalog + +Free core catalogは、次のprovider-neutralな明示iconを`default`、`light`、`dark`に収録しています。 + +| ID | Stable subject | 主な用途 | +| --------------- | --------------------------------- | -------------------------------- | +| `api` | Application programming interface | Public / internal API | +| `web` | Web application | Browser向けWeb experience | +| `mobile` | Mobile application | iOS / Android client | +| `desktop` | Desktop application | Native desktop client | +| `server` | Server host | Virtual machine / physical host | +| `container` | Application container | Containerized workload | +| `cluster` | Compute cluster | Orchestrated compute group | +| `cloud` | Cloud environment | Provider-neutralなcloud boundary | +| `scheduler` | Scheduled execution | Cron job / scheduled work | +| `webhook` | Webhook endpoint | Inbound / outbound callback | +| `identity` | Identity and access | Authentication / authorization | +| `observability` | Observability system | Metrics / logs / traces | + +下のpreviewをlight / darkへ切り替えると、実際のoutputを比較できます。各imageは公開`@stack-sh/engine@0.3.0`がlocalでrenderし、DocumentationにTheme SVG assetを複製していません。Syntaxの行を選ぶとコピーできます。 + + + +Semanticな`kind`と明示iconは独立して選びます。たとえば`icon "web"`は`client`にも`service`にも使えますが、そのnodeの意味は変更しません。 + +## 未収録iconとvendor icon + +Core catalogは現在、`postgresql`、`aws`、`github`、`docker`などのvendor / project markを収録していません。Effective themeにauthored icon identifierがなければ、node kindのfallbackでrenderを続け、warning `STK5001`を出します。 + +上表のprovider-neutral iconを選ぶか、kind fallbackだけで役割が伝わる場合は`icon`を省略してください。Vendor markを別catalogへ収録するには、asset単位のlicense、redistribution、trademark reviewが必要です。 ## Catalogとasset safety diff --git a/docs/ko/language/themes-and-icons.md b/docs/ko/language/themes-and-icons.md index e6d1741..7b2524e 100644 --- a/docs/ko/language/themes-and-icons.md +++ b/docs/ko/language/themes-and-icons.md @@ -60,15 +60,46 @@ diagram "Dark architecture" { ```stack stack 1.0 -diagram "Icon fallback" { - node primary "Primary database" { - kind database - icon "postgresql" +diagram "Explicit icon" { + node gateway "Public API" { + kind service + icon "api" } } ``` -아이콘은 노드를 꾸밀 뿐 `database` kind나 접근성 라벨을 바꾸지 않습니다. 유효 테마에 `postgresql`이 없으면 `kind-database`로 계속 렌더링하고 `STK5001`을 냅니다. 테마마다 그림은 다를 수 있지만 같은 식별자는 같은 논리 대상을 의미해야 합니다. +아이콘은 노드를 꾸밀 뿐 `service` kind, identity, label, 접근성 설명을 바꾸지 않습니다. 테마마다 색상과 그림은 달라질 수 있지만 같은 식별자는 같은 논리 대상을 나타냅니다. + +## First-party 아이콘 catalog + +무료 코어 catalog는 `default`, `light`, `dark`에 다음 provider-neutral 명시적 아이콘을 제공합니다. + +| ID | Stable subject | 주요 용도 | +| --------------- | --------------------------------- | ------------------------------ | +| `api` | Application programming interface | 공개 또는 내부 API | +| `web` | Web application | 브라우저용 Web 경험 | +| `mobile` | Mobile application | iOS 또는 Android 클라이언트 | +| `desktop` | Desktop application | 네이티브 데스크톱 클라이언트 | +| `server` | Server host | 가상 머신 또는 물리 호스트 | +| `container` | Application container | 컨테이너화된 워크로드 | +| `cluster` | Compute cluster | 오케스트레이션된 컴퓨팅 그룹 | +| `cloud` | Cloud environment | provider-neutral 클라우드 경계 | +| `scheduler` | Scheduled execution | Cron 작업 또는 예약 실행 | +| `webhook` | Webhook endpoint | 인바운드 또는 아웃바운드 콜백 | +| `identity` | Identity and access | 인증 또는 권한 부여 | +| `observability` | Observability system | 메트릭, 로그 또는 트레이스 | + +아래 미리보기를 라이트와 다크로 전환하여 실제 출력을 비교할 수 있습니다. 각 이미지는 공개된 `@stack-sh/engine@0.3.0`이 로컬에서 렌더링하며 문서에 Theme SVG 에셋을 복제하지 않습니다. 문법 행을 선택하면 복사됩니다. + + + +의미를 나타내는 `kind`와 명시적 아이콘은 독립적으로 선택합니다. 예를 들어 `icon "web"`은 `client` 또는 `service` 노드를 꾸밀 수 있지만 노드의 의미를 바꾸지 않습니다. + +## 누락 아이콘과 벤더 아이콘 + +코어 catalog에는 현재 `postgresql`, `aws`, `github`, `docker` 같은 벤더 또는 프로젝트 마크가 없습니다. 유효 테마가 작성한 아이콘 식별자를 제공하지 않으면 노드 kind fallback으로 계속 렌더링하고 `STK5001` 경고를 냅니다. + +위 표의 provider-neutral 아이콘을 사용하거나 kind fallback만으로 역할이 명확하면 `icon`을 생략하세요. 벤더 마크는 에셋별 license, 재배포 및 trademark review를 완료해야 별도 catalog에 포함할 수 있습니다. ## Catalog와 에셋 안전성 diff --git a/docs/language/themes-and-icons.md b/docs/language/themes-and-icons.md index e8ae7b3..122a705 100644 --- a/docs/language/themes-and-icons.md +++ b/docs/language/themes-and-icons.md @@ -60,15 +60,46 @@ An explicit icon is a quoted logical identifier: ```stack stack 1.0 -diagram "Icon fallback" { - node primary "Primary database" { - kind database - icon "postgresql" +diagram "Explicit icon" { + node gateway "Public API" { + kind service + icon "api" } } ``` -The icon decorates the node; it does not change the `database` kind or accessible label. If the effective theme does not provide `postgresql`, rendering continues with `kind-database` and warning `STK5001`. The same identifier in different themes must represent the same logical subject, although the artwork may differ. +The icon decorates the node; it does not change the `service` kind, identity, label, or accessible description. The same identifier in different themes represents the same logical subject, although its color and artwork may adapt to the theme. + +## First-party icon catalog + +The free core catalog includes these provider-neutral explicit icons in `default`, `light`, and `dark`: + +| ID | Stable subject | Use it for | +| --------------- | --------------------------------- | ---------------------------------- | +| `api` | Application programming interface | Public or internal APIs | +| `web` | Web application | Browser-facing web experiences | +| `mobile` | Mobile application | iOS or Android clients | +| `desktop` | Desktop application | Native desktop clients | +| `server` | Server host | Virtual machines or physical hosts | +| `container` | Application container | Containerized workloads | +| `cluster` | Compute cluster | Orchestrated compute groups | +| `cloud` | Cloud environment | Provider-neutral cloud boundaries | +| `scheduler` | Scheduled execution | Cron jobs or scheduled work | +| `webhook` | Webhook endpoint | Inbound or outbound callbacks | +| `identity` | Identity and access | Authentication or authorization | +| `observability` | Observability system | Metrics, logs, or traces | + +Switch the preview below between light and dark to inspect the actual output. The published `@stack-sh/engine@0.3.0` renders every image locally; the Documentation does not copy Theme SVG assets. Select a syntax row to copy it. + + + +Use the semantic `kind` independently from the explicit icon. For example, `icon "web"` may decorate either a `client` or `service` node without changing what that node means. + +## Missing and vendor icons + +The core catalog does not currently include vendor or project marks such as `postgresql`, `aws`, `github`, or `docker`. If the effective theme does not provide an authored icon identifier, rendering continues with the node's kind fallback and warning `STK5001`. + +Prefer a provider-neutral icon above or omit `icon` when its kind fallback already communicates the role. Vendor marks require asset-specific license, redistribution, and trademark review before they can ship in a separate catalog. ## Catalog and asset safety diff --git a/docs/zh/language/themes-and-icons.md b/docs/zh/language/themes-and-icons.md index d9da13b..ccad362 100644 --- a/docs/zh/language/themes-and-icons.md +++ b/docs/zh/language/themes-and-icons.md @@ -60,15 +60,46 @@ diagram "Dark architecture" { ```stack stack 1.0 -diagram "Icon fallback" { - node primary "Primary database" { - kind database - icon "postgresql" +diagram "Explicit icon" { + node gateway "Public API" { + kind service + icon "api" } } ``` -图标只装饰节点,不改变 `database` kind 或无障碍标签。如果有效主题没有 `postgresql`,会继续用 `kind-database` 渲染并发出 `STK5001`。同一标识符在不同主题中必须表示同一逻辑对象,但具体图形可以不同。 +图标只装饰节点,不改变 `service` kind、identity、label 或无障碍说明。同一标识符在不同主题中表示同一个逻辑对象,但颜色与图形可以适配主题。 + +## 第一方图标目录 + +免费核心 catalog 在 `default`、`light` 和 `dark` 中提供以下与厂商无关的显式图标: + +| ID | Stable subject | 主要用途 | +| --------------- | --------------------------------- | --------------------- | +| `api` | Application programming interface | 公共或内部 API | +| `web` | Web application | 面向浏览器的 Web 体验 | +| `mobile` | Mobile application | iOS 或 Android 客户端 | +| `desktop` | Desktop application | 原生桌面客户端 | +| `server` | Server host | 虚拟机或物理主机 | +| `container` | Application container | 容器化工作负载 | +| `cluster` | Compute cluster | 编排后的计算集群 | +| `cloud` | Cloud environment | 与厂商无关的云边界 | +| `scheduler` | Scheduled execution | Cron 任务或定时工作 | +| `webhook` | Webhook endpoint | 入站或出站回调 | +| `identity` | Identity and access | 身份验证或授权 | +| `observability` | Observability system | 指标、日志或链路追踪 | + +切换下方预览的浅色与深色模式即可比较真实输出。每张图片都由已发布的 `@stack-sh/engine@0.3.0` 在本地渲染;文档没有复制 Theme SVG 资源。选择语法行即可复制。 + + + +语义 `kind` 与显式图标应独立选择。例如,`icon "web"` 可以装饰 `client` 或 `service` 节点,但不会改变该节点的含义。 + +## 缺失图标与厂商图标 + +核心 catalog 目前不包含 `postgresql`、`aws`、`github` 或 `docker` 等厂商或项目标志。如果有效主题没有编写的图标标识符,引擎会继续使用节点 kind 的回退图标渲染,并发出 `STK5001`。 + +请优先选择上表中与厂商无关的图标;如果 kind 回退已能表达角色,也可以省略 `icon`。厂商标志只有在逐项完成资源 license、再分发和商标审查后,才能进入独立 catalog。 ## Catalog 与资源安全 diff --git a/package-lock.json b/package-lock.json index e51abfd..05a7398 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@fontsource/ibm-plex-mono": "^5.3.0", "@shikijs/core": "4.4.3", "@shikijs/engine-javascript": "4.4.3", - "@stack-sh/engine": "0.2.0", + "@stack-sh/engine": "0.3.0", "@stack-sh/language": "0.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -4869,9 +4869,9 @@ "license": "CC0-1.0" }, "node_modules/@stack-sh/engine": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@stack-sh/engine/-/engine-0.2.0.tgz", - "integrity": "sha512-1IpIG72KHilvCz0w6vq9smOWLFo5JH+3VhpcTXK4Ss6D0alhw1xPDlnu87vGNi0gt4NBEDB6HYY3gaq5uyp0gA==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@stack-sh/engine/-/engine-0.3.0.tgz", + "integrity": "sha512-+D2vjK2w9dpUIY5Pw0WJ05zs50xdq2uhoisXt0a105IM3BUO5z5znra3cU79fFfDOQqyFLgqlpF8J1Ob1PJIhQ==", "license": "Apache-2.0" }, "node_modules/@stack-sh/language": { diff --git a/package.json b/package.json index 858971b..86d03d6 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@fontsource/ibm-plex-mono": "^5.3.0", "@shikijs/core": "4.4.3", "@shikijs/engine-javascript": "4.4.3", - "@stack-sh/engine": "0.2.0", + "@stack-sh/engine": "0.3.0", "@stack-sh/language": "0.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/scripts/validate-docs-output.mjs b/scripts/validate-docs-output.mjs index cd6bc5b..40c3124 100644 --- a/scripts/validate-docs-output.mjs +++ b/scripts/validate-docs-output.mjs @@ -1,4 +1,4 @@ -import { readFile } from "node:fs/promises" +import { readFile, readdir } from "node:fs/promises" import path from "node:path" const outputRoot = path.resolve("dist/docs") @@ -78,6 +78,20 @@ for (const [page, language] of localePages) { } } +for (const locale of ["", "ja/", "zh/", "ko/"]) { + const page = `${locale}language/themes-and-icons.html` + const html = await readFile(path.join(outputRoot, page), "utf8") + const cards = html.match(/class="stack-icon-card"/g)?.length ?? 0 + + if (cards !== 12) throw new Error(`${page} contains ${cards} icon cards instead of 12`) +} + +const documentationAssets = await readdir(path.join(outputRoot, "assets")) + +if (!documentationAssets.some((asset) => /^stack_engine_bg\..+\.wasm$/.test(asset))) { + throw new Error("Built Documentation does not include the published Engine WebAssembly") +} + const sitemap = await readFile(path.join(outputRoot, "sitemap.xml"), "utf8") const locations = [...sitemap.matchAll(/(.*?)<\/loc>/g)].map((match) => match[1]) diff --git a/scripts/validate-docs.mjs b/scripts/validate-docs.mjs index 54dee2f..f26e3c2 100644 --- a/scripts/validate-docs.mjs +++ b/scripts/validate-docs.mjs @@ -17,6 +17,11 @@ const pages = [ const locales = ["ja", "zh", "ko"] const docsRoot = path.resolve("docs") +const packageMetadata = JSON.parse(await readFile(path.resolve("package.json"), "utf8")) + +if (packageMetadata.dependencies["@stack-sh/engine"] !== "0.3.0") { + throw new Error("Documentation must use the exact @stack-sh/engine 0.3.0 release") +} const [ playgroundLogo, @@ -142,6 +147,20 @@ for (const locale of locales) { } const corpus = [...englishPages.values()].join("\n") +const coreIconIds = [ + "api", + "web", + "mobile", + "desktop", + "server", + "container", + "cluster", + "cloud", + "scheduler", + "webhook", + "identity", + "observability", +] const requiredTerms = [ "stack 1.0", "actor", @@ -173,4 +192,20 @@ 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/themes-and-icons.md") + const source = await readFile(page, "utf8") + const componentLocale = locale || "en" + + if (!source.includes(``)) { + throw new Error(`${componentLocale}/language/themes-and-icons.md is missing its icon gallery`) + } + + for (const iconId of coreIconIds) { + if (!source.includes(`| \`${iconId}\``)) { + throw new Error(`${locale || "en"}/language/themes-and-icons.md is missing ${iconId}`) + } + } +} + console.log(`Validated ${pages.length * (locales.length + 1)} documentation pages.`) diff --git a/src/App.test.tsx b/src/App.test.tsx index 122a0a1..cb62ce6 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -19,10 +19,10 @@ vi.mock("@stack-sh/engine", () => ({ import App from "./App" const metadata = { - engineVersion: "0.2.0", + engineVersion: "0.3.0", languageVersion: { major: 1, minor: 0 }, themeCatalogRevision: "sha256:test", - themeCatalogVersion: "0.1.0", + themeCatalogVersion: "0.2.0", } const diagnostic = { @@ -65,6 +65,7 @@ describe("Stack Playground", () => { expect(await screen.findByAltText("Rendered Stack architecture diagram")).toBeInTheDocument() expect(screen.getByText("Render completed")).toBeInTheDocument() expect(engine.render).toHaveBeenCalledOnce() + expect(engine.render).toHaveBeenCalledWith(expect.stringContaining('icon "web"')) }) it("formats the source and refreshes the preview", async () => { diff --git a/src/lib/example.ts b/src/lib/example.ts index fb04118..bd8db85 100644 --- a/src/lib/example.ts +++ b/src/lib/example.ts @@ -27,7 +27,7 @@ diagram "Commerce platform" { node storefront "Storefront" { kind client - icon "kind-client" + icon "web" detail "Web application" }