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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ Formatting and linting use Oxfmt and Oxlint. The production build compiles the R

CI additionally builds the Stack CLI revision pinned in [`scripts/docs-validation.config.mjs`](./scripts/docs-validation.config.mjs) with its minimum supported Rust version, then runs `npm run docs:smoke` through `STACK_CLI_BIN`. This checks the documented version, help surface, and safe executable examples without adding Rust to normal Web builds. Intentional locale or execution differences must be declared in the same configuration with a non-empty reason; the validator rejects stale exceptions.

The example gallery is generated from the public specification commit pinned in [`scripts/example-corpus.config.mjs`](./scripts/example-corpus.config.mjs). [`example-corpus`](./example-corpus) is a hermetic snapshot for the Playground and documentation build; CI checks every catalog, schema, and `.stack` source byte against the pinned provider commit. `npm run examples:check` checks and renders all examples, resolves every namespaced icon against the published provider catalog, and detects stale SVG thumbnails. To intentionally advance the corpus, check out the new pinned specification revision, run `STACK_SPECIFICATION_ROOT=/path/to/specification npm run examples:sync`, and then run `npm run examples:generate`.
The example gallery loads canonical `.stack` sources from the public specification commit pinned in [`scripts/example-corpus.config.mjs`](./scripts/example-corpus.config.mjs). [`example-corpus`](./example-corpus) is a hermetic source snapshot shared with the Playground; CI checks every catalog, schema, and `.stack` source byte against the pinned provider commit. Previews render on the visitor's device with the same pinned WASM engine and adapter as the Playground. They are not checked-in or build-generated SVGs, so updating the site's engine also updates its example rendering. The browser loads examples near the viewport, displays engine SVG as a Blob-backed image, and releases image URLs on navigation. A source link remains available without JavaScript; operational failures offer retry, and engine diagnostics are shown separately. Provider artwork is not fetched or redistributed by the gallery: examples use the engine's fallback shapes unless the caller explicitly supplies icon packs.

`npm run examples:check` checks and renders all examples in memory, resolves every namespaced icon against the published provider catalog, and checks safe SVG output without writing images. To intentionally advance the corpus, check out the new pinned specification revision, run `STACK_SPECIFICATION_ROOT=/path/to/specification npm run examples:sync`, then run `npm run examples:check`. Approved engine regression snapshots are separate test artifacts, not documentation previews.

## Brand mark

Expand Down
14 changes: 7 additions & 7 deletions docs/.vitepress/theme/components/ExampleGallery.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import corpus from "../../../../example-corpus/catalog.json"
import { exampleCorpusSource } from "../../../../scripts/example-corpus.config.mjs"
import ExamplePreview from "./ExamplePreview.vue"

type Locale = "en" | "ja" | "zh" | "ko"
type LearningStage = "starter" | "intermediate" | "advanced"
Expand Down Expand Up @@ -84,13 +85,12 @@ function featureLabel(feature: string) {
class="stack-example-card"
:aria-labelledby="`example-${example.id}`"
>
<a class="stack-example-card__preview" :href="sourceUrl(example.source)">
<img
:src="`/docs/examples/${example.id}.svg`"
:alt="example.thumbnail.alt"
loading="lazy"
/>
</a>
<ExamplePreview
:source="example.source"
:source-url="sourceUrl(example.source)"
:alt="example.thumbnail.alt"
:locale="locale"
/>
<div class="stack-example-card__body">
<div class="stack-example-card__heading">
<h3 :id="`example-${example.id}`">{{ example.title }}</h3>
Expand Down
119 changes: 119 additions & 0 deletions docs/.vitepress/theme/components/ExamplePreview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from "vue"
import { createExamplePreview, type ExamplePreviewState } from "../../../../src/lib/example-preview"
import { renderExample } from "../../../../src/lib/render-example"

const props = defineProps<{
source: string
sourceUrl: string
alt: string
locale: "en" | "ja" | "zh" | "ko"
}>()
const labels = {
en: {
loading: "Rendering this example on your device…",
unavailable: "The preview could not be loaded.",
errors: "This example reported rendering errors.",
diagnostics: "Rendering diagnostics (provider icons may use fallback shapes)",
retry: "Retry preview",
source: "View .stack source",
noScript: "Enable JavaScript to render this example, or read its .stack source.",
},
ja: {
loading: "この端末で作例を描画しています…",
unavailable: "プレビューを読み込めませんでした。",
errors: "作例の描画でエラーが報告されました。",
diagnostics: "描画の診断(Provider iconは代替図形になる場合があります)",
retry: "プレビューを再試行",
source: ".stackソースを見る",
noScript: "JavaScriptを有効にして作例を描画するか、.stackソースをご覧ください。",
},
zh: {
loading: "正在您的设备上渲染此示例…",
unavailable: "无法加载预览。",
errors: "此示例报告了渲染错误。",
diagnostics: "渲染诊断(服务商图标可能显示为替代图形)",
retry: "重试预览",
source: "查看 .stack 源码",
noScript: "请启用 JavaScript 渲染此示例,或阅读其 .stack 源码。",
},
ko: {
loading: "이 기기에서 예제를 렌더링하고 있습니다…",
unavailable: "미리보기를 불러올 수 없습니다.",
errors: "이 예제에서 렌더링 오류가 보고되었습니다.",
diagnostics: "렌더링 진단 (제공자 아이콘이 대체 도형으로 표시될 수 있음)",
retry: "미리보기 다시 시도",
source: ".stack 소스 보기",
noScript: "JavaScript를 활성화하여 예제를 렌더링하거나 .stack 소스를 읽어 보세요.",
},
} as const
const text = computed(() => labels[props.locale])
const element = ref<HTMLElement>()
const state = shallowRef<ExamplePreviewState>({ status: "idle" })
const preview = createExamplePreview(renderExample, (next) => (state.value = next))
const diagnostics = computed(() => ("result" in state.value ? state.value.result.diagnostics : []))
let observer: IntersectionObserver | undefined
let activated = false

function load() {
activated = true
observer?.disconnect()
void preview.load(props.source)
}

onMounted(() => {
if (!("IntersectionObserver" in window)) {
load()
return
}
observer = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) load()
},
{ rootMargin: "200px" },
)
if (element.value) observer.observe(element.value)
})
watch(
() => props.source,
() => {
if (activated) load()
},
)
onUnmounted(() => {
observer?.disconnect()
preview.dispose()
})
</script>

<template>
<div ref="element" class="stack-example-preview" :data-preview-status="state.status">
<div class="stack-example-preview__image" :aria-busy="state.status === 'loading'">
<a v-if="state.status === 'ready'" :href="sourceUrl">
<img :src="state.url" :alt="alt" @error="state = { status: 'unavailable' }" />
</a>
<div v-else class="stack-example-preview__fallback">
<p v-if="state.status === 'loading'" role="status">{{ text.loading }}</p>
<p v-else-if="state.status === 'unavailable'" role="status">{{ text.unavailable }}</p>
<p v-else-if="state.status === 'diagnostics'" role="status">{{ text.errors }}</p>
<a :href="sourceUrl">{{ text.source }}</a>
<button
v-if="state.status === 'unavailable' || state.status === 'diagnostics'"
type="button"
@click="load"
>
{{ text.retry }}
</button>
<noscript>{{ text.noScript }}</noscript>
</div>
</div>
<details v-if="diagnostics.length" class="stack-example-preview__diagnostics">
<summary>{{ text.diagnostics }}</summary>
<ul>
<li v-for="(diagnostic, index) in diagnostics" :key="index">
<code>{{ diagnostic.code }}</code> {{ diagnostic.message }}
</li>
</ul>
</details>
</div>
</template>
31 changes: 29 additions & 2 deletions docs/.vitepress/theme/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ body {
grid-template-rows: auto 1fr;
}

.stack-example-card__preview {
.stack-example-preview__image {
display: grid;
min-height: 12rem;
padding: 1rem;
Expand All @@ -641,14 +641,41 @@ body {
place-items: center;
}

.stack-example-card__preview img {
.stack-example-preview__image a {
width: 100%;
}

.stack-example-preview__image img {
display: block;
width: 100%;
height: 11rem;
margin: 0;
object-fit: contain;
}

.stack-example-preview__fallback {
display: grid;
align-content: center;
gap: 0.5rem;
width: 100%;
height: 11rem;
overflow: auto;
text-align: center;
}

.stack-example-preview__fallback button {
padding: 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 0.375rem;
color: var(--vp-c-text-1);
}

.stack-example-preview__diagnostics {
padding: 0.5rem 1rem;
font-size: 0.75rem;
overflow-wrap: anywhere;
}

.stack-example-card__body {
display: flex;
min-width: 0;
Expand Down
47 changes: 0 additions & 47 deletions docs/public/examples/application-and-data.svg

This file was deleted.

Loading