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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ Authentication, persistence, collaboration, and paid themes remain outside this

The editor keeps a native textarea as the input surface and layers Shiki presentation behind it. Highlighting consumes the raw `@stack-sh/language/grammar` export and does not determine whether source is valid. The Web-owned Shiki themes map grammar scopes to accessible light and dark colors. Validation and diagnostic guidance continue to come only from `@stack-sh/engine`.

Language intelligence runs locally through `@stack-sh/engine` 0.7.0 WebAssembly; it does not start an LSP server or send source to a server. Suggestions follow typing or `Ctrl Space`; use arrow keys to choose, `Enter` or `Tab` to accept, and `Escape` to dismiss. The Context panel follows the caret or pointer. Imported provider packs supply their icon catalog to the same engine APIs. The adapter converts browser UTF-16 selections to engine source positions, rejects stale document results, and suspends completion during IME composition. Diagnostics and preview refresh from the same source snapshot after editing, while explicit actions remain available.
Language intelligence runs locally through the pinned `@stack-sh/engine` WebAssembly package; it does not start an LSP server or send source to a server. Suggestions follow typing or `Ctrl Space`; use arrow keys to choose, `Enter` or `Tab` to accept, and `Escape` to dismiss. The Context panel follows the caret or pointer. Imported provider packs supply their icon catalog to the same engine APIs. The adapter converts browser UTF-16 selections to engine source positions, rejects stale document results, and suspends completion during IME composition. Diagnostics and preview refresh from the same source snapshot after editing, while explicit actions remain available.

## License

Expand Down
44 changes: 3 additions & 41 deletions docs/.vitepress/theme/components/ExampleGallery.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,57 +12,39 @@ const stages: LearningStage[] = ["starter", "intermediate", "advanced"]
const labels = {
en: {
stage: { starter: "Start small", intermediate: "Build fluency", advanced: "Model systems" },
packs: "Icon packs",
core: "Built-in icons",
expected: "Expected output",
structure: { nodes: "nodes", groups: "groups", edges: "edges" },
features: "Syntax features",
source: "View canonical .stack source",
contract: "Pinned source",
},
ja: {
stage: { starter: "小さく始める", intermediate: "表現を広げる", advanced: "Systemを描く" },
packs: "Icon pack",
core: "組み込みicon",
expected: "期待する出力",
structure: { nodes: "node", groups: "group", edges: "edge" },
features: "Syntax feature",
source: "Canonical .stack sourceを見る",
contract: "Pin済みsource",
},
zh: {
stage: { starter: "从小处开始", intermediate: "扩展表达", advanced: "描述系统" },
packs: "图标包",
core: "内置图标",
expected: "预期输出",
structure: { nodes: "节点", groups: "分组", edges: "连线" },
features: "语法特性",
source: "查看规范 .stack 源码",
contract: "固定源版本",
},
ko: {
stage: { starter: "작게 시작하기", intermediate: "표현 넓히기", advanced: "시스템 모델링" },
packs: "아이콘 팩",
core: "기본 아이콘",
expected: "예상 출력",
structure: { nodes: "노드", groups: "그룹", edges: "엣지" },
features: "문법 기능",
source: "Canonical .stack 소스 보기",
contract: "고정된 소스",
},
} as const

function examplesFor(stage: LearningStage) {
return corpus.examples.filter((example) => example.learningStage === stage)
return corpus.examples.filter(
(example) => example.learningStage === stage && example.providers.length === 0,
)
}

function sourceUrl(source: string) {
return `https://raw.githubusercontent.com/${exampleCorpusSource.repository}/${exampleCorpusSource.revision}/examples/${source}`
}

function featureLabel(feature: string) {
return feature.replaceAll("-", " ")
}
</script>

<template>
Expand Down Expand Up @@ -98,33 +80,13 @@ function featureLabel(feature: string) {
</div>
<p>{{ example.summary }}</p>

<div class="stack-example-card__meta">
<div>
<strong>{{ labels[locale].packs }}</strong>
<span>{{ example.providers.join(", ") || labels[locale].core }}</span>
</div>
<div>
<strong>{{ labels[locale].expected }}</strong>
<span>{{ example.expected.description }}</span>
</div>
</div>

<dl class="stack-example-card__counts">
<div v-for="field in ['nodes', 'groups', 'edges'] as const" :key="field">
<dt>{{ labels[locale].structure[field] }}</dt>
<dd>{{ example.expected[field] }}</dd>
</div>
</dl>

<details class="stack-example-card__features">
<summary>{{ labels[locale].features }}</summary>
<ul>
<li v-for="feature in example.features" :key="feature">
{{ featureLabel(feature) }}
</li>
</ul>
</details>

<a class="stack-example-card__source" :href="sourceUrl(example.source)">
{{ labels[locale].source }}
</a>
Expand Down
56 changes: 56 additions & 0 deletions docs/.vitepress/theme/components/ExampleLightbox.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from "vue"
import { createImageDialog } from "../../../../scripts/image-dialog.mjs"

const props = defineProps<{ url: string; alt: string; locale: "en" | "ja" | "zh" | "ko" }>()
const labels = {
en: { title: "Expanded diagram", close: "Close", actual: "Actual size", fit: "Fit to screen" },
ja: { title: "作例を拡大", close: "閉じる", actual: "実寸で見る", fit: "画面に合わせる" },
zh: { title: "放大图表", close: "关闭", actual: "实际大小", fit: "适应屏幕" },
ko: { title: "다이어그램 확대", close: "닫기", actual: "실제 크기", fit: "화면에 맞추기" },
} as const
const text = computed(() => labels[props.locale])
const dialog = ref<HTMLDialogElement>()
const actualSize = ref(false)
let controller: ReturnType<typeof createImageDialog> | undefined
onMounted(() => {
controller = createImageDialog(dialog.value!, () => (actualSize.value = false))
})
watch(
() => props.url,
() => controller?.close(),
)
onUnmounted(() => controller?.dispose())
function backdrop(event: MouseEvent) {
if (event.target !== dialog.value) return
const bounds = dialog.value!.getBoundingClientRect()
if (
event.clientX < bounds.left ||
event.clientX > bounds.right ||
event.clientY < bounds.top ||
event.clientY > bounds.bottom
)
controller?.close()
}
defineExpose({ open: () => controller?.open() })
</script>

<template>
<dialog ref="dialog" class="stack-example-lightbox" :aria-label="text.title" @click="backdrop">
<header class="stack-example-lightbox__toolbar">
<strong>{{ text.title }}</strong>
<button type="button" :aria-pressed="actualSize" @click="actualSize = !actualSize">
{{ actualSize ? text.fit : text.actual }}
</button>
<button type="button" autofocus @click="controller?.close()">{{ text.close }}</button>
</header>
<div
class="stack-example-lightbox__canvas"
:class="{ 'is-actual-size': actualSize }"
tabindex="0"
:aria-label="alt"
>
<img :src="url" :alt="alt" />
</div>
</dialog>
</template>
25 changes: 23 additions & 2 deletions docs/.vitepress/theme/components/ExamplePreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
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"
import ExampleLightbox from "./ExampleLightbox.vue"

const props = defineProps<{
source: string
Expand All @@ -11,6 +12,7 @@ const props = defineProps<{
}>()
const labels = {
en: {
enlarge: "Enlarge diagram",
loading: "Rendering this example on your device…",
unavailable: "The preview could not be loaded.",
errors: "This example reported rendering errors.",
Expand All @@ -20,6 +22,7 @@ const labels = {
noScript: "Enable JavaScript to render this example, or read its .stack source.",
},
ja: {
enlarge: "作例を拡大",
loading: "この端末で作例を描画しています…",
unavailable: "プレビューを読み込めませんでした。",
errors: "作例の描画でエラーが報告されました。",
Expand All @@ -29,6 +32,7 @@ const labels = {
noScript: "JavaScriptを有効にして作例を描画するか、.stackソースをご覧ください。",
},
zh: {
enlarge: "放大图表",
loading: "正在您的设备上渲染此示例…",
unavailable: "无法加载预览。",
errors: "此示例报告了渲染错误。",
Expand All @@ -38,6 +42,7 @@ const labels = {
noScript: "请启用 JavaScript 渲染此示例,或阅读其 .stack 源码。",
},
ko: {
enlarge: "다이어그램 확대",
loading: "이 기기에서 예제를 렌더링하고 있습니다…",
unavailable: "미리보기를 불러올 수 없습니다.",
errors: "이 예제에서 렌더링 오류가 보고되었습니다.",
Expand All @@ -49,6 +54,7 @@ const labels = {
} as const
const text = computed(() => labels[props.locale])
const element = ref<HTMLElement>()
const lightbox = ref<InstanceType<typeof ExampleLightbox>>()
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 : []))
Expand Down Expand Up @@ -89,9 +95,17 @@ onUnmounted(() => {
<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">
<button
v-if="state.status === 'ready'"
type="button"
class="stack-example-preview__enlarge"
aria-haspopup="dialog"
:aria-label="`${text.enlarge}: ${alt}`"
@click="lightbox?.open()"
>
<img :src="state.url" :alt="alt" @error="state = { status: 'unavailable' }" />
</a>
<span class="stack-example-preview__hint">{{ text.enlarge }} ↗</span>
</button>
<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>
Expand All @@ -115,5 +129,12 @@ onUnmounted(() => {
</li>
</ul>
</details>
<ExampleLightbox
v-if="state.status === 'ready'"
ref="lightbox"
:url="state.url"
:alt="alt"
:locale="locale"
/>
</div>
</template>
Loading