Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f832c765b | |||
| 904bafeb16 | |||
| 0d0cf1e65c | |||
| a14d225f0f | |||
| 8fc933b9db | |||
| 8600d7e5d5 | |||
| bdfce1ea7d | |||
| 8f1da1831b | |||
| 476173b6b5 | |||
| 19519dd5ea | |||
| 04b97cc2b7 | |||
| 8014e796a4 | |||
| a59015a246 | |||
| ad6fa36f3e |
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Windows canvas smoke under Wine.
|
||||
#
|
||||
# Exercises the Windows gpu_surface software path (src/platform/windows/
|
||||
# webview2_host.cpp: child HWND, WM_TIMER frame events, SetDIBitsToDevice
|
||||
# blits) without Windows hardware: cross-compiles examples/ui-inbox for
|
||||
# Exercises the Windows gpu_surface Direct2D path (src/platform/windows/
|
||||
# webview2_host.cpp: child HWND, WM_TIMER frame events, retained binary
|
||||
# packets) under Wine: cross-compiles examples/ui-inbox for
|
||||
# x86_64-windows-gnu, runs the .exe under Xvfb + Wine, and asserts against
|
||||
# the automation snapshot:
|
||||
#
|
||||
# 1. snapshot ready=true (app booted, automation server live)
|
||||
# 2. gpu_backend=software (the SetDIBitsToDevice path is active)
|
||||
# 2. gpu_backend=direct2d (the retained packet path is active)
|
||||
# 3. gpu_nonblank=true (real pixels were presented)
|
||||
# 4. widget-click "Add task" -> '4 open' (automation input mutates state)
|
||||
# 5. real X11 click + typing lands in the draft textbox (XTEST -> Wine ->
|
||||
@@ -116,9 +116,9 @@ app_pid=$!
|
||||
poll 180 'ready=true' || fail "snapshot never became ready"
|
||||
echo "== ready: $(head -1 "$snap" | cut -d'|' -f1)"
|
||||
|
||||
# ---- 2 + 3: software backend presented non-blank pixels --------------------
|
||||
# ---- 2 + 3: Direct2D backend presented non-blank pixels --------------------
|
||||
poll 60 'gpu_nonblank=true' || fail "gpu_nonblank never became true"
|
||||
poll 10 'gpu_backend=software' || fail "gpu_backend is not software"
|
||||
poll 10 'gpu_backend=direct2d' || fail "gpu_backend is not direct2d"
|
||||
echo "== canvas: $(grep -o 'gpu_backend=[a-z]*' "$snap" | head -1)" \
|
||||
"$(grep -o 'gpu_nonblank=[a-z]*' "$snap" | head -1)" \
|
||||
"$(grep -o 'gpu_sample=0x[0-9a-f]*' "$snap" | head -1)" \
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
# the app's trace log:
|
||||
#
|
||||
# 1. snapshot ready=true (app booted, automation server live)
|
||||
# 2. gpu_backend=software + nonblank (the canvas presented real pixels)
|
||||
# 2. gpu_backend=direct2d + nonblank (the canvas presented real pixels)
|
||||
# 3. widget-click "Start stream" (fx.spawn launches cmd.exe under
|
||||
# Wine; streamed lines land in the
|
||||
# model and grow the snapshot)
|
||||
@@ -118,9 +118,9 @@ app_pid=$!
|
||||
poll 180 'ready=true' || fail "snapshot never became ready"
|
||||
echo "== ready: $(head -1 "$snap" | cut -d'|' -f1)"
|
||||
|
||||
# ---- 2: software backend presented non-blank pixels ------------------------
|
||||
# ---- 2: Direct2D backend presented non-blank pixels ------------------------
|
||||
poll 60 'gpu_nonblank=true' || fail "gpu_nonblank never became true"
|
||||
grep -q 'gpu_backend=software' "$snap" || fail "gpu_backend is not software"
|
||||
grep -q 'gpu_backend=direct2d' "$snap" || fail "gpu_backend is not direct2d"
|
||||
echo "== canvas: $(grep -o 'gpu_backend=[a-z]*' "$snap" | head -1)" \
|
||||
"$(grep -o 'gpu_nonblank=[a-z]*' "$snap" | head -1)"
|
||||
grep -q 'idle' "$snap" || fail "probe did not start idle"
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -27,12 +27,60 @@ jobs:
|
||||
- run: zig build test
|
||||
- run: zig build validate
|
||||
|
||||
compiled-core-parity:
|
||||
name: Compiled-Core Parity
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
# The transpiled lane of every pairing runs the repo's own transpiler under node at build time; it needs its installed dependency.
|
||||
- run: npm ci --prefix packages/core
|
||||
# The external core compiler, at the release the profiles' determinism fence tables are pinned to. tests/compiled-core/core_compiler_pin is the ONE place the pin lives — build_core.sh refuses any other release, so a bump is a one-line change there and this step follows.
|
||||
- name: Install the external core compiler
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pin="$(cat tests/compiled-core/core_compiler_pin)"
|
||||
npm install --prefix .zig-cache/core-compiler "scriptc@${pin}"
|
||||
compiler="$PWD/.zig-cache/core-compiler/node_modules/.bin/scriptc"
|
||||
test "$("$compiler" -v)" = "$pin"
|
||||
echo "NATIVE_SDK_CORE_COMPILER=$compiler" >> "$GITHUB_ENV"
|
||||
# Per-fixture contract artifacts the external compile consumes: the effective sidecar plus its generated entry module and compiler profile, under zig-out/core-contracts.
|
||||
- run: zig build stage-core-contracts
|
||||
# Determinism-fence negative control: the pristine markup fixture compiles and its co-emitted sidecar attests deterministic: true, then one injected ambient read (Date.now() in update) must be refused by the profile's fences — proving the fences fire, not merely that clean cores pass under them.
|
||||
- name: Determinism fences fire (negative control)
|
||||
run: tests/compiled-core/fence_check.sh .zig-cache/fence-check
|
||||
- name: Build the five fixture cores
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for fixture in host-fixture soundboard system-monitor ai-chat markup; do
|
||||
tests/compiled-core/build_core.sh "$fixture" ".zig-cache/compiled-cores/$fixture"
|
||||
done
|
||||
# Each fixture app's OWN e2e battery over a paired core — the transpiled lane vs the compiled archive, byte-compared at every seam. Locally this step is env-gated (no external compiler on a stock checkout); this job is where it always runs. Serial (-j1): the soundboard battery measures wall-clock dispatch budgets, and five test binaries racing on a two-core runner turn scheduler contention into failures the budgets were never meant to catch.
|
||||
- name: Run the compiled-core parity battery
|
||||
run: zig build test-compiled-core-parity -j1
|
||||
env:
|
||||
NATIVE_SDK_EXTERNAL_CORE_ARCHIVE_HOST: ${{ github.workspace }}/.zig-cache/compiled-cores/host-fixture/libhost_fixture_core.a
|
||||
NATIVE_SDK_EXTERNAL_CORE_SIDECAR_HOST: ${{ github.workspace }}/.zig-cache/compiled-cores/host-fixture/core.contract.json
|
||||
NATIVE_SDK_EXTERNAL_CORE_ARCHIVE_SOUNDBOARD: ${{ github.workspace }}/.zig-cache/compiled-cores/soundboard/libsoundboard_core.a
|
||||
NATIVE_SDK_EXTERNAL_CORE_SIDECAR_SOUNDBOARD: ${{ github.workspace }}/.zig-cache/compiled-cores/soundboard/core.contract.json
|
||||
NATIVE_SDK_EXTERNAL_CORE_ARCHIVE_SYSTEM_MONITOR: ${{ github.workspace }}/.zig-cache/compiled-cores/system-monitor/libsystem_monitor_core.a
|
||||
NATIVE_SDK_EXTERNAL_CORE_SIDECAR_SYSTEM_MONITOR: ${{ github.workspace }}/.zig-cache/compiled-cores/system-monitor/core.contract.json
|
||||
NATIVE_SDK_EXTERNAL_CORE_ARCHIVE_AI_CHAT: ${{ github.workspace }}/.zig-cache/compiled-cores/ai-chat/libai_chat_core.a
|
||||
NATIVE_SDK_EXTERNAL_CORE_SIDECAR_AI_CHAT: ${{ github.workspace }}/.zig-cache/compiled-cores/ai-chat/core.contract.json
|
||||
NATIVE_SDK_EXTERNAL_CORE_ARCHIVE_MARKUP: ${{ github.workspace }}/.zig-cache/compiled-cores/markup/libmarkup_core.a
|
||||
NATIVE_SDK_EXTERNAL_CORE_SIDECAR_MARKUP: ${{ github.workspace }}/.zig-cache/compiled-cores/markup/core.contract.json
|
||||
|
||||
macos-webview:
|
||||
name: macOS WebView
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
- run: zig build test-webview-system-link
|
||||
@@ -59,7 +107,7 @@ jobs:
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
# Percentile perf check: 5 cold launches asserting p90
|
||||
@@ -81,7 +129,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
- name: Install WebKitGTK dependencies
|
||||
@@ -102,7 +150,7 @@ jobs:
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
# Builds the WebView example with the system engine, compiling the
|
||||
@@ -112,6 +160,10 @@ jobs:
|
||||
# include path, or a conformance error in the embedded layer — is a
|
||||
# compile failure here, not a silent WebViewNotFound at runtime.
|
||||
- run: zig build test-webview-system-link -Dplatform=windows
|
||||
# Effects.spawn is a pipe-backed background transport. Run its
|
||||
# Windows-only PowerShell probe natively so removing CREATE_NO_WINDOW
|
||||
# cannot leave the platform-neutral and Wine lanes green.
|
||||
- run: zig build test-windows-effects-no-console
|
||||
# The registered-font receipt, natively on Windows: runs the
|
||||
# font-registry suite — registration validation, the glyph-budget
|
||||
# gate, present/reference pixel parity, and the Chinese-receipt
|
||||
@@ -128,7 +180,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -167,7 +219,7 @@ jobs:
|
||||
step: test-examples-native-shard-4
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -186,7 +238,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
# Declare-to-use, proven on real Windows executables: the
|
||||
@@ -218,7 +270,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
# Deliberately NO libwebkitgtk-6.0-dev: ui-inbox declares no web
|
||||
@@ -244,7 +296,7 @@ jobs:
|
||||
# C diagnostics on failure, which is exactly the escalation this
|
||||
# step pins against. The throwaway cache dir keeps the compile
|
||||
# cold: on a cache hit zig replays nothing, stderr included, so a
|
||||
# warm cache (setup-zig restores one) would hide the diagnostics
|
||||
# warm compiler cache would hide the diagnostics
|
||||
# this step exists to catch.
|
||||
- name: WebKitGTK stub compile is diagnostic-free
|
||||
run: |
|
||||
@@ -300,7 +352,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -330,7 +382,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
- name: Install Wine, Xvfb, and xdotool
|
||||
@@ -352,8 +404,8 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
# Cross-compiles ui-inbox for x86_64-windows-gnu and drives the
|
||||
# gpu_surface software path (child HWND + WM_TIMER + SetDIBitsToDevice)
|
||||
# under Wine: snapshot ready, gpu_backend=software, gpu_nonblank=true,
|
||||
# retained Direct2D packet path (child HWND + WM_TIMER) under Wine:
|
||||
# snapshot ready, gpu_backend=direct2d, gpu_nonblank=true,
|
||||
# automation widget-click, and real XTEST pointer/keyboard input.
|
||||
# Wineprefix init happens inline in the script (measured 21s from
|
||||
# scratch in an ubuntu-24.04 container, so no cache step).
|
||||
@@ -366,7 +418,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
- name: Install Wine and Xvfb
|
||||
@@ -385,7 +437,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
- run: zig build test-examples-frontends
|
||||
@@ -395,7 +447,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
- run: zig build test-examples-mobile
|
||||
@@ -405,7 +457,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Zig
|
||||
uses: mlugg/setup-zig@v2
|
||||
uses: vercel-labs/setup-zig@v1
|
||||
with:
|
||||
version: 0.16.0
|
||||
|
||||
|
||||
@@ -2,12 +2,57 @@
|
||||
|
||||
All notable changes to the Native SDK (formerly zero-native) will be documented in this file.
|
||||
|
||||
## 0.7.0
|
||||
## 0.7.1
|
||||
|
||||
<!-- release:start -->
|
||||
|
||||
### New Features
|
||||
|
||||
- **Declarative folder-to-code editor example**: `examples/code-editor` authors its complete view in hot-reloadable `.native` markup, unifies its titlebar, file pane, tab-strip canvas, and editor background, centers the opened folder name beside a trailing ghost Save icon in a custom titlebar, opens or replaces the focused window's folder with Cmd+O, creates independent editor windows with Cmd+N, builds a clean bounded folders-first disclosure tree with outline-free selection-only Up/Down navigation, leaf-to-parent Left movement, Left/Right expansion, in-place disk-backed Enter rename, Cmd+Enter permanent tabs, and folder focus independent from the active editor, and presents editable syntax-highlighted files (including `.mjs` and large practical sources) in a resizable second pane with flat VS Code-style tabs whose active tab has no top accent and breaks the baseline to meet the editor, replaceable italic previews that pin when double-clicked, dirty dots, active/hover close buttons, native Close/Close Others tab menus, wrapping Cmd+Shift+[/] tab cycling, Cmd+W tab-or-empty-window closing, and serialized disk-backed Save/Cmd+S.
|
||||
- **Generated compiled-core facade**: `corewire --facade` now emits the complete compiler entry and matching profile from the contract sidecar, including explicit `--f64-slot` demotions, authored type provenance, and signed or unsigned integer proofs at every host ingress.
|
||||
- **Facade contract hardening**: generated entries preserve subdirectory module paths, reconstruct private reachable types without invalid imports, preserve Model-first resolution for homonymous unbound bindings, decode optional and composite record fields with a running cursor, prove nullable integer helpers, handle signed and unsigned text-selection sentinels consistently, and refuse legacy sidecars that lack the authored facts a facade requires.
|
||||
- **Effective sidecar projection**: `corewire --effective-sidecar` emits the contract after explicit slot demotions, and staged facade/profile/sidecar triples now describe one compiled layout.
|
||||
- **Editable highlighted code**: `ui.code` and `<code>` keep their read-only default, while `editable` plus `on-input` opts into a syntax-colored multiline editor in both retained and direct rendering, with selection, caret-row highlighting, IME, clipboard, undo/redo, indentation-aware Tab input (tabs or inferred 2–8-space widths, defaulting to two spaces), and no textarea chrome.
|
||||
- **Markdown source highlighting**: `markdown`/`md` joins the code lexer names with themed headings, lists, emphasis, links, inline and fenced code, and comments; the code-editor example selects it for Markdown files.
|
||||
- **Stable line-number gutter**: numbered code reserves at least three marker columns, so short files keep a useful gutter while larger line counts still expand it.
|
||||
- **Double-click messages in Native markup**: `on-double-press` exposes the canvas runtime's additive double-click channel to `.native` views, so the first click can select or preview and the second can perform or pin without a timer. Multi-click chains stay scoped to one control and physical pointer, and a third click returns to the ordinary press action instead of repeating the double action.
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Composable code presentation**: `ui.code` and `<code>` now provide bare highlighted content without their own background, border, radius, shadow, or padding; wrap them in a panel or card when surface chrome is wanted. An enabled line-number gutter remains opaque while horizontally scrolling so source glyphs cannot clash with its pinned markers.
|
||||
- **Flat tree keyboard hierarchy**: `treeitem` rows can declare a one-based `tree-level`, letting Left/Right find logical parents and children in loop-rendered flat trees, while `on-change` can keep arrow-key selection distinct from pointer activation.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Live code docs preview**: The Code component page now loads its real WASM-backed engine scene instead of silently remaining on the static screenshot fallback.
|
||||
- **Reliable large-code editing**: editable code now repaints only visible selected glyphs and caches longest-line width measurements, keeping large selections and steady-state no-wrap rendering inside bounded display-list and host-measurement budgets.
|
||||
- **Complete wrapped long lines**: scrolling a single logical line beyond 128 wrapped rows now pages its visible glyphs instead of leaving the remainder blank.
|
||||
- **Stable code-editor reads**: switching tabs no longer cancels a pinned file's load, and reopened secondary windows keep monotonic file-effect keys so late completions cannot populate a newer document.
|
||||
- **Unsaved-edit protection**: opening another folder or closing a secondary editor window now refuses while that window still has dirty documents.
|
||||
- **Steady editor tabs**: active and inactive tabs now share the same background and label alignment, so filenames no longer shift when selection changes.
|
||||
- **Balance explorer rows**: file-tree hover and selection backgrounds now keep even visual gutters beside the sidebar edge and split handle while preserving compact label alignment.
|
||||
- **Complete repository roots**: the explorer now indexes a folder when it expands instead of spending its bounded tree budget in an eager depth-first walk, so large subtrees cannot hide root files or unexplored sibling folders; `.next` and `.pnpm-store` remain visible but are not recursively indexed.
|
||||
- **Familiar file opening**: Command+Down Arrow now opens the selected tree file as a persistent tab; Command+Enter remains available to the focused control.
|
||||
- **Visible active tabs**: inactive tabs retain their bottom divider, and opening, clicking, or keyboard-cycling to a tab now minimally scrolls it into view horizontally without shifting an already visible tab.
|
||||
- **Distinct new windows**: Command+N now opens each editor window slightly down and to the right of the active window so the new window is immediately apparent.
|
||||
- **Clear empty-window title**: editor windows now show “Code Explorer” in the title bar until a folder is opened.
|
||||
- **Stable editable-code repainting**: syntax-highlighted editors now keep unique retained command IDs while edited text and highlighted spans occupy different runtime storage, preventing a selected editor from crashing when the app deactivates.
|
||||
- **Safe large widget text**: views keep their ordinary 64 KiB text pools inline and allocate practical source-file capacity only when a large layout or edit needs it, while edit, presentation, and context-menu workspaces stay off constrained native stacks and large single-line pastes continue stripping line breaks.
|
||||
- **Folder-only macOS open dialogs**: `allow_directories = true` now matches Linux and Windows by selecting directories rather than allowing files alongside them in AppKit and CEF hosts.
|
||||
- **Code-editor presentation polish**: JavaScript and TypeScript object keys and typed bindings now use the same syntax color as variables, while CSS declaration names retain their property color; numbered editors also use their full trailing width so fitting lines do not produce false horizontal scrolling.
|
||||
- **Complete JSX and TSX syntax highlighting**: JSX-family code blocks now combine JavaScript or TypeScript token coloring with JSX tags and attributes instead of treating the whole file as plain HTML outside `{…}` expressions.
|
||||
- **YAML syntax highlighting**: code surfaces, Markdown fences, and the code-editor example now recognize `yaml` and `yml`, coloring mapping keys, scalars, document markers, anchors, tags, and comments.
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
|
||||
<!-- release:end -->
|
||||
|
||||
## 0.7.0
|
||||
|
||||
### New Features
|
||||
|
||||
- **Code component**: `ui.code` and markup `<code>` render highlighted source with the Geist Code Block palette in both built-in themes, wrapping by default, opt-in logical line numbers, unwrapped horizontal scrolling, and vertical scrolling for height-constrained surfaces; Markdown fences share the same component.
|
||||
|
||||
### Bug Fixes
|
||||
@@ -19,8 +64,6 @@ All notable changes to the Native SDK (formerly zero-native) will be documented
|
||||
|
||||
- @ctate
|
||||
|
||||
<!-- release:end -->
|
||||
|
||||
## 0.6.3
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -517,13 +517,15 @@ pub fn build(b: *std.Build) void {
|
||||
test_step.dependOn(&battery_run.step);
|
||||
}
|
||||
// The corpus contract artifacts an external core toolchain
|
||||
// consumes: per fixture, the extracted contract sidecar and its
|
||||
// TypeScript facade projection under zig-out/core-contracts.
|
||||
const contracts_step = b.step("stage-core-contracts", "Install each ts-core fixture's contract sidecar and TypeScript facade projection under zig-out/core-contracts (requires node)");
|
||||
// consumes: per fixture, the extracted contract sidecar, the
|
||||
// generated entry module, and the compiler profile that builds
|
||||
// it, under zig-out/core-contracts.
|
||||
const contracts_step = b.step("stage-core-contracts", "Install each ts-core fixture's contract sidecar, generated entry module, and compiler profile under zig-out/core-contracts (requires node)");
|
||||
for (ts_core_artifacts.core_contracts) |contract| {
|
||||
const dir: std.Build.InstallDir = .{ .custom = b.fmt("core-contracts/{s}", .{contract.name}) };
|
||||
contracts_step.dependOn(&b.addInstallFileWithDir(contract.sidecar, dir, "core.contract.json").step);
|
||||
contracts_step.dependOn(&b.addInstallFileWithDir(contract.facade, dir, "core_facade.ts").step);
|
||||
contracts_step.dependOn(&b.addInstallFileWithDir(contract.profile, dir, "core_profile.json").step);
|
||||
}
|
||||
ts_core_e2e_step.dependOn(&host_e2e_run.step);
|
||||
ts_core_e2e_step.dependOn(&soundboard_e2e_run.step);
|
||||
@@ -564,6 +566,9 @@ pub fn build(b: *std.Build) void {
|
||||
.{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "\"gpu_surfaces\"" },
|
||||
.{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "\"gpuSurfaces\"" },
|
||||
.{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "gpuFirstFrameLatencyNs: number" },
|
||||
.{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "gpuBackend: NativeSdkGpuSurfaceBackend;" },
|
||||
.{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "export type NativeSdkGpuSurfaceBackendRequest = \"metal\" | \"software\";" },
|
||||
.{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "gpuBackend?: NativeSdkGpuSurfaceBackendRequest;" },
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-ts-toolchain-twins", "Verify the CLI's toolchain-resolution gate and its direct-`zig build` twin stay in lockstep (both resolve the aliased real compiler @typescript/old from packages/core — the same origin runtime imports it from — hold its resolved version against the manifest-read pin, never probe the unused @typescript/typescript6 wrapper, and teach instead of panicking)", &.{
|
||||
// The resolution twins probe the aliased REAL compiler
|
||||
@@ -628,19 +633,19 @@ pub fn build(b: *std.Build) void {
|
||||
.{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "close:function(options){return invoke('native-sdk.view.close',viewSelectorPayload(options))" },
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-docs-command-contracts", "Verify command docs match native view update contracts", &.{
|
||||
.{ .path = "docs/src/app/commands/page.mdx", .pattern = ".text = \"Refreshed\"" },
|
||||
.{ .path = "docs/src/app/commands/page.mdx", .pattern = "const commands = await window.zero.commands.list();" },
|
||||
.{ .path = "docs/src/app/docs/commands/page.mdx", .pattern = ".text = \"Refreshed\"" },
|
||||
.{ .path = "docs/src/app/docs/commands/page.mdx", .pattern = "const commands = await window.zero.commands.list();" },
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-docs-native-view-contracts", "Verify native surface docs describe view identity", &.{
|
||||
.{ .path = "docs/src/app/native-surfaces/page.mdx", .pattern = "ViewInfo.id" },
|
||||
.{ .path = "docs/src/app/native-surfaces/page.mdx", .pattern = "window.zero.views.update(\"status\"" },
|
||||
.{ .path = "docs/src/app/native-surfaces/page.mdx", .pattern = "first-frame latency budget" },
|
||||
.{ .path = "docs/src/app/docs/native-surfaces/page.mdx", .pattern = "ViewInfo.id" },
|
||||
.{ .path = "docs/src/app/docs/native-surfaces/page.mdx", .pattern = "window.zero.views.update(\"status\"" },
|
||||
.{ .path = "docs/src/app/docs/native-surfaces/page.mdx", .pattern = "first-frame latency budget" },
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-docs-shell-manifest-contracts", "Verify app.zon docs describe shell compatibility window labels", &.{
|
||||
.{ .path = "docs/src/app/app-zon/page.mdx", .pattern = "labels must stay unique across both lists" },
|
||||
.{ .path = "docs/src/app/docs/app-zon/page.mdx", .pattern = "labels must stay unique across both lists" },
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-docs-media-producer-contracts", "Verify the media producer docs' typed callback stays mirrored by the compile-shaped pin in media_surface_tests.zig", &.{
|
||||
.{ .path = "docs/src/app/media-producers/page.mdx", .pattern = "producer: media.MediaSurfaceProducer" },
|
||||
.{ .path = "docs/src/app/docs/media-producers/page.mdx", .pattern = "producer: media.MediaSurfaceProducer" },
|
||||
.{ .path = "src/runtime/media_surface_tests.zig", .pattern = "producer: media.MediaSurfaceProducer" },
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-session-replay-image-codec", "Verify the replay runner installs the host image codec headlessly (the desktop arms cannot link in unit tests; the null-fallback arm is covered in session_tests.zig)", &.{
|
||||
@@ -984,9 +989,37 @@ pub fn build(b: *std.Build) void {
|
||||
// this step until the encoder comment, the host decoder comment, and
|
||||
// the patterns below move with it.
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-wire-format-version-prose", "Verify wire-format version prose matches the packet version constant", &.{
|
||||
.{ .path = "src/primitives/canvas/serialization.zig", .pattern = "pub const binary_packet_version: u8 = 4;" },
|
||||
.{ .path = "src/primitives/canvas/serialization.zig", .pattern = "Compact binary gpu-surface packet encoding (wire format v4)." },
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "Compact binary gpu-surface packet decoding (wire format v4)." },
|
||||
.{ .path = "src/primitives/canvas/serialization.zig", .pattern = "pub const binary_packet_version: u8 = 5;" },
|
||||
.{ .path = "src/primitives/canvas/serialization.zig", .pattern = "Compact binary gpu-surface packet encoding (wire format v5)." },
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "Compact binary gpu-surface packet decoding (wire format v5)." },
|
||||
.{ .path = "src/platform/windows/gpu_surface_renderer.cpp", .pattern = "Compact binary gpu-surface packet decoding (wire format v5)." },
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-windows-gpu-packet-presenter", "Verify Windows uses retained Direct2D packets with recovery, bounded resources, and dirty-region pixel fallback", &.{
|
||||
.{ .path = "src/platform/windows/root.zig", .pattern = ".present_gpu_surface_packet_binary_fn = presentGpuSurfacePacketBinary" },
|
||||
.{ .path = "src/platform/windows/root.zig", .pattern = ".backend = if (event.gpu_backend == 1) .direct2d else .software" },
|
||||
.{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "view.gpu_surface->present(request, &info)" },
|
||||
.{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "InvalidateRect(view.hwnd, &info.dirty_rects[index], FALSE)" },
|
||||
.{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "for (size_t y_index = y0; y_index < y1; ++y_index)" },
|
||||
.{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "InvalidateRect(view.hwnd, partial_update ? &dirty_pixels : nullptr, FALSE)" },
|
||||
.{ .path = "src/platform/windows/gpu_surface_renderer.cpp", .pattern = "D2D1_RENDER_TARGET_TYPE_HARDWARE" },
|
||||
.{ .path = "src/platform/windows/gpu_surface_renderer.cpp", .pattern = "retained_commands_ = std::move(next_retained)" },
|
||||
.{ .path = "src/platform/windows/gpu_surface_renderer.cpp", .pattern = "PushAxisAlignedClip(d2dRect(requested)" },
|
||||
.{ .path = "src/platform/windows/gpu_surface_renderer.cpp", .pattern = "const float expansion = effect.spread + blur" },
|
||||
.{ .path = "src/platform/windows/gpu_surface_renderer.cpp", .pattern = "releaseImageBitmap(action.id)" },
|
||||
.{ .path = "src/platform/windows/gpu_surface_renderer.cpp", .pattern = "resumeAndDrawBlur" },
|
||||
.{ .path = "src/platform/windows/gpu_surface_renderer.cpp", .pattern = "const Rect target = blurTarget(*command, outer_clip)" },
|
||||
.{ .path = "src/platform/windows/gpu_surface_renderer.cpp", .pattern = "blur_snapshot_->CopyFromBitmap" },
|
||||
.{ .path = "src/platform/windows/gpu_surface_renderer.cpp", .pattern = "command.effect.blur * transformScale(command.transform)" },
|
||||
.{ .path = "src/platform/windows/gpu_surface_renderer.cpp", .pattern = "D2D1_EXTEND_MODE_CLAMP" },
|
||||
.{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "!GetClientRect(view.hwnd, &client) || !PtInRect(&client, sample)" },
|
||||
.{ .path = "src/platform/windows/gpu_surface_renderer.cpp", .pattern = "draw_line(text.text, text.origin.x, text.origin.y)" },
|
||||
.{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "gpuSurfaceUpdateRegionRects" },
|
||||
.{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "info.dirty_rect_count > 0" },
|
||||
.{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "view->gpu_force_full_repaint_pending = true" },
|
||||
.{ .path = "src/platform/windows/root.zig", .pattern = ".canvas_frame_full_repaint = event.force_full_repaint != 0" },
|
||||
.{ .path = ".github/scripts/windows-canvas-smoke.sh", .pattern = "gpu_backend=direct2d" },
|
||||
.{ .path = ".github/scripts/windows-effects-smoke.sh", .pattern = "gpu_backend=direct2d" },
|
||||
.{ .path = "build/app.zig", .pattern = "app_mod.linkSystemLibrary(\"d2d1\", .{})" },
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-packet-blur-effects", "Verify AppKit GPU packet presenter applies blur effects", &.{
|
||||
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkPacketApplyBlur" },
|
||||
@@ -1222,8 +1255,8 @@ pub fn build(b: *std.Build) void {
|
||||
.{ .path = "src/platform/macos/root.zig", .pattern = ".appearance_changed => state.emit" },
|
||||
});
|
||||
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-docs-builtin-bridge-policy", "Verify bridge policy docs include guarded dialog commands", &.{
|
||||
.{ .path = "docs/src/app/security/page.mdx", .pattern = ".{ .name = \"native-sdk.dialog.saveFile\"" },
|
||||
.{ .path = "docs/src/app/bridge/builtin-commands/page.mdx", .pattern = ".{ .name = \"native-sdk.dialog.saveFile\"" },
|
||||
.{ .path = "docs/src/app/docs/security/page.mdx", .pattern = ".{ .name = \"native-sdk.dialog.saveFile\"" },
|
||||
.{ .path = "docs/src/app/docs/bridge/builtin-commands/page.mdx", .pattern = ".{ .name = \"native-sdk.dialog.saveFile\"" },
|
||||
});
|
||||
|
||||
addTestStep(b, "test-geometry", "Run geometry module tests", geometry_tests);
|
||||
@@ -1248,6 +1281,12 @@ pub fn build(b: *std.Build) void {
|
||||
// on a Windows runner makes that a Windows-native receipt. The same
|
||||
// tests also run inside `zig build test` via the canvas-frame shard.
|
||||
addTestStep(b, "test-canvas-fonts", "Run the runtime font-registry tests (includes the registered-CJK Chinese receipt)", filteredTestArtifact(b, desktop_mod, "canvas-fonts-tests", &.{"runtime.canvas_font_tests.test"}));
|
||||
// The console-attachment regression is Windows-only, so the general
|
||||
// Linux `zig build test` lane can only compile and skip it. Expose the
|
||||
// exact test as a focused step for the real-Windows CI runner; this
|
||||
// keeps CREATE_NO_WINDOW behavior covered without making that runner
|
||||
// execute the entire framework runtime suite.
|
||||
addTestStep(b, "test-windows-effects-no-console", "Verify Windows effect spawns run without an attached console", filteredTestArtifact(b, desktop_mod, "windows-effects-no-console-tests", &.{"runtime.effects_tests.test.Windows background spawns run without an attached console"}));
|
||||
addTestStep(b, "test-automation-protocol", "Run automation protocol tests", automation_protocol_tests);
|
||||
addTestStep(b, "test-automation-cli", "Run native automate CLI tests", automation_cli_tests);
|
||||
addTestStep(b, "test-markup-cli", "Run native markup CLI tests", markup_cli_tests);
|
||||
@@ -1384,6 +1423,7 @@ pub fn build(b: *std.Build) void {
|
||||
addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-soundboard-ts", "Run soundboard-ts example tests", "examples/soundboard-ts", .managed),
|
||||
addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-deck", "Run deck example tests", "examples/deck", .managed),
|
||||
addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-markdown-viewer", "Run markdown viewer example tests", "examples/markdown-viewer", .managed),
|
||||
addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-code-editor", "Run code editor example tests", "examples/code-editor", .managed),
|
||||
addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-calculator", "Run calculator example tests", "examples/calculator", .managed),
|
||||
addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-notes", "Run notes example tests", "examples/notes", .managed),
|
||||
addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-split-collapse", "Run split collapse example tests", "examples/split-collapse", .managed),
|
||||
@@ -2009,7 +2049,13 @@ pub fn build(b: *std.Build) void {
|
||||
\\ ready_uptime="$(printf '%s\n' "$ready" | sed -n 's/.*runtime_uptime_ns=\([0-9][0-9]*\).*/\1/p')"
|
||||
\\ case "$ready_uptime" in ''|*[!0-9]*) echo "gpu-dashboard automation ready uptime was missing" >&2; exit 1 ;; esac
|
||||
\\ if [ "$ready_uptime" -le 0 ] || [ "$ready_uptime" -gt "$ready_budget_ns" ]; then echo "gpu-dashboard automation ready exceeded $ready_budget_ms ms: $ready_uptime ns" >&2; exit 1; fi
|
||||
\\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)"
|
||||
\\ attempts=0
|
||||
\\ while [ "$attempts" -lt 50 ]; do
|
||||
\\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)"
|
||||
\\ case "$snapshot" in *'window @w1 "Native SDK GPU Dashboard"'*'view @w1/dashboard-canvas kind=gpu_surface'*'accessibility_label="Native-rendered product dashboard canvas"'*) break ;; esac
|
||||
\\ attempts=$((attempts + 1))
|
||||
\\ sleep 0.1
|
||||
\\ done
|
||||
\\ case "$snapshot" in *'window @w1 "Native SDK GPU Dashboard"'*) ;; *) echo "gpu-dashboard window was missing from snapshot" >&2; exit 1 ;; esac
|
||||
\\ case "$snapshot" in *'view @w1/main kind=webview'*) echo "dashboard should not create an implicit main WebView" >&2; exit 1 ;; *) ;; esac
|
||||
\\ case "$snapshot" in *'source kind=html bytes=0'*) echo "dashboard should not publish an empty default WebView source" >&2; exit 1 ;; *) ;; esac
|
||||
@@ -2970,8 +3016,8 @@ const TsCoreE2eArtifacts = struct {
|
||||
/// — every battery skipped — otherwise.
|
||||
compiled_core_parity: []const CompiledCoreParity,
|
||||
/// Per-fixture contract artifacts for an external core toolchain:
|
||||
/// the extracted contract sidecar and its TypeScript facade
|
||||
/// projection, installed by the stage-core-contracts step.
|
||||
/// the effective contract sidecar and its TypeScript facade/profile
|
||||
/// projections, installed by the stage-core-contracts step.
|
||||
core_contracts: []const CoreContract,
|
||||
};
|
||||
|
||||
@@ -2984,6 +3030,7 @@ const CoreContract = struct {
|
||||
name: []const u8,
|
||||
sidecar: std.Build.LazyPath,
|
||||
facade: std.Build.LazyPath,
|
||||
profile: std.Build.LazyPath,
|
||||
};
|
||||
|
||||
/// One fixture's compiled-core supply: the archive link input(s) and
|
||||
@@ -3114,52 +3161,55 @@ fn tsCoreE2eArtifact(
|
||||
|
||||
const conformance_mod = module(b, target, optimize, "tests/sidecar/conformance_tests.zig");
|
||||
conformance_mod.addImport("native_sdk", desktop_mod);
|
||||
// The canonical value encoder the facade parity axis compares
|
||||
// The canonical value encoder the envelope and snapshot axes compare
|
||||
// against (the same module the generated shims stage).
|
||||
conformance_mod.addImport("corewire_rt", module(b, target, optimize, "tools/corewire/shim_rt.zig"));
|
||||
conformance_mod.addImport("ts_markup_core", markup_fixture_mod);
|
||||
conformance_mod.addImport("shim_markup_core", sidecarShimModule(b, target, optimize, corewire_exe, b.path("tests/sidecar/markup_fixture.contract.json")));
|
||||
conformance_mod.addImport("facade_markup_core", facadeCoreModule(b, target, optimize, node, corewire_exe, b.path("tests/sidecar/markup_fixture.contract.json")));
|
||||
// The integer-class fixture: a hand-written sidecar attesting mixed
|
||||
// i64/u64 slot classes, so the suite drives boundary and full-range
|
||||
// integer values through a generated mirror's decode paths and
|
||||
// holds the compiled facade's integer encoders to the same bytes.
|
||||
// integer values through a generated mirror's decode paths.
|
||||
conformance_mod.addImport("shim_integer_core", sidecarShimModule(b, target, optimize, corewire_exe, b.path("tests/sidecar/integer_fixture.contract.json")));
|
||||
conformance_mod.addImport("facade_integer_core", facadeCoreModule(b, target, optimize, node, corewire_exe, b.path("tests/sidecar/integer_fixture.contract.json")));
|
||||
const conformance_fixtures = [_]struct {
|
||||
ts_import: []const u8,
|
||||
shim_import: []const u8,
|
||||
facade_import: []const u8,
|
||||
contract_name: []const u8,
|
||||
core_mod: *std.Build.Module,
|
||||
entry: []const u8,
|
||||
/// Attested integer slots the compiled projection carries as f64
|
||||
/// (values that reach the f64-exact boundary have no honest i64
|
||||
/// declaration on that side).
|
||||
f64_slots: []const []const u8 = &.{},
|
||||
}{
|
||||
.{ .ts_import = "ts_host_core", .shim_import = "shim_host_core", .facade_import = "facade_host_core", .contract_name = "host-fixture", .core_mod = fixture_mod, .entry = "tests/ts-core/fixture.ts" },
|
||||
.{ .ts_import = "ts_soundboard_core", .shim_import = "shim_soundboard_core", .facade_import = "facade_soundboard_core", .contract_name = "soundboard", .core_mod = soundboard_core_mod, .entry = "examples/soundboard-ts/src/core.ts" },
|
||||
.{ .ts_import = "ts_monitor_core", .shim_import = "shim_monitor_core", .facade_import = "facade_monitor_core", .contract_name = "system-monitor", .core_mod = monitor_core_mod, .entry = "examples/system-monitor-ts/src/core.ts" },
|
||||
.{ .ts_import = "ts_ai_chat_core", .shim_import = "shim_ai_chat_core", .facade_import = "facade_ai_chat_core", .contract_name = "ai-chat", .core_mod = ai_chat_core_mod, .entry = "examples/ai-chat-ts/src/core.ts" },
|
||||
.{ .ts_import = "ts_host_core", .shim_import = "shim_host_core", .contract_name = "host-fixture", .core_mod = fixture_mod, .entry = "tests/ts-core/fixture.ts", .f64_slots = &.{"Model.pastBytes"} },
|
||||
.{ .ts_import = "ts_soundboard_core", .shim_import = "shim_soundboard_core", .contract_name = "soundboard", .core_mod = soundboard_core_mod, .entry = "examples/soundboard-ts/src/core.ts" },
|
||||
.{ .ts_import = "ts_monitor_core", .shim_import = "shim_monitor_core", .contract_name = "system-monitor", .core_mod = monitor_core_mod, .entry = "examples/system-monitor-ts/src/core.ts" },
|
||||
.{ .ts_import = "ts_ai_chat_core", .shim_import = "shim_ai_chat_core", .contract_name = "ai-chat", .core_mod = ai_chat_core_mod, .entry = "examples/ai-chat-ts/src/core.ts" },
|
||||
};
|
||||
// The corpus contract artifacts an external core toolchain consumes
|
||||
// (stage-core-contracts): the extracted sidecar plus its facade
|
||||
// projection, per fixture.
|
||||
// (stage-core-contracts): the effective sidecar plus its generated
|
||||
// entry module and compiler profile, per fixture.
|
||||
var core_contracts: std.ArrayList(CoreContract) = .empty;
|
||||
{
|
||||
const markup_sidecar = sidecarExtractJson(b, target, optimize, extract_mod, markup_fixture_mod, "tests/ts-core/markup_fixture.ts");
|
||||
const projections = facadeProjections(b, corewire_exe, markup_sidecar, &.{});
|
||||
core_contracts.append(b.allocator, .{
|
||||
.name = "markup-fixture",
|
||||
.sidecar = markup_sidecar,
|
||||
.facade = facadeTsFile(b, corewire_exe, markup_sidecar),
|
||||
.sidecar = projections.sidecar,
|
||||
.facade = projections.facade,
|
||||
.profile = projections.profile,
|
||||
}) catch @panic("OOM");
|
||||
}
|
||||
for (conformance_fixtures) |fixture| {
|
||||
const sidecar_json = sidecarExtractJson(b, target, optimize, extract_mod, fixture.core_mod, fixture.entry);
|
||||
conformance_mod.addImport(fixture.ts_import, fixture.core_mod);
|
||||
conformance_mod.addImport(fixture.shim_import, sidecarShimModule(b, target, optimize, corewire_exe, sidecar_json));
|
||||
conformance_mod.addImport(fixture.facade_import, facadeCoreModule(b, target, optimize, node, corewire_exe, sidecar_json));
|
||||
const projections = facadeProjections(b, corewire_exe, sidecar_json, fixture.f64_slots);
|
||||
core_contracts.append(b.allocator, .{
|
||||
.name = fixture.contract_name,
|
||||
.sidecar = sidecar_json,
|
||||
.facade = facadeTsFile(b, corewire_exe, sidecar_json),
|
||||
.sidecar = projections.sidecar,
|
||||
.facade = projections.facade,
|
||||
.profile = projections.profile,
|
||||
}) catch @panic("OOM");
|
||||
}
|
||||
|
||||
@@ -3192,17 +3242,12 @@ fn tsCoreE2eArtifact(
|
||||
while (inputs.next()) |input| {
|
||||
parity_mod.addObjectFile(.{ .cwd_relative = b.dupe(input) });
|
||||
}
|
||||
const parity_tests = filteredTestArtifact(b, parity_mod, "external-core-parity-tests", &.{});
|
||||
// Run the checker over the supplied sidecar explicitly (the
|
||||
// shim generation above validates too, but the checker tier —
|
||||
// both projections, integer_slots structural rules included —
|
||||
// is the surface an external compile is verified against).
|
||||
const check_sidecar = b.addRunArtifact(corewire_exe);
|
||||
check_sidecar.addArg("--sidecar");
|
||||
check_sidecar.addFileArg(parity_sidecar);
|
||||
check_sidecar.addArg("--check");
|
||||
parity_tests.step.dependOn(&check_sidecar.step);
|
||||
break :blk parity_tests;
|
||||
// The generated mirror is the only projection this lane uses;
|
||||
// sidecarShimModule validates exactly that surface. Do not run
|
||||
// the all-projections checker here: an external compiler's
|
||||
// sidecar may legitimately predate facade-only metadata while
|
||||
// remaining a valid mirror contract for its linked archive.
|
||||
break :blk filteredTestArtifact(b, parity_mod, "external-core-parity-tests", &.{});
|
||||
} else null;
|
||||
|
||||
// The full-corpus compiled-core batteries: each supplied fixture's
|
||||
@@ -3247,14 +3292,6 @@ fn tsCoreE2eArtifact(
|
||||
battery_mod.addImport(entry.core_import, paired_mod);
|
||||
if (entry.second_import) |second| battery_mod.addImport(second, entry.second_mod.?);
|
||||
const battery = filteredTestArtifact(b, battery_mod, b.fmt("compiled-core-{s}-tests", .{entry.name}), &.{});
|
||||
// Run the checker tier over the supplied sidecar explicitly —
|
||||
// the same surface an external compile is verified against in
|
||||
// the markup parity suite.
|
||||
const check_sidecar = b.addRunArtifact(corewire_exe);
|
||||
check_sidecar.addArg("--sidecar");
|
||||
check_sidecar.addFileArg(supply.sidecar);
|
||||
check_sidecar.addArg("--check");
|
||||
battery.step.dependOn(&check_sidecar.step);
|
||||
compiled_core_parity.append(b.allocator, .{ .name = b.dupe(entry.name), .tests = battery }) catch @panic("OOM");
|
||||
}
|
||||
|
||||
@@ -3331,18 +3368,36 @@ fn pairedCoreModule(
|
||||
return mod;
|
||||
}
|
||||
|
||||
/// A sidecar's TypeScript facade projection alone (core_facade.ts) —
|
||||
/// the contract module an external core toolchain compiles.
|
||||
fn facadeTsFile(
|
||||
const FacadeProjections = struct {
|
||||
sidecar: std.Build.LazyPath,
|
||||
facade: std.Build.LazyPath,
|
||||
profile: std.Build.LazyPath,
|
||||
};
|
||||
|
||||
/// A sidecar's compiled-core projections: the effective contract after
|
||||
/// caller-stated demotions, the generated entry module (core_facade.ts), and
|
||||
/// the compiler profile that builds it (core_profile.json), emitted by one
|
||||
/// corewire invocation so no staged sibling can describe a different layout.
|
||||
fn facadeProjections(
|
||||
b: *std.Build,
|
||||
corewire_exe: *std.Build.Step.Compile,
|
||||
sidecar_json: std.Build.LazyPath,
|
||||
) std.Build.LazyPath {
|
||||
f64_slots: []const []const u8,
|
||||
) FacadeProjections {
|
||||
const generate = b.addRunArtifact(corewire_exe);
|
||||
generate.addArg("--sidecar");
|
||||
generate.addFileArg(sidecar_json);
|
||||
generate.addArg("--facade");
|
||||
return generate.addOutputFileArg("core_facade.ts");
|
||||
const facade = generate.addOutputFileArg("core_facade.ts");
|
||||
generate.addArg("--profile");
|
||||
const profile = generate.addOutputFileArg("core_profile.json");
|
||||
generate.addArg("--effective-sidecar");
|
||||
const sidecar = generate.addOutputFileArg("core.contract.json");
|
||||
for (f64_slots) |slot| {
|
||||
generate.addArg("--f64-slot");
|
||||
generate.addArg(slot);
|
||||
}
|
||||
return .{ .sidecar = sidecar, .facade = facade, .profile = profile };
|
||||
}
|
||||
|
||||
/// One fixture's generated-mirror module: run corewire over the
|
||||
@@ -3372,50 +3427,6 @@ fn sidecarShimModule(
|
||||
});
|
||||
}
|
||||
|
||||
/// One fixture's compiled-facade module: run corewire over the sidecar
|
||||
/// for its TypeScript projection (core_facade.ts), then compile that
|
||||
/// projection through the shipped transpiler — the checker/emitter tier
|
||||
/// is the compile, so this step IS the subset-acceptance proof — and
|
||||
/// stage the emitted Zig beside its rt kernel like any fixture core.
|
||||
fn facadeCoreModule(
|
||||
b: *std.Build,
|
||||
target: std.Build.ResolvedTarget,
|
||||
optimize: std.builtin.OptimizeMode,
|
||||
node: []const u8,
|
||||
corewire_exe: *std.Build.Step.Compile,
|
||||
sidecar_json: std.Build.LazyPath,
|
||||
) *std.Build.Module {
|
||||
const generate = b.addRunArtifact(corewire_exe);
|
||||
generate.addArg("--sidecar");
|
||||
generate.addFileArg(sidecar_json);
|
||||
generate.addArg("--facade");
|
||||
const facade_ts = generate.addOutputFileArg("core_facade.ts");
|
||||
const staged_ts = b.addWriteFiles();
|
||||
const facade_entry = staged_ts.addCopyFile(facade_ts, "core.ts");
|
||||
|
||||
const transpile = b.addSystemCommand(&.{node});
|
||||
transpile.addFileArg(b.path("packages/core/src/cli.ts"));
|
||||
transpile.addFileArg(facade_entry);
|
||||
transpile.addArg("-o");
|
||||
const emitted_core = transpile.addOutputFileArg("core.zig");
|
||||
tsCoreAddDirInputs(b, transpile, "packages/core/sdk");
|
||||
const transpiler_sources = [_][]const u8{
|
||||
"checker.ts", "cli.ts", "diagnostics.ts", "emitter.ts", "infer.ts", "modules.ts", "transpile.ts", "typed_ast.ts", "types.ts",
|
||||
};
|
||||
for (transpiler_sources) |source| {
|
||||
transpile.addFileInput(b.path(b.fmt("packages/core/src/{s}", .{source})));
|
||||
}
|
||||
|
||||
const staged = b.addWriteFiles();
|
||||
const core_root = staged.addCopyFile(emitted_core, "core.zig");
|
||||
_ = staged.addCopyFile(b.path("packages/core/rt/rt.zig"), "rt.zig");
|
||||
return b.createModule(.{
|
||||
.root_source_file = core_root,
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
}
|
||||
|
||||
/// Extract a fixture's contract sidecar from its transpiled module
|
||||
/// (tools/corewire/extract.zig) — a generated one-line main per
|
||||
/// fixture, so corpus sidecars regenerate whenever the fixture or the
|
||||
|
||||
@@ -1040,6 +1040,7 @@ fn linkPlatform(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.Res
|
||||
// fails the compile by design if it cannot be found.
|
||||
app_mod.addIncludePath(dep.path("third_party/webview2/include"));
|
||||
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/webview2_host.cpp"), .flags = &.{"-std=c++17"} });
|
||||
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/gpu_surface_renderer.cpp"), .flags = &.{"-std=c++17"} });
|
||||
// WebView2Loader.dll rides next to the installed app
|
||||
// executable: the host loads it at runtime to discover
|
||||
// the machine's WebView2 runtime. Canvas apps never
|
||||
@@ -1064,6 +1065,7 @@ fn linkPlatform(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.Res
|
||||
// WebViewNotFound the moment an app actually uses a
|
||||
// WebView.
|
||||
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/webview2_host.cpp"), .flags = &.{ "-std=c++17", "-DNATIVE_SDK_ALLOW_WEBVIEW2_STUB" } });
|
||||
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/gpu_surface_renderer.cpp"), .flags = &.{"-std=c++17"} });
|
||||
},
|
||||
.chromium => {
|
||||
const cef_check = addCefCheck(b, target, cef_dir);
|
||||
@@ -1075,6 +1077,7 @@ fn linkPlatform(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.Res
|
||||
const include_arg = b.fmt("-I{s}", .{cef_dir});
|
||||
const define_arg = b.fmt("-DNATIVE_SDK_CEF_DIR=\"{s}\"", .{cef_dir});
|
||||
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/cef_host.cpp"), .flags = &.{ "-std=c++17", include_arg, define_arg } });
|
||||
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/gpu_surface_renderer.cpp"), .flags = &.{"-std=c++17"} });
|
||||
app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.lib", .{cef_dir})));
|
||||
app_mod.addLibraryPath(b.path(b.fmt("{s}/Release", .{cef_dir})));
|
||||
},
|
||||
@@ -1083,6 +1086,11 @@ fn linkPlatform(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.Res
|
||||
app_mod.linkSystemLibrary("c++", .{});
|
||||
app_mod.linkSystemLibrary("user32", .{});
|
||||
app_mod.linkSystemLibrary("gdi32", .{});
|
||||
// Retained gpu_surface packets are composited into a hardware
|
||||
// Direct2D target; DirectWrite draws the engine-measured text
|
||||
// runs (including registered in-memory fonts) on that target.
|
||||
app_mod.linkSystemLibrary("d2d1", .{});
|
||||
app_mod.linkSystemLibrary("dwrite", .{});
|
||||
app_mod.linkSystemLibrary("imm32", .{});
|
||||
app_mod.linkSystemLibrary("comctl32", .{});
|
||||
app_mod.linkSystemLibrary("ole32", .{});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
fix: **Theme-accurate disabled buttons**: disabled buttons now keep shadcn's knockout label treatment in the default theme and use Geist's gray-100/gray-700 swap, gray-400 edge, and distinct half-opacity tertiary register in the Geist theme.
|
||||
@@ -0,0 +1 @@
|
||||
feature: **Geist-style code diffs**: `ui.code` and `<code>` can mark added and removed logical lines with theme-aware full-row washes, renderer-owned `+`/`-` markers, optional line numbers, and unchanged syntax-highlighted clipboard source.
|
||||
@@ -0,0 +1 @@
|
||||
fix: **Canonical documentation routes**: documentation now lives under `/docs/`, with permanent redirects from every previous URL, explicit canonical metadata, `.md` siblings, and a generated `llms.txt` that stays aligned with each page's canonical MDX source.
|
||||
@@ -0,0 +1 @@
|
||||
fix: **Geist primary tabs match the design system**: tab strips now use the reference 50px row, full-width bottom rail, content-hugging 14px triggers, 24px spacing, and 16px icon treatment without changing default-theme pill tabs; the GPU component gallery now pairs a compact theme picker with a scrollable component tree and focused specimen views.
|
||||
@@ -0,0 +1 @@
|
||||
improvement: **Verified Zig setup**: repository and generated CI workflows now install Zig with `vercel-labs/setup-zig`, including signed archive and checksum verification.
|
||||
@@ -0,0 +1 @@
|
||||
fix: **Quiet Windows subprocesses**: `Effects.spawn` no longer opens or flashes a console window when a GUI or tray app launches a console-subsystem helper such as `node.exe`; interactive terminal children remain on the separate PTY API.
|
||||
@@ -0,0 +1,4 @@
|
||||
fix: **Responsive Windows GPU surfaces**: Windows now renders retained binary canvas packets with Direct2D and DirectWrite, applies dirty-region patches (including edge-safe GPU-resident backdrop blur), and limits RGBA-to-BGRA conversion and invalidation to dirty pixels when software fallback is required.
|
||||
- **Exact Windows packet text and chrome**: Packet rendering now refuses when the bundled/custom font path cannot preserve engine-planned metrics, preserves explicitly positioned glyph runs, prevents system glyph substitution, and samples a covered, changed hidden-titlebar pixel for native caption contrast.
|
||||
- **Truthful GPU backend types**: TypeScript creation options now expose only portable backend requests while view and frame state can report the concrete Direct2D renderer; explicit software requests bypass packet encoding and image uploads and stay on the reference renderer and pixel presenter.
|
||||
- **Reliable registered-image replacement**: Unregistering and then re-registering identical pixels now recreates the removed GPU resource instead of retaining a stale cache key and silently omitting the image.
|
||||
@@ -1,5 +1,8 @@
|
||||
import createMDX from "@next/mdx";
|
||||
import { createRequire } from "node:module";
|
||||
import { readdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// Resolve the plugin to an absolute path (still a string, so the config
|
||||
// stays serializable for Turbopack). A bare "remark-gfm" is require()d
|
||||
@@ -7,6 +10,19 @@ import { createRequire } from "node:module";
|
||||
// module isolation cannot see this app's dependencies — production
|
||||
// builds resolved it, the Turbopack dev server did not.
|
||||
const require = createRequire(import.meta.url);
|
||||
const docsContentDir = fileURLToPath(new URL("./src/app/docs", import.meta.url));
|
||||
|
||||
function docsSlugs(dir = docsContentDir, segments = []) {
|
||||
const slugs = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.isDirectory()) {
|
||||
slugs.push(...docsSlugs(path.join(dir, entry.name), [...segments, entry.name]));
|
||||
} else if (entry.name === "page.mdx" && segments.length > 0) {
|
||||
slugs.push(segments.join("/"));
|
||||
}
|
||||
}
|
||||
return slugs;
|
||||
}
|
||||
|
||||
const withMDX = createMDX({
|
||||
options: {
|
||||
@@ -30,9 +46,19 @@ const nextConfig = {
|
||||
ignored: ["**/.next-gate/**", "**/.next-check/**"],
|
||||
},
|
||||
async redirects() {
|
||||
// Config redirects preserve the request query string. Keeping these out
|
||||
// of the prerendered catch-all route avoids baking a query-less Location
|
||||
// header into every legacy URL's static response.
|
||||
const legacyDocsRedirects = docsSlugs().flatMap((slug) => [
|
||||
{ source: `/${slug}`, destination: `/docs/${slug}`, permanent: true },
|
||||
{ source: `/${slug}.md`, destination: `/docs/${slug}.md`, permanent: true },
|
||||
{ source: `/md/${slug}`, destination: `/docs/${slug}.md`, permanent: true },
|
||||
]);
|
||||
|
||||
return [
|
||||
// The Philosophy page became the Introduction, the opening page of the docs.
|
||||
{ source: "/philosophy", destination: "/introduction", permanent: true },
|
||||
{ source: "/philosophy", destination: "/docs/introduction", permanent: true },
|
||||
...legacyDocsRedirects,
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"check": "pnpm typecheck && pnpm build && node scripts/check-code-toggle.mjs"
|
||||
"check": "pnpm typecheck && pnpm build && node scripts/check-doc-routes.mjs && node scripts/check-code-toggle.mjs && node scripts/check-wasm-preview.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdx-js/loader": "^3",
|
||||
@@ -41,4 +41,4 @@
|
||||
"postcss@<8.5.10": ">=8.5.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,203 @@
|
||||
// SEO regression gate for the /docs migration. Every page.mdx must build at
|
||||
// one canonical HTML URL and one canonical Markdown sibling; the former route
|
||||
// must redirect permanently rather than render duplicate content. Canonical
|
||||
// metadata, sitemap entries, llms.txt, and rendered internal links must all
|
||||
// point directly into /docs so crawlers never have to choose between copies.
|
||||
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { dirname, join, relative, sep } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const docsDir = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const sourceDir = join(docsDir, "src", "app", "docs");
|
||||
const distDir = join(docsDir, process.env.NEXT_DIST_DIR || ".next");
|
||||
const appOutputDir = join(distDir, "server", "app");
|
||||
const siteUrl = "https://native-sdk.dev";
|
||||
|
||||
function* mdxPages(dir) {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const full = join(dir, entry);
|
||||
if (statSync(full).isDirectory()) yield* mdxPages(full);
|
||||
else if (entry === "page.mdx") yield full;
|
||||
}
|
||||
}
|
||||
|
||||
function readRequired(file, route) {
|
||||
if (!existsSync(file)) {
|
||||
throw new Error(`${route}: missing build output ${file}`);
|
||||
}
|
||||
return readFileSync(file, "utf8");
|
||||
}
|
||||
|
||||
const routesManifest = JSON.parse(
|
||||
readRequired(join(distDir, "routes-manifest.json"), "redirect manifest"),
|
||||
);
|
||||
|
||||
function assertConfiguredRedirect(source, destination) {
|
||||
const redirect = routesManifest.redirects.find((candidate) => candidate.source === source);
|
||||
if (!redirect || redirect.statusCode !== 308 || redirect.destination !== destination) {
|
||||
throw new Error(
|
||||
`${source}: expected a query-preserving 308 config redirect to ${destination}, got ${JSON.stringify(redirect)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function proseLines(markdown) {
|
||||
const lines = [];
|
||||
let fence = null;
|
||||
for (const line of markdown.split("\n")) {
|
||||
const marker = line.trimStart().match(/^(```|~~~)/)?.[1];
|
||||
if (marker) {
|
||||
if (fence === marker) fence = null;
|
||||
else if (fence === null) fence = marker;
|
||||
continue;
|
||||
}
|
||||
if (fence === null) lines.push(line);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
function headings(markdown) {
|
||||
return proseLines(markdown)
|
||||
.map((line) => line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/))
|
||||
.filter(Boolean)
|
||||
.map((match) => `${match[1]} ${match[2]}`);
|
||||
}
|
||||
|
||||
function assertCleanMarkdown(source, markdown, route) {
|
||||
// Generic type spellings such as `Sub<Msg>` are legitimate inside inline
|
||||
// code and must not be mistaken for unresolved MDX components.
|
||||
const prose = proseLines(markdown).join("\n").replace(/`[^`\n]*`/g, "");
|
||||
const unresolved = prose.match(/<[A-Z][A-Za-z0-9]*(?:\s|\/?>)/)?.[0];
|
||||
if (unresolved) {
|
||||
throw new Error(`${route}: unresolved MDX component in Markdown output: ${unresolved}`);
|
||||
}
|
||||
const unresolvedExpression = prose.match(/\{"[^"\\\r\n]*"\}|\{'[^'\\\r\n]*'\}/)?.[0];
|
||||
if (unresolvedExpression) {
|
||||
throw new Error(
|
||||
`${route}: unresolved MDX string expression in Markdown output: ${unresolvedExpression}`,
|
||||
);
|
||||
}
|
||||
|
||||
const renderedHeadings = new Set(headings(markdown));
|
||||
for (const heading of headings(source)) {
|
||||
if (!renderedHeadings.has(heading)) {
|
||||
throw new Error(`${route}: Markdown output dropped source heading ${heading}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const component of source.matchAll(/<EjectSection\s+components=\{\[([\s\S]*?)\]\}\s*\/>/g)) {
|
||||
if (!renderedHeadings.has("## Eject")) {
|
||||
throw new Error(`${route}: EjectSection did not render its heading`);
|
||||
}
|
||||
for (const name of component[1].matchAll(/"([^"]+)"/g)) {
|
||||
const command = `native eject component ${name[1]}`;
|
||||
if (!markdown.includes(command)) {
|
||||
throw new Error(`${route}: EjectSection did not render command ${command}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pageFiles = [...mdxPages(sourceDir)];
|
||||
const canonicalPaths = new Set(
|
||||
pageFiles.map((page) => {
|
||||
const slug = relative(sourceDir, dirname(page)).split(sep).join("/");
|
||||
return `/docs/${slug}`;
|
||||
}),
|
||||
);
|
||||
const canonicalUrls = new Set([...canonicalPaths].map((route) => `${siteUrl}${route}`));
|
||||
const canonicalMarkdownUrls = new Set(
|
||||
[...canonicalPaths].map((route) => `${siteUrl}${route}.md`),
|
||||
);
|
||||
let pages = 0;
|
||||
|
||||
for (const page of pageFiles) {
|
||||
pages += 1;
|
||||
const slug = relative(sourceDir, dirname(page)).split(sep).join("/");
|
||||
const canonicalPath = `/docs/${slug}`;
|
||||
const canonicalUrl = `${siteUrl}${canonicalPath}`;
|
||||
const output = join(appOutputDir, "docs", slug);
|
||||
const html = readRequired(`${output}.html`, canonicalPath);
|
||||
const source = readFileSync(page, "utf8");
|
||||
|
||||
if (!html.includes(`<link rel="canonical" href="${canonicalUrl}"/>`)) {
|
||||
throw new Error(`${canonicalPath}: missing its exact canonical link tag`);
|
||||
}
|
||||
if (!html.includes(`<meta property="og:url" content="${canonicalUrl}"/>`)) {
|
||||
throw new Error(`${canonicalPath}: Open Graph URL is not canonical`);
|
||||
}
|
||||
|
||||
for (const match of html.matchAll(/<a\b[^>]*\bhref="([^"]+)"/g)) {
|
||||
const href = match[1];
|
||||
if (href?.startsWith("/") && href !== "/" && !href.startsWith("/docs/")) {
|
||||
throw new Error(`${canonicalPath}: rendered internal link bypasses /docs: ${href}`);
|
||||
}
|
||||
if (href?.startsWith("/docs/")) {
|
||||
const target = href.split(/[?#]/, 1)[0];
|
||||
if (target && !canonicalPaths.has(target)) {
|
||||
throw new Error(`${canonicalPath}: rendered internal link targets no docs page: ${href}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const markdownMeta = JSON.parse(readRequired(`${output}.md.meta`, `${canonicalPath}.md`));
|
||||
if (
|
||||
markdownMeta.status !== 200 ||
|
||||
markdownMeta.headers?.["content-type"] !== "text/markdown; charset=utf-8"
|
||||
) {
|
||||
throw new Error(`${canonicalPath}.md: expected a static text/markdown response`);
|
||||
}
|
||||
if (markdownMeta.headers?.link !== `<${canonicalUrl}>; rel="canonical"`) {
|
||||
throw new Error(`${canonicalPath}.md: missing its exact canonical HTTP Link header`);
|
||||
}
|
||||
const markdown = readRequired(`${output}.md.body`, `${canonicalPath}.md`);
|
||||
assertCleanMarkdown(source, markdown, `${canonicalPath}.md`);
|
||||
|
||||
assertConfiguredRedirect(`/${slug}`, canonicalPath);
|
||||
assertConfiguredRedirect(`/${slug}.md`, `${canonicalPath}.md`);
|
||||
assertConfiguredRedirect(`/md/${slug}`, `${canonicalPath}.md`);
|
||||
|
||||
for (const legacyOutput of [
|
||||
join(appOutputDir, `${slug}.html`),
|
||||
join(appOutputDir, `${slug}.meta`),
|
||||
join(appOutputDir, `${slug}.md.meta`),
|
||||
join(appOutputDir, "md", `${slug}.meta`),
|
||||
]) {
|
||||
if (existsSync(legacyOutput)) {
|
||||
throw new Error(`/${slug}: legacy URL was prerendered instead of using its config redirect`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pages === 0) throw new Error("no docs page.mdx files found");
|
||||
|
||||
const sitemap = readRequired(join(appOutputDir, "sitemap.xml.body"), "/sitemap.xml");
|
||||
const sitemapUrls = new Set([...sitemap.matchAll(/<loc>([^<]+)<\/loc>/g)].map((match) => match[1]));
|
||||
for (const match of sitemap.matchAll(/<loc>([^<]+)<\/loc>/g)) {
|
||||
const url = match[1];
|
||||
if (url !== `${siteUrl}/` && !canonicalUrls.has(url)) {
|
||||
throw new Error(`/sitemap.xml: non-canonical documentation URL ${url}`);
|
||||
}
|
||||
}
|
||||
for (const url of canonicalUrls) {
|
||||
if (!sitemapUrls.has(url)) throw new Error(`/sitemap.xml: missing canonical page ${url}`);
|
||||
}
|
||||
|
||||
const llms = readRequired(join(appOutputDir, "llms.txt.body"), "/llms.txt");
|
||||
const llmsUrls = new Set();
|
||||
for (const line of llms.split("\n")) {
|
||||
if (!line.startsWith("- [")) continue;
|
||||
const url = line.match(/^- \[[^\]]+\]\(([^)]+)\)$/)?.[1];
|
||||
if (!url || !canonicalMarkdownUrls.has(url)) {
|
||||
throw new Error(`/llms.txt: non-canonical documentation link: ${line}`);
|
||||
}
|
||||
llmsUrls.add(url);
|
||||
}
|
||||
for (const url of canonicalMarkdownUrls) {
|
||||
if (!llmsUrls.has(url)) throw new Error(`/llms.txt: missing canonical page ${url}`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`docs route check passed: ${pages} canonical pages, Markdown siblings, and query-preserving legacy redirects verified`,
|
||||
);
|
||||
@@ -0,0 +1,63 @@
|
||||
// Regression pin for the Code component docs previews. ComponentPreview
|
||||
// deliberately keeps a webp fallback under its live canvas, so a stale
|
||||
// or incompatible wasm scene can otherwise fail silently and leave the
|
||||
// page looking correct while it is only showing the screenshot.
|
||||
//
|
||||
// Require the checked-in module to instantiate the exact `code` and
|
||||
// `code-diff` scenes used by /components/code. Runs after `next build`
|
||||
// as part of `pnpm check`.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const docsDir = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const wasmPath = join(docsDir, "public", "wasm", "component-preview.wasm");
|
||||
const vocabPath = join(docsDir, "src", "lib", "component-vocab.json");
|
||||
const bytes = readFileSync(wasmPath);
|
||||
const vocab = JSON.parse(readFileSync(vocabPath, "utf8"));
|
||||
const { instance } = await WebAssembly.instantiate(bytes, {});
|
||||
const exports = instance.exports;
|
||||
const sceneNames = ["code", "code-diff"];
|
||||
|
||||
if (
|
||||
typeof exports.preview_code_diff_metadata_round_trip !== "function" ||
|
||||
exports.preview_code_diff_metadata_round_trip() !== 1
|
||||
) {
|
||||
throw new Error(
|
||||
"the checked-in component-preview.wasm truncates code-diff metadata above line 96 on wasm32",
|
||||
);
|
||||
}
|
||||
|
||||
for (const sceneName of sceneNames) {
|
||||
const scene = new TextEncoder().encode(sceneName);
|
||||
const scenePtr = exports.preview_alloc(scene.length);
|
||||
|
||||
if (!scenePtr) {
|
||||
throw new Error(`${sceneName} WASM preview check could not allocate its scene name`);
|
||||
}
|
||||
|
||||
new Uint8Array(exports.memory.buffer).set(scene, scenePtr);
|
||||
const handle = exports.preview_create(scenePtr, scene.length, 0);
|
||||
exports.preview_free(scenePtr, scene.length);
|
||||
|
||||
if (!handle) {
|
||||
throw new Error(
|
||||
`the checked-in component-preview.wasm cannot create the \`${sceneName}\` scene; rebuild it with \`zig build docs-wasm-preview\``,
|
||||
);
|
||||
}
|
||||
|
||||
const width = exports.preview_logical_width(handle);
|
||||
const height = exports.preview_logical_height(handle);
|
||||
exports.preview_destroy(handle);
|
||||
const expectedWidth = vocab.previews[sceneName].width / 2;
|
||||
const expectedHeight = vocab.previews[sceneName].height / 2;
|
||||
|
||||
if (width !== expectedWidth || height !== expectedHeight) {
|
||||
throw new Error(
|
||||
`the ${sceneName} WASM preview is ${width}x${height}; expected the catalog's ${expectedWidth}x${expectedHeight} scene`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`${sceneName} WASM preview check passed: live scene instantiated at ${width}x${height}`);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { readFile, readdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { mdxToCleanMarkdown } from "@/lib/mdx-to-markdown";
|
||||
import { siteUrl } from "@/lib/site";
|
||||
|
||||
/**
|
||||
* Serve every canonical docs page as clean Markdown beside its HTML route.
|
||||
* Legacy URLs are config redirects so Next can preserve each request's query
|
||||
* string; this route stays fully static and only emits the canonical siblings.
|
||||
*/
|
||||
|
||||
export const dynamic = "force-static";
|
||||
export const dynamicParams = false;
|
||||
|
||||
const docsDir = () => path.join(process.cwd(), "src", "app", "docs");
|
||||
|
||||
export async function generateStaticParams(): Promise<{ slug: string[] }[]> {
|
||||
const params: { slug: string[] }[] = [];
|
||||
async function walk(dir: string, slug: string[]): Promise<void> {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
await walk(path.join(dir, entry.name), [...slug, entry.name]);
|
||||
} else if (entry.name === "page.mdx" && slug.length > 0) {
|
||||
const markdownSlug = [...slug];
|
||||
markdownSlug[markdownSlug.length - 1] += ".md";
|
||||
params.push({ slug: ["docs", ...markdownSlug] });
|
||||
}
|
||||
}
|
||||
}
|
||||
await walk(docsDir(), []);
|
||||
return params;
|
||||
}
|
||||
|
||||
export async function GET(_request: Request, context: { params: Promise<{ slug: string[] }> }) {
|
||||
const { slug } = await context.params;
|
||||
if (slug[0] !== "docs") return new Response("Not found", { status: 404 });
|
||||
const sourceSlug = slug.slice(1);
|
||||
const filename = sourceSlug.at(-1);
|
||||
if (!filename?.endsWith(".md") || filename === ".md") {
|
||||
return new Response("Not found", { status: 404 });
|
||||
}
|
||||
|
||||
sourceSlug[sourceSlug.length - 1] = filename.slice(0, -3);
|
||||
const canonicalUrl = `${siteUrl}/docs/${sourceSlug.join("/")}`;
|
||||
const filePath = path.join(docsDir(), ...sourceSlug, "page.mdx");
|
||||
// Static params come from the filesystem walk above, but never follow
|
||||
// a path that escapes src/app/docs.
|
||||
if (!filePath.startsWith(docsDir() + path.sep)) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
}
|
||||
try {
|
||||
const source = await readFile(filePath, "utf8");
|
||||
return new Response(mdxToCleanMarkdown(source) + "\n", {
|
||||
headers: {
|
||||
"Content-Type": "text/markdown; charset=utf-8",
|
||||
Link: `<${canonicalUrl}>; rel="canonical"`,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return new Response("Not found", { status: 404 });
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { ComponentPreview } from "@/components/component-preview";
|
||||
import { AttrTable } from "@/components/attr-table";
|
||||
|
||||
# Code
|
||||
|
||||
Presents source text in a themed monospace surface with deterministic syntax highlighting. Code wraps by default. Set `wrap="false"` to preserve logical lines inside one horizontal scroll region, and opt into logical line numbers with `line-numbers`.
|
||||
|
||||
HTML-family highlighting understands HTML, XML, SVG, JSX, and TSX structure: element or component tags, attributes, strings, comments, numbers, and JavaScript/TypeScript expressions receive distinct theme-token colors.
|
||||
|
||||
<ComponentPreview name="code" alt="A JSX code sample with syntax highlighting and line numbers" caption="JSX tags, attributes, strings, expressions, and an unwrapped horizontal viewport" />
|
||||
|
||||
## Markup
|
||||
|
||||
```html
|
||||
<code
|
||||
source="{component_source}"
|
||||
language="tsx"
|
||||
line-numbers
|
||||
wrap="false"
|
||||
width="480"
|
||||
label="Accordion example"
|
||||
/>
|
||||
```
|
||||
|
||||
`source` is required and must be one `{binding}` producing text. `language` is a literal lexer name; unknown names are validation errors. Line numbers are off by default and remain decorative, so selecting and copying a numbered block returns only the source text. Numbered presentation is limited to 128 logical lines; longer sources keep all code and omit the gutter.
|
||||
|
||||
## Programmatic construction (Zig)
|
||||
|
||||
```zig
|
||||
ui.code(.{
|
||||
.language = .html,
|
||||
.line_numbers = true,
|
||||
.wrap = false,
|
||||
.width = 480,
|
||||
.semantics = .{ .label = "Accordion example" },
|
||||
}, model.component_source)
|
||||
```
|
||||
|
||||
The public lexer model is `native_sdk.canvas.code`. `languageFromName` resolves markup spellings, `languageFromFence` reads a Markdown info string, and `highlight` produces the same bounded, theme-colored span runs both renderers use.
|
||||
|
||||
## Languages
|
||||
|
||||
Zig; JavaScript and TypeScript; JSX and TSX; JSON; shell; Python; Rust; C, C++, C#, Java, Kotlin, and Swift; Go; HTML, XML, and SVG; CSS, SCSS, and Less; and SQL. An omitted language renders plain monospace.
|
||||
|
||||
## Attributes
|
||||
|
||||
<AttrTable element="code" attrs={["source", "language", "line-numbers", "wrap", "width", "height", "min-width", "grow", "key", "global-key", "label"]} />
|
||||
@@ -1,62 +0,0 @@
|
||||
import { ComponentPreview } from "@/components/component-preview";
|
||||
import { AttrTable } from "@/components/attr-table";
|
||||
|
||||
# Tree
|
||||
|
||||
A disclosure-tree container. The rows are ordinary elements (usually [list items](/components/list)) carrying `role="treeitem"`, and every such descendant joins one roving keyboard focus set: Up/Down walk the visible rows, Left collapses a row or moves to its parent, Right expands or moves to the first child, Home/End jump to the edges. Expandable rows bind `expanded` and dispatch `on-toggle`; selection follows each row's `on-press`. The model owns both states — children of a collapsed row are simply not rendered.
|
||||
|
||||
<ComponentPreview name="tree" alt="A file tree with an expanded folder and a selected row rendered by the engine" caption="an expanded folder, indented children, and a collapsed sibling" />
|
||||
|
||||
## Markup
|
||||
|
||||
```html
|
||||
<tree width="340" gap="2">
|
||||
<list-item icon="folder-open" role="treeitem" expanded="{src_open}" on-toggle="toggle_src" on-press="select_src">src</list-item>
|
||||
<if test="{src_open}">
|
||||
<row>
|
||||
<column width="20" />
|
||||
<column gap="2" grow="1">
|
||||
<list-item icon="file-text" role="treeitem" selected="{file == main}" on-press="select_main">main.zig</list-item>
|
||||
<list-item icon="file-text" role="treeitem" selected="{file == view}" on-press="select_view">view.zig</list-item>
|
||||
</column>
|
||||
</row>
|
||||
</if>
|
||||
<list-item icon="folder" role="treeitem" expanded="{assets_open}" on-toggle="toggle_assets" on-press="select_assets">assets</list-item>
|
||||
</tree>
|
||||
```
|
||||
|
||||
The indent is plain layout — a fixed-width spacer column beside the children. Omit `expanded` on leaf rows; only rows that bind it participate in Left/Right disclosure.
|
||||
|
||||
## Programmatic construction (Zig)
|
||||
|
||||
In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:
|
||||
|
||||
```zig
|
||||
ui.tree(.{ .width = 340, .gap = 2 }, .{
|
||||
ui.listItem(.{
|
||||
.icon = "folder-open",
|
||||
.expanded = model.src_open,
|
||||
.on_toggle = .toggle_src,
|
||||
.on_press = .select_src,
|
||||
.semantics = .{ .role = .treeitem },
|
||||
}, "src"),
|
||||
if (model.src_open) ui.row(.{}, .{
|
||||
ui.column(.{ .width = 20 }, .{}),
|
||||
ui.column(.{ .gap = 2, .grow = 1 }, .{
|
||||
ui.listItem(.{ .icon = "file-text", .selected = model.file == .main, .on_press = .select_main, .semantics = .{ .role = .treeitem } }, "main.zig"),
|
||||
ui.listItem(.{ .icon = "file-text", .selected = model.file == .view, .on_press = .select_view, .semantics = .{ .role = .treeitem } }, "view.zig"),
|
||||
}),
|
||||
}) else ui.stack(.{}, .{}),
|
||||
ui.listItem(.{
|
||||
.icon = "folder",
|
||||
.expanded = model.assets_open,
|
||||
.on_toggle = .toggle_assets,
|
||||
.on_press = .select_assets,
|
||||
.semantics = .{ .role = .treeitem },
|
||||
}, "assets"),
|
||||
})
|
||||
```
|
||||
|
||||
## Attributes
|
||||
|
||||
<AttrTable attrs={["role", "expanded", "selected", "on-toggle", "on-press", "gap", "label"]} />
|
||||
@@ -11,7 +11,7 @@ A Native SDK app is one loop with four parts:
|
||||
|
||||
The runtime owns everything else: window creation, GPU presentation, resize, pointer and keyboard dispatch, timers, accessibility, and hot reload. Your code never handles a raw event — input lands on a widget, the widget's bound message dispatches into `update`, the view rebuilds from the new model, and the engine repaints what changed.
|
||||
|
||||
The loop is the same in both authoring languages. By default the core is TypeScript (`src/core.ts`, compiled to native code at build time — [TypeScript Cores](/typescript) covers that tier in depth); a Zig core (`src/main.zig`, from `native init --template zig-core`) is first-class by choice, and the rest of this page — wiring, identity, hot reload — applies to both. The Zig-specific wiring sections below are exactly what the build generates for a TypeScript app, so they double as its eject story.
|
||||
The loop is the same in both authoring languages. By default the core is TypeScript (`src/core.ts`, compiled to native code at build time — [TypeScript Cores](/docs/typescript) covers that tier in depth); a Zig core (`src/main.zig`, from `native init --template zig-core`) is first-class by choice, and the rest of this page — wiring, identity, hot reload — applies to both. The Zig-specific wiring sections below are exactly what the build generates for a TypeScript app, so they double as its eject story.
|
||||
|
||||
## The loop in full
|
||||
|
||||
@@ -103,15 +103,15 @@ pub fn main(init: std.process.Init) !void {
|
||||
}
|
||||
```
|
||||
|
||||
`create` requires every `Model` field to carry a default; the model starts as `.{}` and boot state is assigned through the returned pointer. The `scene` declares the native window and its GPU surface view — see [Windows](/windows) and [Native Surfaces](/native-surfaces) for multi-view scenes.
|
||||
`create` requires every `Model` field to carry a default; the model starts as `.{}` and boot state is assigned through the returned pointer. The `scene` declares the native window and its GPU surface view — see [Windows](/docs/windows) and [Native Surfaces](/docs/native-surfaces) for multi-view scenes.
|
||||
|
||||
## Rebuilds and widget identity
|
||||
|
||||
After every `update`, the runtime rebuilds the view from the model. Rebuilds are cheap and safe by design:
|
||||
|
||||
- **Widget identity is structural.** A widget keeps its id across rebuilds, reorders, and hot reloads, so engine-owned state — scroll offsets, text carets, focus — survives. List items carry `key` (or `global-key` for items that move between containers) to keep identity through reorders. Unkeyed same-kind siblings take positional identity (sibling index), so an `<if>` that inserts or removes an earlier same-kind sibling re-disambiguates the trailing ones — engine-owned state like carets and scroll can hop; keyed items and keyed ancestors hold identity.
|
||||
- **The source wins.** Engine-retained state (a scroll offset, a toggle) survives rebuilds until the model asserts a different value; then the model's value applies. This is why controlled patterns echo runtime-applied values back through the model — see [State & Data Flow](/state).
|
||||
- **Errors degrade, they never crash.** A failing `update` arm is caught, recorded in a bounded error ring (visible in [automation](/automation) snapshots as `dispatch_errors=`), and the app keeps running.
|
||||
- **The source wins.** Engine-retained state (a scroll offset, a toggle) survives rebuilds until the model asserts a different value; then the model's value applies. This is why controlled patterns echo runtime-applied values back through the model — see [State & Data Flow](/docs/state).
|
||||
- **Errors degrade, they never crash.** A failing `update` arm is caught, recorded in a bounded error ring (visible in [automation](/docs/automation) snapshots as `dispatch_errors=`), and the app keeps running.
|
||||
|
||||
## Hot reload in development
|
||||
|
||||
@@ -156,8 +156,8 @@ A Zig-root app keeps dev-time hot reload for its embedded fragments too: build w
|
||||
|
||||
## Side effects
|
||||
|
||||
`update` stays pure by routing anything asynchronous — subprocesses, HTTP, file persistence, timers, clipboard — through the effects channel, and results come back as ordinary messages. In a TypeScript core, effects are `Cmd` data returned from `update` and recurring timers are declared `Sub` data — see [TypeScript Cores: Effects](/typescript#effects-are-cmd-data). In a Zig core, declare `.update_fx` instead of `.update` and spawn from message arms; boot-time work goes in `.init_fx`, which runs exactly once before the first paint. See [Native UI: Effects](/native-ui#effects).
|
||||
`update` stays pure by routing anything asynchronous — subprocesses, HTTP, file persistence, timers, clipboard — through the effects channel, and results come back as ordinary messages. In a TypeScript core, effects are `Cmd` data returned from `update` and recurring timers are declared `Sub` data — see [TypeScript Cores: Effects](/docs/typescript#effects-are-cmd-data). In a Zig core, declare `.update_fx` instead of `.update` and spawn from message arms; boot-time work goes in `.init_fx`, which runs exactly once before the first paint. See [Native UI: Effects](/docs/native-ui#effects).
|
||||
|
||||
## Dropping down
|
||||
|
||||
`UiApp` is a layer over the lower-level `App`/`Runtime` pair, which any app can use directly — for custom lifecycle callbacks, imperative window and view management, or embedding [web content](/frontend). The [App & Runtime](/runtime) reference documents that layer, and [Embedded App](/embed) covers driving the runtime from an existing host (including iOS and Android).
|
||||
`UiApp` is a layer over the lower-level `App`/`Runtime` pair, which any app can use directly — for custom lifecycle callbacks, imperative window and view management, or embedding [web content](/docs/frontend). The [App & Runtime](/docs/runtime) reference documents that layer, and [Embedded App](/docs/embed) covers driving the runtime from an existing host (including iOS and Android).
|
||||
@@ -45,7 +45,7 @@ The manifest `native init` generates — identity, one shell window with a GPU s
|
||||
|
||||
## Example: app with web content, menus, and shortcuts
|
||||
|
||||
A fuller manifest for an app that also [embeds web content](/frontend) and declares commands, shortcuts, menus, and packaging metadata:
|
||||
A fuller manifest for an app that also [embeds web content](/docs/frontend) and declares commands, shortcuts, menus, and packaging metadata:
|
||||
|
||||
```zig:app.zon
|
||||
.{
|
||||
@@ -150,23 +150,23 @@ A fuller manifest for an app that also [embeds web content](/frontend) and decla
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>permissions</code></td>
|
||||
<td>Runtime permissions (see <a href="/security">Security</a>)</td>
|
||||
<td>Runtime permissions (see <a href="/docs/security">Security</a>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>capabilities</code></td>
|
||||
<td>Feature declarations (see <a href="/security">Security</a>)</td>
|
||||
<td>Feature declarations (see <a href="/docs/security">Security</a>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>bridge</code></td>
|
||||
<td>Bridge command policies (see <a href="/bridge">Bridge</a>)</td>
|
||||
<td>Bridge command policies (see <a href="/docs/bridge">Bridge</a>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>security</code></td>
|
||||
<td>Navigation and external link policies (see <a href="/security">Security</a>)</td>
|
||||
<td>Navigation and external link policies (see <a href="/docs/security">Security</a>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>web_engine</code></td>
|
||||
<td><code>system</code> or <code>chromium</code>; Chromium is currently supported for macOS builds (see <a href="/web-engines">Web Engines</a>)</td>
|
||||
<td><code>system</code> or <code>chromium</code>; Chromium is currently supported for macOS builds (see <a href="/docs/web-engines">Web Engines</a>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>webview_layer</code></td>
|
||||
@@ -174,7 +174,7 @@ A fuller manifest for an app that also [embeds web content](/frontend) and decla
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>theme</code></td>
|
||||
<td>Built-in theme pack: <code>house</code> (default) or <code>geist</code>; an unknown name is a build/check error (see <a href="/theming">Theming</a>)</td>
|
||||
<td>Built-in theme pack: <code>house</code> (default) or <code>geist</code>; an unknown name is a build/check error (see <a href="/docs/theming">Theming</a>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>theme_accent</code></td>
|
||||
@@ -190,7 +190,7 @@ A fuller manifest for an app that also [embeds web content](/frontend) and decla
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>windows</code></td>
|
||||
<td>Window definitions (see <a href="/windows">Windows</a>)</td>
|
||||
<td>Window definitions (see <a href="/docs/windows">Windows</a>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>shell</code></td>
|
||||
@@ -202,11 +202,11 @@ A fuller manifest for an app that also [embeds web content](/frontend) and decla
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>shortcuts</code></td>
|
||||
<td>Keyboard shortcuts delivered as <code>shortcut</code> events (see <a href="/keyboard-shortcuts">Keyboard Shortcuts</a>)</td>
|
||||
<td>Keyboard shortcuts delivered as <code>shortcut</code> events (see <a href="/docs/keyboard-shortcuts">Keyboard Shortcuts</a>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>menus</code></td>
|
||||
<td>Native menu declarations delivered through the command event path (see <a href="/menus">Menus</a>)</td>
|
||||
<td>Native menu declarations delivered through the command event path (see <a href="/docs/menus">Menus</a>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_associations</code></td>
|
||||
@@ -218,7 +218,7 @@ A fuller manifest for an app that also [embeds web content](/frontend) and decla
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>frontend</code></td>
|
||||
<td>Frontend build/dev config (see <a href="/frontend">Frontend Projects</a>)</td>
|
||||
<td>Frontend build/dev config (see <a href="/docs/frontend">Frontend Projects</a>)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -269,15 +269,15 @@ For a scene-first app — a `UiApp` passing its Zig scene (`shell_scene`) to the
|
||||
},
|
||||
```
|
||||
|
||||
Each window takes a `label` plus optional `title`, `width`, `height`, `x`, `y`, `resizable`, `restore_state`, `restore_policy` (`clamp_to_visible_screen` or `center_on_primary`), `min_width`/`min_height` (a content min-size floor the window itself enforces — macOS `contentMinSize`; the first shell window's declaration threads through the startup create like `titlebar`, negative values are a manifest error, 0 means no floor), and `titlebar` (`standard`, `hidden_inset`, `hidden_inset_tall` — the tall variant centers macOS's traffic lights in the 52pt unified band for toolbar-height headers — or `chromeless`, the fully-skinned opt-in that removes all OS chrome including the system buttons; only for apps that draw their own working window controls, see `examples/deck`). `titlebar = "hidden_inset"` hides the titlebar and extends content under it (macOS keeps the traffic lights) — the first shell window's declaration threads through the STARTUP window create, so the main window's chrome is right from the first frame; the app's own header then takes over dragging and inset padding through the `window-drag` attribute and the `on_chrome` hook (see <a href="/native-ui">Native UI</a>). Platforms without the concept keep standard chrome. The same `titlebar` field is accepted on top-level `windows` entries.
|
||||
Each window takes a `label` plus optional `title`, `width`, `height`, `x`, `y`, `resizable`, `restore_state`, `restore_policy` (`clamp_to_visible_screen` or `center_on_primary`), `min_width`/`min_height` (a content min-size floor the window itself enforces — macOS `contentMinSize`; the first shell window's declaration threads through the startup create like `titlebar`, negative values are a manifest error, 0 means no floor), and `titlebar` (`standard`, `hidden_inset`, `hidden_inset_tall` — the tall variant centers macOS's traffic lights in the 52pt unified band for toolbar-height headers — or `chromeless`, the fully-skinned opt-in that removes all OS chrome including the system buttons; only for apps that draw their own working window controls, see `examples/deck`). `titlebar = "hidden_inset"` hides the titlebar and extends content under it (macOS keeps the traffic lights) — the first shell window's declaration threads through the STARTUP window create, so the main window's chrome is right from the first frame; the app's own header then takes over dragging and inset padding through the `window-drag` attribute and the `on_chrome` hook (see <a href="/docs/native-ui">Native UI</a>). Platforms without the concept keep standard chrome. The same `titlebar` field is accepted on top-level `windows` entries.
|
||||
|
||||
Overlay presentation is also fixed at create time: `transparent` makes the top-level window alpha-capable, `always_on_top` selects its floating/topmost level, `click_through` passes pointer input to windows underneath, and `activate_on_show = false` reveals it without activating the app or taking focus. Canvas windows already use present-before-show, so these flags are applied while the window is hidden and its first alpha-correct frame becomes the first visible frame. Pair `transparent = true` with a non-opaque `gpu_alpha_mode`; `UiApp.WindowDescriptor` makes that canvas-alpha choice and uses an alpha-zero clear automatically. See the <a href="/windows#overlay-windows">overlay window recipe</a>. These fields are accepted on top-level `windows`, `shell.windows`, runtime `WindowCreateOptions`, and `UiApp.WindowDescriptor`.
|
||||
Overlay presentation is also fixed at create time: `transparent` makes the top-level window alpha-capable, `always_on_top` selects its floating/topmost level, `click_through` passes pointer input to windows underneath, and `activate_on_show = false` reveals it without activating the app or taking focus. Canvas windows already use present-before-show, so these flags are applied while the window is hidden and its first alpha-correct frame becomes the first visible frame. Pair `transparent = true` with a non-opaque `gpu_alpha_mode`; `UiApp.WindowDescriptor` makes that canvas-alpha choice and uses an alpha-zero clear automatically. See the <a href="/docs/windows#overlay-windows">overlay window recipe</a>. These fields are accepted on top-level `windows`, `shell.windows`, runtime `WindowCreateOptions`, and `UiApp.WindowDescriptor`.
|
||||
|
||||
Windows also take `close_policy` (`quit`, the default — the close affordance really closes, behavior unchanged for every existing app — or `hide`, the menu-bar-app shape: close hides the window and the app keeps running behind its status item). Like `titlebar`, close handling is host window state fixed at create, and the first shell window's declaration threads through the startup create. `hide` is supported on macOS and Windows — on Windows it additionally requires the `"tray"` capability (the tray icon is the only re-show affordance there; a declaration without it is refused at build time with a teaching). Linux has no status item to bring a hidden window back, so the declaration is refused at build time with a teaching. See <a href="/windows#close-policy">Windows</a> and the <a href="/tray#the-menu-bar-app-lifecycle">tray lifecycle recipe</a>. The same `close_policy` field is accepted on top-level `windows` entries.
|
||||
Windows also take `close_policy` (`quit`, the default — the close affordance really closes, behavior unchanged for every existing app — or `hide`, the menu-bar-app shape: close hides the window and the app keeps running behind its status item). Like `titlebar`, close handling is host window state fixed at create, and the first shell window's declaration threads through the startup create. `hide` is supported on macOS and Windows — on Windows it additionally requires the `"tray"` capability (the tray icon is the only re-show affordance there; a declaration without it is refused at build time with a teaching). Linux has no status item to bring a hidden window back, so the declaration is refused at build time with a teaching. See <a href="/docs/windows#close-policy">Windows</a> and the <a href="/docs/tray#the-menu-bar-app-lifecycle">tray lifecycle recipe</a>. The same `close_policy` field is accepted on top-level `windows` entries.
|
||||
|
||||
Supported `kind` values are `webview`, `toolbar`, `titlebar_accessory`, `sidebar`, `statusbar`, `split`, `stack`, `button`, `icon_button`, `list_item`, `checkbox`, `toggle`, `segmented_control`, `text_field`, `search_field`, `label`, `spacer`, `gpu_surface`, and `progress_indicator`.
|
||||
|
||||
Each view has a required `label` and `kind`. WebView views require `url`. Optional layout fields are `parent`, `edge`, `axis`, `x`, `y`, `width`, `height`, `min_width`, `min_height`, `max_width`, `max_height`, `fill`, and `layer`. Optional behavior and accessibility metadata are `visible`, `enabled`, `role`, `accessibility_label`, `text`, and `command`. `gpu_surface` views may also set `gpu_backend`, `gpu_pixel_format`, `gpu_present_mode`, `gpu_alpha_mode`, `gpu_color_space`, and `gpu_vsync`; those fields are rejected on non-GPU view kinds. The currently implemented macOS system-WebView backend uses `metal`, `bgra8_unorm`, `timer`, `opaque`, `srgb`, and `gpu_vsync = true`. `gpu_backend` also accepts `software` (the CPU reference-renderer path); on Linux and Windows system-WebView hosts any declared backend falls back to software presentation rather than erroring. The `axis` field accepts `row` or `column` on parent containers such as `toolbar`, `sidebar`, `split`, and `stack`; it defaults to `row`.
|
||||
Each view has a required `label` and `kind`. WebView views require `url`. Optional layout fields are `parent`, `edge`, `axis`, `x`, `y`, `width`, `height`, `min_width`, `min_height`, `max_width`, `max_height`, `fill`, and `layer`. Optional behavior and accessibility metadata are `visible`, `enabled`, `role`, `accessibility_label`, `text`, and `command`. `gpu_surface` views may also set `gpu_backend`, `gpu_pixel_format`, `gpu_present_mode`, `gpu_alpha_mode`, `gpu_color_space`, and `gpu_vsync`; those fields are rejected on non-GPU view kinds. The macOS system-WebView host presents with Metal, while the Windows host presents representable binary canvas packets with Direct2D/DirectWrite; frame events expose the concrete `metal` or `direct2d` backend. `gpu_backend` accepts `metal` or `software` as portable requests; omit it to select the default. Linux falls back to software presentation, and Windows uses software for unrepresentable commands, transparent layered windows, or when Direct2D is unavailable. The `axis` field accepts `row` or `column` on parent containers such as `toolbar`, `sidebar`, `split`, and `stack`; it defaults to `row`.
|
||||
|
||||
`createShellWindow` and `createShellViews` dock `edge` views against the remaining window content, let one or more top-level `fill` views use the final remaining rectangle, clamp resolved frames with any min/max size fields, and lay out parented controls such as toolbar buttons with small native defaults when explicit `x`, `y`, `width`, or `height` values are omitted. Parent containers flow omitted child positions horizontally with `axis = "row"` and vertically with `axis = "column"`. `split` containers use the same axis without inner spacing, so fixed-size children can sit beside a `fill` child. The runtime keeps the shell view slice as a layout binding and reapplies it when the window is resized, so pass data that lives for the lifetime of the window.
|
||||
|
||||
@@ -497,4 +497,4 @@ await window.zero.credentials.delete({
|
||||
});
|
||||
```
|
||||
|
||||
See also: [Multiple WebViews](/webviews) for frame and layer semantics, [Dialogs](/dialogs) for the full dialog type reference, [Capabilities](/capabilities) for OS capability support, and [Security](/security) for policy details.
|
||||
See also: [Multiple WebViews](/docs/webviews) for frame and layer semantics, [Dialogs](/docs/dialogs) for the full dialog type reference, [Capabilities](/docs/capabilities) for OS capability support, and [Security](/docs/security) for policy details.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Bridge
|
||||
|
||||
For apps that [embed web content](/frontend), the bridge connects JavaScript in the WebView to native Zig handlers via JSON messages. Native-rendered apps have no bridge — markup dispatches typed messages straight into `update` (see [App Model](/app-model)).
|
||||
For apps that [embed web content](/docs/frontend), the bridge connects JavaScript in the WebView to native Zig handlers via JSON messages. Native-rendered apps have no bridge — markup dispatches typed messages straight into `update` (see [App Model](/docs/app-model)).
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -175,4 +175,4 @@ try {
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
See also: [Builtin Commands](/bridge/builtin-commands) for `native-sdk.command.*`, `native-sdk.window.*`, `native-sdk.view.*`, `native-sdk.webview.*`, `native-sdk.dialog.*`, `native-sdk.os.*`, `native-sdk.clipboard.*`, and `native-sdk.credentials.*`.
|
||||
See also: [Builtin Commands](/docs/bridge/builtin-commands) for `native-sdk.command.*`, `native-sdk.window.*`, `native-sdk.view.*`, `native-sdk.webview.*`, `native-sdk.dialog.*`, `native-sdk.os.*`, `native-sdk.clipboard.*`, and `native-sdk.credentials.*`.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Building Components
|
||||
|
||||
The library's built-ins cover the common register, and [theming](/theming) restyles all of them at once. This page is about the pieces the library does not hand you: how to build a component of your own — first as a markup template, then as a Zig view function when the shape needs one — how it themes, and how component files spread across an app. Component code is toolkit-extension territory, so the Zig here applies whatever language the app core is written in: a TypeScript app that needs one custom widget writes that widget in Zig and keeps its core in TypeScript. The mechanics (template grammar, import rules, slots) are specified in [Native UI](/native-ui#templates); this page builds one real component end to end.
|
||||
The library's built-ins cover the common register, and [theming](/docs/theming) restyles all of them at once. This page is about the pieces the library does not hand you: how to build a component of your own — first as a markup template, then as a Zig view function when the shape needs one — how it themes, and how component files spread across an app. Component code is toolkit-extension territory, so the Zig here applies whatever language the app core is written in: a TypeScript app that needs one custom widget writes that widget in Zig and keeps its core in TypeScript. The mechanics (template grammar, import rules, slots) are specified in [Native UI](/docs/native-ui#templates); this page builds one real component end to end.
|
||||
|
||||
The ownership model in one line: **use and theme the built-ins by default; eject a library composite when you need to own its shape; build new composites from primitives when the library has no shape for it.** The last two are this page.
|
||||
|
||||
@@ -89,11 +89,11 @@ Args carry values; a `<slot/>` carries markup. A template body may mark one inse
|
||||
</column>
|
||||
```
|
||||
|
||||
This is the container-component pattern: the template owns the frame, the caller owns the content. The full rules (one slot per body, children without a slot are an error, ids hash as if inlined) are in [Native UI § Components](/native-ui#components).
|
||||
This is the container-component pattern: the template owns the frame, the caller owns the content. The full rules (one slot per body, children without a slot are an error, ids hash as if inlined) are in [Native UI § Components](/docs/native-ui#components).
|
||||
|
||||
## When a component needs Zig
|
||||
|
||||
The markup grammar is deliberately closed, and a few shapes sit outside it — the honest list is in [Native UI § Elements](/native-ui#elements): components that carry **image ids** (pixels registered at runtime; the avatar's `image="{binding}"` is the one declarative exception), **per-cell templates** (a data grid's arbitrary render-per-column callbacks), and **Zig-side floating surfaces** (`popover`, `menu_surface`; the anchored `dropdown-menu` covers the declarative case). Beyond those, anything needing per-state styling past tokens (`ElementOptions.style`) or logic past the expression language belongs in Zig.
|
||||
The markup grammar is deliberately closed, and a few shapes sit outside it — the honest list is in [Native UI § Elements](/docs/native-ui#elements): components that carry **image ids** (pixels registered at runtime; the avatar's `image="{binding}"` is the one declarative exception), **per-cell templates** (a data grid's arbitrary render-per-column callbacks), and **Zig-side floating surfaces** (`popover`, `menu_surface`; the anchored `dropdown-menu` covers the declarative case). Beyond those, anything needing per-state styling past tokens (`ElementOptions.style`) or logic past the expression language belongs in Zig.
|
||||
|
||||
A Zig component is just a function that takes the view builder and returns a node — the same primitives markup lowers to, with the same structural identity rules:
|
||||
|
||||
@@ -159,7 +159,7 @@ Zig components distribute the ordinary Zig way: a file per component (or a `comp
|
||||
|
||||
Three moves, in order of preference:
|
||||
|
||||
**Theme it.** Every built-in reads the token register, and [theme packs, overrides, and full design systems](/theming) reach every visual decision — colors, radii, control metrics, state washes, type. If your need is "the stepper, but in our palette," that is a token change, not a component.
|
||||
**Theme it.** Every built-in reads the token register, and [theme packs, overrides, and full design systems](/docs/theming) reach every visual decision — colors, radii, control metrics, state washes, type. If your need is "the stepper, but in our palette," that is a token change, not a component.
|
||||
|
||||
**Eject it.** When you need to own a composite's *shape* — reorder its parts, change its structure, grow it a feature — `native eject component <name>` writes the library composite's canonical source into `src/components/`, and from then on it is your code: edit freely, SDK updates never touch it. The ejectable set is exactly the library views that are honest compositions of primitives — today `stepper` and `timeline-item` (Zig view functions; their conditional structure and formatted text sit outside the markup grammar) and `timeline` (a markup template, since the container is pure composition). Each ejected file builds a widget tree identical to its library form at the moment of ejection — held by tests in the SDK — so ejecting changes ownership, never pixels. Ejected templates are reached through `<use template="timeline" ...>`, never a new element name, so the built-in element keeps working at unmigrated call sites; ejecting twice errors instead of overwriting your edits. Engine controls (buttons, text fields, tabs, ...) are deliberately not on the menu: their behavior lives in the runtime, and the way to change them is the token system, not a fork.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("built-in-components");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
# Built-in Components
|
||||
|
||||
This page moved. The catalog now has a full [Components section](/components) — one page per component with engine-rendered light/dark previews, validated Native markup, the Zig builder equivalent, and attribute tables generated from the markup vocabulary.
|
||||
This page moved. The catalog now has a full [Components section](/docs/components) — one page per component with engine-rendered light/dark previews, validated Native markup, the Zig builder equivalent, and attribute tables generated from the markup vocabulary.
|
||||
|
||||
The short version: the Native SDK ships a native-rendered component catalog with house-style defaults — neutral surfaces, Geist typography, subtle borders, focus states, and token-driven color, radius, shadow, blur, and motion — owned by the SDK and rendered through the retained canvas surface, not platform widget skins. Every component is expressible in Native markup through its element and programmatically through the `canvas.Ui` builder.
|
||||
|
||||
The canonical catalog lives in `native_sdk.canvas.builtin_component_kinds` and `builtin_component_names`; each descriptor reports its style, root widget kind, semantic role, and composite flag, and `native_sdk.canvas.builtinComponentWidget(...)` constructs a component's default foundation directly. See [Native UI](/native-ui) for markup semantics and the runtime contract.
|
||||
The canonical catalog lives in `native_sdk.canvas.builtin_component_kinds` and `builtin_component_names`; each descriptor reports its style, root widget kind, semantic role, and composite flag, and `native_sdk.canvas.builtinComponentWidget(...)` constructs a component's default foundation directly. See [Native UI](/docs/native-ui) for markup semantics and the runtime contract.
|
||||
@@ -0,0 +1,7 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("capabilities");
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
# Capabilities
|
||||
|
||||
Native SDK capabilities are native OS services and app events exposed through `PlatformServices`, runtime methods, lifecycle events, and — for apps that [embed web content](/frontend) — guarded bridge commands. Native code reaches them directly; web content does not receive capability access by default. In a [`UiApp`](/app-model), clipboard access rides the effects channel (`fx.writeClipboard` / `fx.readClipboard`) so `update` never needs a runtime handle.
|
||||
Native SDK capabilities are native OS services and app events exposed through `PlatformServices`, runtime methods, lifecycle events, and — for apps that [embed web content](/docs/frontend) — guarded bridge commands. Native code reaches them directly; web content does not receive capability access by default. In a [`UiApp`](/docs/app-model), clipboard access rides the effects channel (`fx.writeClipboard` / `fx.readClipboard`) so `update` never needs a runtime handle.
|
||||
|
||||
Web content itself is declare-to-use: an app ships the embedded web layer only when it declares web intent — `"webview"` in `.capabilities`, a `.frontend` block, a `.shell` webview view, or a web engine resolved to Chromium (`.web_engine = "chromium"` in app.zon, or the `-Dweb-engine`/`--web-engine` flags) — and an app that declares none of them builds native-only, where any attempt to create a webview fails with a teaching error instead of loading a layer the app never asked for. The [`webview_layer`](/app-zon) manifest field overrides the inference in either direction. Native-only builds shed the platform web stack for real: the Windows executable carries no `WebView2Loader.dll` reference, and the Linux host neither links WebKitGTK nor requires `libwebkitgtk` on user machines.
|
||||
Web content itself is declare-to-use: an app ships the embedded web layer only when it declares web intent — `"webview"` in `.capabilities`, a `.frontend` block, a `.shell` webview view, or a web engine resolved to Chromium (`.web_engine = "chromium"` in app.zon, or the `-Dweb-engine`/`--web-engine` flags) — and an app that declares none of them builds native-only, where any attempt to create a webview fails with a teaching error instead of loading a layer the app never asked for. The [`webview_layer`](/docs/app-zon) manifest field overrides the inference in either direction. Native-only builds shed the platform web stack for real: the Windows executable carries no `WebView2Loader.dll` reference, and the Linux host neither links WebKitGTK nor requires `libwebkitgtk` on user machines.
|
||||
|
||||
## Current capability pack
|
||||
|
||||
@@ -220,4 +220,4 @@ window.zero.on("app:activate", () => {
|
||||
});
|
||||
```
|
||||
|
||||
See also: [Builtin Commands](/bridge/builtin-commands) and [Security](/security).
|
||||
See also: [Builtin Commands](/docs/bridge/builtin-commands) and [Security](/docs/security).
|
||||
@@ -29,11 +29,11 @@ native dev [dir] --core [--script msgs.ndjson] [--watch]
|
||||
native dev --binary <path> [--manifest app.zon] [--url <url>] [--command "<cmd>"] [--timeout-ms <n>]
|
||||
```
|
||||
|
||||
Build and run the app in the current (or given) app directory — a Debug build by default, printing a one-line completion and naming any failing step. The markup hot-reload watcher and the Debug-only teaching diagnostics are compiled in only in Debug; pass `-Doptimize=...` to override. Apps with a frontend dev config also get the managed dev server — see [Dev Server](/cli/dev).
|
||||
Build and run the app in the current (or given) app directory — a Debug build by default, printing a one-line completion and naming any failing step. The markup hot-reload watcher and the Debug-only teaching diagnostics are compiled in only in Debug; pass `-Doptimize=...` to override. Apps with a frontend dev config also get the managed dev server — see [Dev Server](/docs/cli/dev).
|
||||
|
||||
<dl>
|
||||
<dt><code>--core</code></dt>
|
||||
<dd>Run the TypeScript core's logic loop under node instead of building the app: dispatch Msgs as JSON lines on stdin, watch the committed model and effect transcript, advance a virtual clock to fire timers. Honestly not a renderer — plain <code>native dev</code> runs the real app. See <a href="/typescript#the-dev-loop">TypeScript Cores</a>.</dd>
|
||||
<dd>Run the TypeScript core's logic loop under node instead of building the app: dispatch Msgs as JSON lines on stdin, watch the committed model and effect transcript, advance a virtual clock to fire timers. Honestly not a renderer — plain <code>native dev</code> runs the real app. See <a href="/docs/typescript#the-dev-loop">TypeScript Cores</a>.</dd>
|
||||
<dt><code>--script</code></dt>
|
||||
<dd>With <code>--core</code>: replay a newline-delimited JSON message file instead of reading stdin.</dd>
|
||||
<dt><code>--watch</code></dt>
|
||||
@@ -87,7 +87,7 @@ Validate the whole tree without building the app. A TypeScript core (`src/core.t
|
||||
native eject [dir]
|
||||
```
|
||||
|
||||
Write an owned `build.zig`/`build.zig.zon` into the app (once); the verbs then drive your files via `zig build`. Ejecting is only for owning the build files — it is never a prerequisite: zero-config apps build, test, and [package](/packaging) directly (`native package` works on the zero-config build as-is).
|
||||
Write an owned `build.zig`/`build.zig.zon` into the app (once); the verbs then drive your files via `zig build`. Ejecting is only for owning the build files — it is never a prerequisite: zero-config apps build, test, and [package](/docs/packaging) directly (`native package` works on the zero-config build as-is).
|
||||
|
||||
### `native eject component`
|
||||
|
||||
@@ -95,7 +95,7 @@ Write an owned `build.zig`/`build.zig.zon` into the app (once); the verbs then d
|
||||
native eject component <name> [dir]
|
||||
```
|
||||
|
||||
Write an owned copy of a library composite into `src/components/` (once, never overwriting — ejecting again errors with the file to delete first). Ejectable today: `stepper`, `timeline`, `timeline-item` — the library views that are honest compositions of primitives; engine controls are not on the menu (theme them through [tokens](/theming) instead). `timeline` lands as a markup template (use it via `<use template="timeline" ...>`), the others as Zig view functions; each file opens with a header comment walking through the call-site migration, and each builds a widget tree identical to its library form at the moment of ejection. Unknown names get a did-you-mean plus the full ejectable list. See [Building Components](/building-components#use-eject-or-build).
|
||||
Write an owned copy of a library composite into `src/components/` (once, never overwriting — ejecting again errors with the file to delete first). Ejectable today: `stepper`, `timeline`, `timeline-item` — the library views that are honest compositions of primitives; engine controls are not on the menu (theme them through [tokens](/docs/theming) instead). `timeline` lands as a markup template (use it via `<use template="timeline" ...>`), the others as Zig view functions; each file opens with a header comment walking through the call-site migration, and each builds a widget tree identical to its library form at the moment of ejection. Unknown names get a did-you-mean plus the full ejectable list. See [Building Components](/docs/building-components#use-eject-or-build).
|
||||
|
||||
### `native doctor`
|
||||
|
||||
@@ -103,7 +103,7 @@ Write an owned copy of a library composite into `src/components/` (once, never o
|
||||
native doctor [--strict] [--manifest app.zon] [--web-engine system|chromium] [--cef-dir path] [--cef-auto-install]
|
||||
```
|
||||
|
||||
Check host environment, WebView, manifest, and CEF. See [native doctor](/debugging/doctor) for what each check means.
|
||||
Check host environment, WebView, manifest, and CEF. See [native doctor](/docs/debugging/doctor) for what each check means.
|
||||
|
||||
### `native validate`
|
||||
|
||||
@@ -200,7 +200,7 @@ Work with Native markup files directly, outside the app verbs.
|
||||
native automate <command>
|
||||
```
|
||||
|
||||
Interact with the automation server of a running automation-enabled app. See [Automation](/automation) for the full workflow.
|
||||
Interact with the automation server of a running automation-enabled app. See [Automation](/docs/automation) for the full workflow.
|
||||
|
||||
<dl>
|
||||
<dt><code>automate list</code></dt>
|
||||
@@ -259,7 +259,7 @@ native skills get <name> [--full]
|
||||
native skills get --all [--full]
|
||||
```
|
||||
|
||||
List and print the built-in AI agent skills the CLI ships — the version-matched content the `npx skills add vercel-labs/native` discovery skill loads from. See [Agent Skills](/skills) for the one-command agent install, what each skill covers, and how to deliver one to an agent.
|
||||
List and print the built-in AI agent skills the CLI ships — the version-matched content the `npx skills add vercel-labs/native` discovery skill loads from. See [Agent Skills](/docs/skills) for the one-command agent install, what each skill covers, and how to deliver one to an agent.
|
||||
|
||||
<dl>
|
||||
<dt><code>skills list</code></dt>
|
||||
@@ -4,7 +4,7 @@ import { CodeToggle } from "@/components/code-toggle";
|
||||
|
||||
# Accordion
|
||||
|
||||
A disclosure surface with a header. The header label comes from the `text` attribute, children show while `selected` is true, and a header press dispatches `on-toggle` — the model owns the open state, in the usual [model–message loop](/native-ui). Items render the house accordion look — borderless rows divided by hairline separators, with the chevron on the trailing edge rotating as the item expands — and size themselves: the header band alone while closed, header plus content while open, so a toggle reflows the column with no hand-managed heights.
|
||||
A disclosure surface with a header. The header label comes from the `text` attribute, children show while `selected` is true, and a header press dispatches `on-toggle` — the model owns the open state, in the usual [model–message loop](/docs/native-ui). Items render the house accordion look — borderless rows divided by hairline separators, with the chevron on the trailing edge rotating as the item expands — and size themselves: the header band alone while closed, header plus content while open, so a toggle reflows the column with no hand-managed heights.
|
||||
|
||||
<ComponentPreview name="accordion" alt="Open and closed accordion items rendered by the engine" caption="an open item and a closed one" />
|
||||
|
||||
@@ -18,7 +18,7 @@ An inline callout surface. The title comes from the `text` attribute and renders
|
||||
</column>
|
||||
```
|
||||
|
||||
For a message the user can dismiss, render the alert under an `if` and clear the flag from a button inside it — the model owns the visibility, as with every surface. For modal interruptions reach for [dialog](/components/dialog), [drawer](/components/drawer), or [sheet](/components/sheet) instead.
|
||||
For a message the user can dismiss, render the alert under an `if` and clear the flag from a button inside it — the model owns the visibility, as with every surface. For modal interruptions reach for [dialog](/docs/components/dialog), [drawer](/docs/components/drawer), or [sheet](/docs/components/sheet) instead.
|
||||
|
||||
## Programmatic construction (Zig)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AttrTable } from "@/components/attr-table";
|
||||
|
||||
# Badge
|
||||
|
||||
A text leaf for counts and statuses: the content is the label (with `{}` interpolation), `variant` picks the color treatment, and an optional `icon` draws a built-in vector icon inline before the text. Badges are not interactive — for a pressable chip see [toggle-button](/components/toggle) or [button](/components/button).
|
||||
A text leaf for counts and statuses: the content is the label (with `{}` interpolation), `variant` picks the color treatment, and an optional `icon` draws a built-in vector icon inline before the text. Badges are not interactive — for a pressable chip see [toggle-button](/docs/components/toggle) or [button](/docs/components/button).
|
||||
|
||||
<ComponentPreview name="badge" alt="Badge variants rendered by the engine" caption="default, secondary, outline, destructive, and icon badges" />
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AttrTable } from "@/components/attr-table";
|
||||
|
||||
# Breadcrumb
|
||||
|
||||
A row container for a hierarchy trail — children flow horizontally, and the trail is plain composition: muted `text` leaves for the ancestors, a muted `chevron-right` [icon](/components/icon) between them, and an unmuted leaf for the current page. Binding `on-press` on a text leaf makes it pressable, so each ancestor can navigate.
|
||||
A row container for a hierarchy trail — children flow horizontally, and the trail is plain composition: muted `text` leaves for the ancestors, a muted `chevron-right` [icon](/docs/components/icon) between them, and an unmuted leaf for the current page. Binding `on-press` on a text leaf makes it pressable, so each ancestor can navigate.
|
||||
|
||||
<ComponentPreview name="breadcrumb" alt="A breadcrumb trail rendered by the engine" caption="muted ancestors, chevron separators, and the current page" />
|
||||
|
||||
@@ -3,9 +3,9 @@ import { AttrTable } from "@/components/attr-table";
|
||||
|
||||
# Button Group
|
||||
|
||||
A row container that attaches related action [buttons](/components/button) into one segmented bar. At the default gap of 0 the engine collapses the group into a single shape: the first segment keeps its leading corners, the last its trailing corners, middles square off, and each interior boundary is drawn by exactly one shared 1px seam. Every segment stays its own button — its own label, its own `on-press`, its own disabled state.
|
||||
A row container that attaches related action [buttons](/docs/components/button) into one segmented bar. At the default gap of 0 the engine collapses the group into a single shape: the first segment keeps its leading corners, the last its trailing corners, middles square off, and each interior boundary is drawn by exactly one shared 1px seam. Every segment stays its own button — its own label, its own `on-press`, its own disabled state.
|
||||
|
||||
For an exclusive active segment use a [toggle-group](/components/toggle) (pressed state is the toggle family's job) or [tabs](/components/tabs); a button group is attached *actions*, never a choice.
|
||||
For an exclusive active segment use a [toggle-group](/docs/components/toggle) (pressed state is the toggle family's job) or [tabs](/docs/components/tabs); a button group is attached *actions*, never a choice.
|
||||
|
||||
<ComponentPreview name="button-group" alt="A flush button group rendered by the engine" caption="three outline actions attached into one bar with shared seams" />
|
||||
|
||||
@@ -33,7 +33,7 @@ Grouped buttons keep their variant's fill — a primary or secondary group reads
|
||||
|
||||
## Spacing out
|
||||
|
||||
`gap` above 0 opts out of the attached treatment: the children render as ordinary separate buttons with full corners and borders. Use a plain [row](/components/layout) if the actions are not related enough to attach.
|
||||
`gap` above 0 opts out of the attached treatment: the children render as ordinary separate buttons with full corners and borders. Use a plain [row](/docs/native-ui) if the actions are not related enough to attach.
|
||||
|
||||
```html
|
||||
<button-group gap="8">
|
||||
@@ -58,4 +58,4 @@ ui.el(.button_group, .{}, .{
|
||||
|
||||
<AttrTable attrs={["gap", "cross"]} />
|
||||
|
||||
Children are ordinary buttons; see the [button attributes](/components/button) for the per-segment surface.
|
||||
Children are ordinary buttons; see the [button attributes](/docs/components/button) for the per-segment surface.
|
||||
@@ -36,7 +36,7 @@ Four sizes: `sm`, `default`, `lg`, and `icon` — the icon size renders a square
|
||||
|
||||
## Icons
|
||||
|
||||
`icon` names a built-in vector icon (see the [icon registry](/components/icon)) drawn inline before the label — one hit target, one enabled/disabled tint.
|
||||
`icon` names a built-in vector icon (see the [icon registry](/docs/components/icon)) drawn inline before the label — one hit target, one enabled/disabled tint.
|
||||
|
||||
<ComponentPreview name="button-icons" alt="Buttons with inline icons rendered by the engine" />
|
||||
|
||||
@@ -56,7 +56,7 @@ Hover and press styling is engine-owned render state; `disabled` is a source att
|
||||
|
||||
## Button Group
|
||||
|
||||
[`button-group`](/components/button-group) attaches related action buttons into one segmented bar — flush segments, one shared corner language, one interior seam. It has its own page.
|
||||
[`button-group`](/docs/components/button-group) attaches related action buttons into one segmented bar — flush segments, one shared corner language, one interior seam. It has its own page.
|
||||
|
||||
## Programmatic construction (Zig)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AttrTable } from "@/components/attr-table";
|
||||
|
||||
# Card
|
||||
|
||||
`card` is the bordered, elevated surface container. It is a stacking container — children layer on top of each other and `gap` is rejected — so put a single `column` (or `row`) inside for flow. Cards carry 24px of content padding by default (the house inset; 16 at `size="sm"`) — set `padding` explicitly to override it. Binding `on-press` makes the whole surface pressable, the list-of-cards pattern. For the plain surface with the same stacking contract, see [panel](/components/panel).
|
||||
`card` is the bordered, elevated surface container. It is a stacking container — children layer on top of each other and `gap` is rejected — so put a single `column` (or `row`) inside for flow. Cards carry 24px of content padding by default (the house inset; 16 at `size="sm"`) — set `padding` explicitly to override it. Binding `on-press` makes the whole surface pressable, the list-of-cards pattern. For the plain surface with the same stacking contract, see [panel](/docs/components/panel).
|
||||
|
||||
<ComponentPreview name="card" alt="A card rendered by the engine" />
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CodeToggle } from "@/components/code-toggle";
|
||||
|
||||
# Checkbox
|
||||
|
||||
A binary value control: the label rides the `text` attribute — checkbox is not a text-bearing element, so text content between the tags is rejected with a teaching error (`label="..."` alone names one for accessibility without a visible label). The model binds `checked`, and `on-toggle` dispatches its Msg — the engine never flips state on its own. For a single choice among options, use [radio](/components/radio); for an on/off setting rendered as a sliding thumb, use [switch](/components/switch).
|
||||
A binary value control: the label rides the `text` attribute — checkbox is not a text-bearing element, so text content between the tags is rejected with a teaching error (`label="..."` alone names one for accessibility without a visible label). The model binds `checked`, and `on-toggle` dispatches its Msg — the engine never flips state on its own. For a single choice among options, use [radio](/docs/components/radio); for an on/off setting rendered as a sliding thumb, use [switch](/docs/components/switch).
|
||||
|
||||
<ComponentPreview name="checkbox" alt="Checkboxes rendered by the engine" caption="checked, unchecked, and disabled checkboxes" />
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { ComponentPreview } from "@/components/component-preview";
|
||||
import { AttrTable } from "@/components/attr-table";
|
||||
|
||||
# Code
|
||||
|
||||
Presents source text as bare monospace content with deterministic, theme-aware syntax highlighting. The component supplies no background, border, radius, shadow, or padding; wrap it in a `panel`, `card`, or another container when the surrounding design calls for chrome. Code is selectable and read-only by default. Add `editable` and `on-input` to opt into the multiline editor without losing syntax colors. Code wraps by default. Set `wrap="false"` to preserve logical lines inside one horizontal scroll region, and opt into logical line numbers with `line-numbers`.
|
||||
|
||||
HTML-family highlighting understands HTML, XML, SVG, JSX, and TSX structure: element or component tags, attributes, strings, comments, numbers, and JavaScript/TypeScript expressions receive distinct theme-token colors.
|
||||
|
||||
<ComponentPreview name="code" alt="A bare JSX code sample with syntax highlighting and line numbers" caption="Bare highlighted content with JSX tags, attributes, strings, expressions, and an unwrapped horizontal viewport" />
|
||||
|
||||
## Markup
|
||||
|
||||
```html
|
||||
<code
|
||||
source="{component_source}"
|
||||
language="tsx"
|
||||
line-numbers
|
||||
wrap="false"
|
||||
width="480"
|
||||
label="Accordion example"
|
||||
/>
|
||||
```
|
||||
|
||||
`source` is required and must be one `{binding}` producing text. `language` is a literal lexer name; unknown names are validation errors. Line numbers are off by default and remain decorative, so selecting and copying a numbered block returns only the source text. Numbered presentation is limited to 128 logical lines; longer sources keep all code and omit the gutter.
|
||||
|
||||
## Added and removed lines
|
||||
|
||||
Diff presentation follows Geist Code Block in the default and Geist theme packs, across light and dark appearances. `added-lines` and `removed-lines` apply full-width green/red washes and renderer-owned `+`/`-` markers while the underlying source, syntax highlighting, selection, and copied text stay unchanged.
|
||||
|
||||
<ComponentPreview name="code-diff" alt="A JavaScript configuration diff with green added and red removed lines" caption="Geist-style added and removed lines over ordinary JavaScript highlighting" />
|
||||
|
||||
```html
|
||||
<code
|
||||
source="{migration_source}"
|
||||
language="javascript"
|
||||
line-numbers
|
||||
added-lines="5"
|
||||
removed-lines="2-4"
|
||||
wrap="false"
|
||||
width="480"
|
||||
label="Configuration migration"
|
||||
/>
|
||||
```
|
||||
|
||||
Line specs are one-based comma lists and inclusive ranges: `added-lines="5, 9-11"`. They annotate clean source—the `+` and `-` are decoration, not bytes callers must splice into the model. This keeps the selected/copied result usable and lets `language` continue highlighting the real grammar. A line cannot be both added and removed. Diff metadata is bounded to lines 1–128; read-only sources longer than 128 lines keep every source byte and omit the diff treatment.
|
||||
|
||||
For editable code, apply each `TextInputEvent` to the same model-owned buffer that supplies `source`:
|
||||
|
||||
```html
|
||||
<code
|
||||
source="{document}"
|
||||
language="tsx"
|
||||
editable
|
||||
on-input="edit_document"
|
||||
line-numbers
|
||||
wrap="false"
|
||||
grow="1"
|
||||
label="Document editor"
|
||||
/>
|
||||
```
|
||||
|
||||
The editor path includes multiline selection, caret navigation, IME, clipboard, and undo/redo behavior. It does not add textarea background, border, focus ring, or padding.
|
||||
|
||||
Surface styling belongs to a wrapper:
|
||||
|
||||
```html
|
||||
<panel padding="12">
|
||||
<code source="{component_source}" language="tsx" />
|
||||
</panel>
|
||||
```
|
||||
|
||||
## Programmatic construction (Zig)
|
||||
|
||||
```zig
|
||||
ui.code(.{
|
||||
.language = .javascript,
|
||||
.line_numbers = true,
|
||||
.added_lines = &.{5},
|
||||
.removed_lines = &.{ 2, 3, 4 },
|
||||
.wrap = false,
|
||||
.width = 480,
|
||||
.semantics = .{ .label = "Configuration migration" },
|
||||
}, model.migration_source)
|
||||
```
|
||||
|
||||
The editable path uses the same `added_lines` and `removed_lines` options when an editor needs annotations; keep those line numbers synchronized as edits change the document.
|
||||
|
||||
The Zig builder composes the same way when chrome is wanted:
|
||||
|
||||
```zig
|
||||
ui.panel(.{ .padding = 12 }, .{
|
||||
ui.code(.{ .language = .html }, model.component_source),
|
||||
})
|
||||
```
|
||||
|
||||
The public lexer model is `native_sdk.canvas.code`. `languageFromName` resolves markup spellings, `languageFromFence` reads a Markdown info string, and `highlight` produces the same bounded, theme-colored span runs both renderers use.
|
||||
|
||||
## Languages
|
||||
|
||||
Zig; JavaScript and TypeScript; JSX and TSX; JSON; YAML; shell; Python; Rust; C, C++, C#, Java, Kotlin, and Swift; Go; HTML, XML, and SVG; CSS, SCSS, and Less; SQL; and Markdown. An omitted language renders plain monospace.
|
||||
|
||||
## Attributes
|
||||
|
||||
<AttrTable element="code" attrs={["source", "language", "editable", "on-input", "line-numbers", "added-lines", "removed-lines", "wrap", "width", "height", "min-width", "grow", "key", "global-key", "label"]} />
|
||||
@@ -3,7 +3,7 @@ import { AttrTable } from "@/components/attr-table";
|
||||
|
||||
# Combobox
|
||||
|
||||
`combobox` is a trigger-only primitive like [select](/components/select), but the trigger is a text entry with a menu affordance: `on-input` names a Msg variant that receives every edit as a text-input event (`canvas.TextInputEvent` in a Zig core; the `TextInputEvent` union from `@native-sdk/core/text` in a TypeScript core), and the model filters the options as the user types. The options themselves are composed the same way as the select's — an anchored [dropdown-menu](/components/dropdown-menu) of menu-items beside the trigger in a `stack`, rendered under an `if`, with `on-dismiss` clearing the model's open flag when Escape or a click outside closes the surface.
|
||||
`combobox` is a trigger-only primitive like [select](/docs/components/select), but the trigger is a text entry with a menu affordance: `on-input` names a Msg variant that receives every edit as a text-input event (`canvas.TextInputEvent` in a Zig core; the `TextInputEvent` union from `@native-sdk/core/text` in a TypeScript core), and the model filters the options as the user types. The options themselves are composed the same way as the select's — an anchored [dropdown-menu](/docs/components/dropdown-menu) of menu-items beside the trigger in a `stack`, rendered under an `if`, with `on-dismiss` clearing the model's open flag when Escape or a click outside closes the surface.
|
||||
|
||||
<ComponentPreview name="combobox" alt="A combobox rendered by the engine" caption="a combobox trigger with its search placeholder" />
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CodeToggle } from "@/components/code-toggle";
|
||||
|
||||
# Dialog
|
||||
|
||||
A modal dialog surface rendered in place: the title comes from the `text` attribute, and visibility is model-owned — wrap the dialog in an `if` on an open flag. `on-dismiss` dispatches when Escape or a click outside dismisses the surface, so `update` clears the flag; the engine hides the surface immediately as an optimistic echo, and the source tree wins on the next rebuild. The title is drawn by the surface chrome and children stack over the full content box, so lead the body column with a fixed-height spacer that clears the title line. For edge-anchored surfaces with the same contract, see [drawer](/components/drawer) and [sheet](/components/sheet).
|
||||
A modal dialog surface rendered in place: the title comes from the `text` attribute, and visibility is model-owned — wrap the dialog in an `if` on an open flag. `on-dismiss` dispatches when Escape or a click outside dismisses the surface, so `update` clears the flag; the engine hides the surface immediately as an optimistic echo, and the source tree wins on the next rebuild. The title is drawn by the surface chrome and children stack over the full content box, so lead the body column with a fixed-height spacer that clears the title line. For edge-anchored surfaces with the same contract, see [drawer](/docs/components/drawer) and [sheet](/docs/components/sheet).
|
||||
|
||||
<ComponentPreview name="dialog" alt="A modal dialog rendered by the engine" caption="title chrome, body content, and a trailing action row" />
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AttrTable } from "@/components/attr-table";
|
||||
|
||||
# Drawer
|
||||
|
||||
A side-anchored surface with the same contract as [dialog](/components/dialog): rendered in place, title via the `text` attribute, visibility model-owned behind an `if`, and `on-dismiss` dispatched on Escape or click-outside so `update` clears the open flag. As with the dialog, the title chrome is engine-drawn and children stack over the full content box, so lead the body column with a fixed-height spacer. For the bottom-edge variant, see [sheet](/components/sheet).
|
||||
A side-anchored surface with the same contract as [dialog](/docs/components/dialog): rendered in place, title via the `text` attribute, visibility model-owned behind an `if`, and `on-dismiss` dispatched on Escape or click-outside so `update` clears the open flag. As with the dialog, the title chrome is engine-drawn and children stack over the full content box, so lead the body column with a fixed-height spacer. For the bottom-edge variant, see [sheet](/docs/components/sheet).
|
||||
|
||||
<ComponentPreview name="drawer" alt="A drawer surface rendered by the engine" caption="a side drawer with a title and stacked controls" />
|
||||
|
||||
@@ -24,7 +24,7 @@ A vertical menu surface whose children are `menu-item` elements. Setting `anchor
|
||||
</stack>
|
||||
```
|
||||
|
||||
Handle the item's Msg in `update` and clear the open flag there — a menu press does not dismiss implicitly. [Select](/components/select) and [combobox](/components/combobox) build their option lists from exactly this pattern (with `anchor-alignment="stretch"` for the select-menu width).
|
||||
Handle the item's Msg in `update` and clear the open flag there — a menu press does not dismiss implicitly. [Select](/docs/components/select) and [combobox](/docs/components/combobox) build their option lists from exactly this pattern (with `anchor-alignment="stretch"` for the select-menu width).
|
||||
|
||||
## Programmatic construction (Zig)
|
||||
|
||||
@@ -63,7 +63,7 @@ ui.el(.menu_surface, .{ .min_width = 220 }, .{
|
||||
})
|
||||
```
|
||||
|
||||
Right-click context menus are a separate channel with its own element: a `<context-menu>` child on a pressable element (or `ElementOptions.context_menu` in Zig views) presents the platform's native menu — `NSMenu` on macOS, `TrackPopupMenu` on Windows, `GtkPopoverMenu` on Linux — and falls back to an anchored surface at the click point on hosts without one; selections dispatch typed Msgs. The anchored `dropdown-menu` here is for app-designed surfaces you open yourself (pickers, button dropdowns, hold-reveal menus) — see [Menus](/menus).
|
||||
Right-click context menus are a separate channel with its own element: a `<context-menu>` child on a pressable element (or `ElementOptions.context_menu` in Zig views) presents the platform's native menu — `NSMenu` on macOS, `TrackPopupMenu` on Windows, `GtkPopoverMenu` on Linux — and falls back to an anchored surface at the click point on hosts without one; selections dispatch typed Msgs. The anchored `dropdown-menu` here is for app-designed surfaces you open yourself (pickers, button dropdowns, hold-reveal menus) — see [Menus](/docs/menus).
|
||||
|
||||
## Attributes
|
||||
|
||||
@@ -4,7 +4,7 @@ import { IconGallery } from "@/components/icon-gallery";
|
||||
|
||||
# Icon
|
||||
|
||||
A vector icon leaf: `name` selects the icon, tint comes from the `foreground` color token, and size from `width`/`height` (square by default). A bare literal name is one of the curated built-in stroke icons — the markup compiler and the Zig builder both validate it at comptime against the registry, so icon references never rot. The same grammar powers the inline `icon` attribute on [button](/components/button), toggle-button, list-item, menu-item, and badge, where the icon draws as part of the control's single hit target.
|
||||
A vector icon leaf: `name` selects the icon, tint comes from the `foreground` color token, and size from `width`/`height` (square by default). A bare literal name is one of the curated built-in stroke icons — the markup compiler and the Zig builder both validate it at comptime against the registry, so icon references never rot. The same grammar powers the inline `icon` attribute on [button](/docs/components/button), toggle-button, list-item, menu-item, and badge, where the icon draws as part of the control's single hit target.
|
||||
|
||||
<ComponentPreview name="icon" alt="Built-in vector icons rendered by the engine" caption="a row of registry icons at the default size and tint" />
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AttrTable } from "@/components/attr-table";
|
||||
|
||||
# Input Group
|
||||
|
||||
The composer shape: one bordered field wrapping a [textarea](/components/textarea) plus an accessory row of controls inside the same border — attach on the bottom-left, send on the bottom-right. The group wears the focus ring whenever focus is on any control inside it, and the textarea's own chrome dissolves automatically, so the whole group reads as a single field. The textarea keeps its full behavior: `text` and `placeholder` bind from the model, `on-input` hears every edit, `on-submit` rides the primary chord, and `autofocus` lands the keyboard on mount.
|
||||
The composer shape: one bordered field wrapping a [textarea](/docs/components/textarea) plus an accessory row of controls inside the same border — attach on the bottom-left, send on the bottom-right. The group wears the focus ring whenever focus is on any control inside it, and the textarea's own chrome dissolves automatically, so the whole group reads as a single field. The textarea keeps its full behavior: `text` and `placeholder` bind from the model, `on-input` hears every edit, `on-submit` rides the primary chord, and `autofocus` lands the keyboard on mount.
|
||||
|
||||
<ComponentPreview name="input-group" alt="An input group rendered by the engine" />
|
||||
|
||||
@@ -18,7 +18,7 @@ Single-line text entry. `input` and `text-field` are the same foundation under t
|
||||
</column>
|
||||
```
|
||||
|
||||
The core side is a text field the control renders and one arm reducing each edit over it — [Native UI § Messages](/native-ui#messages) walks the whole contract:
|
||||
The core side is a text field the control renders and one arm reducing each edit over it — [Native UI § Messages](/docs/native-ui#messages) walks the whole contract:
|
||||
|
||||
<CodeToggle>
|
||||
|
||||
@@ -45,7 +45,7 @@ pub fn email(model: *const Model) []const u8 {
|
||||
|
||||
## Search field
|
||||
|
||||
`search-field` renders the search affordance but binds exactly like an input; pair it with a model-filtered list. Whenever the field holds text it also shows a built-in clear affordance — a small x inside its trailing edge — and pressing it (or pressing Escape while focused) clears through the standard text-edit path, so the `on-input` handler receives the clear like any other edit and a model-owned buffer empties with it. No attribute enables or disables this; searchable fields simply carry it. For text entry that opens a menu of suggestions, see [combobox](/components/combobox).
|
||||
`search-field` renders the search affordance but binds exactly like an input; pair it with a model-filtered list. Whenever the field holds text it also shows a built-in clear affordance — a small x inside its trailing edge — and pressing it (or pressing Escape while focused) clears through the standard text-edit path, so the `on-input` handler receives the clear like any other edit and a model-owned buffer empties with it. No attribute enables or disables this; searchable fields simply carry it. For text entry that opens a menu of suggestions, see [combobox](/docs/components/combobox).
|
||||
|
||||
<ComponentPreview name="search-field" alt="A search field rendered by the engine" />
|
||||
|
||||
@@ -20,7 +20,18 @@ A vertical stack of items. The rows are `list-item` text leaves — the label is
|
||||
|
||||
## Selection and activation
|
||||
|
||||
When selection is model state — the accent row your app owns — rows use the desktop gesture split: a single click selects (`on-press`), and the primary action (open the record, play the track) rides the double click and Enter. Bind the primary action twice — `on_double_press` for the pointer (Zig builder only; markup has no double-click event) and `on-submit` for the keyboard:
|
||||
When selection is model state — the accent row your app owns — rows use the desktop gesture split: a single click selects (`on-press`), and the primary action (open the record, play the track) rides the double click and Enter. In markup, bind the primary action to both `on-double-press` and `on-submit`:
|
||||
|
||||
```html
|
||||
<list-item
|
||||
selected="{selected_id == track.id}"
|
||||
on-press="select_track:{track.id}"
|
||||
on-double-press="play_track:{track.id}"
|
||||
on-submit="play_track:{track.id}"
|
||||
>{track.title}</list-item>
|
||||
```
|
||||
|
||||
The Zig builder spells the same channels with underscores:
|
||||
|
||||
```zig
|
||||
ui.listItem(.{
|
||||
@@ -33,11 +44,11 @@ ui.listItem(.{
|
||||
|
||||
The double click is additive, never a delay: the first click dispatches the select on its own release, the second release dispatches the play — select-then-act, with no press timer. On the keyboard, a bound `on-submit` makes plain Enter the row's primary action while Space keeps select; rows without one resolve Enter as select, unchanged.
|
||||
|
||||
The arrows stay app-owned after clicking around: pointer focus on a plain list row is quiet (no ring), and a quietly focused row routes no keys — arrows and Enter fall through to the app-level key fallback, where a selection-owning app moves its own selection. Tab onto a row draws the ring and restores the row's full keymap (arrows walk the rows, Space selects, Enter submits), and rows carrying `role="treeitem"` keep the [tree](/components/tree)'s roving keymap under either register. The full routing order is in [Native UI § Keyboard routing](/native-ui#keyboard-routing-focus-registers-quiet-list-rows-and-the-app-level-fallback); `examples/soundboard`'s track lists are the live reference for the whole pattern.
|
||||
The arrows stay app-owned after clicking around: pointer focus on a plain list row is quiet (no ring), and a quietly focused row routes no keys — arrows and Enter fall through to the app-level key fallback, where a selection-owning app moves its own selection. Tab onto a row draws the ring and restores the row's full keymap (arrows walk the rows, Space selects, Enter submits), and rows carrying `role="treeitem"` keep the [tree](/docs/components/tree)'s roving keymap under either register. The full routing order is in [Native UI § Keyboard routing](/docs/native-ui#keyboard-routing-focus-registers-quiet-list-rows-and-the-app-level-fallback); `examples/soundboard`'s track lists are the live reference for the whole pattern.
|
||||
|
||||
## Virtualization
|
||||
|
||||
For long row sets, turn on `virtualized` and give each row a fixed `virtual-item-extent` — the engine lays out only the rows in view. Loop the model data with a keyed `for` and dispatch a payload per row. The rows still all BUILD (this bounds layout and paint, not the tree), so it suits row sets the model already holds — hundreds, not hundreds of thousands. For dataset-scale rows where the view should only ever build the visible window, use the builder's [virtual list](/components/virtual-list).
|
||||
For long row sets, turn on `virtualized` and give each row a fixed `virtual-item-extent` — the engine lays out only the rows in view. Loop the model data with a keyed `for` and dispatch a payload per row. The rows still all BUILD (this bounds layout and paint, not the tree), so it suits row sets the model already holds — hundreds, not hundreds of thousands. For dataset-scale rows where the view should only ever build the visible window, use the builder's [virtual list](/docs/components/virtual-list).
|
||||
|
||||
```html
|
||||
<list virtualized="true" virtual-item-extent="28" grow="1">
|
||||
@@ -68,4 +79,4 @@ List:
|
||||
|
||||
List-item:
|
||||
|
||||
<AttrTable attrs={["text", "icon", "selected", "disabled", "on-press", "on-submit"]} />
|
||||
<AttrTable attrs={["text", "icon", "selected", "disabled", "on-press", "on-double-press", "on-submit"]} />
|
||||
@@ -3,7 +3,7 @@ import { AttrTable } from "@/components/attr-table";
|
||||
|
||||
# Markdown
|
||||
|
||||
Renders a markdown string (a GFM subset, pipe tables included) as native widgets through the same text pipeline as every other component — deterministic layout, selectable text. `source` is required and must be one `{binding}`; the element takes no children. Links dispatch `on-link` with the URL as payload (bare URLs autolink), `<details>` blocks toggle through `on-details` plus a model-owned `details-expanded` flag list, and `#123` references linkify through `issue-link-base`. Fenced blocks lower through the reusable [Code](/components/code) component, so indentation and syntax behavior stay identical.
|
||||
Renders a markdown string (a GFM subset, pipe tables included) as native widgets through the same text pipeline as every other component — deterministic layout, selectable text. `source` is required and must be one `{binding}`; the element takes no children. Links dispatch `on-link` with the URL as payload (bare URLs autolink), `<details>` blocks toggle through `on-details` plus a model-owned `details-expanded` flag list, and `#123` references linkify through `issue-link-base`. Fenced blocks lower through the reusable [Code](/docs/components/code) component, so indentation and syntax behavior stay identical.
|
||||
|
||||
<ComponentPreview name="markdown" alt="A markdown document rendered by the engine" caption="headings, emphasis, inline code, lists, links, and a code block" />
|
||||
|
||||