diff --git a/.github/assets/calculator-dark.webp b/.github/assets/calculator-dark.webp new file mode 100644 index 00000000..d83aeaa3 Binary files /dev/null and b/.github/assets/calculator-dark.webp differ diff --git a/.github/assets/calculator-light.webp b/.github/assets/calculator-light.webp new file mode 100644 index 00000000..52093a2f Binary files /dev/null and b/.github/assets/calculator-light.webp differ diff --git a/.github/assets/notes-dark.webp b/.github/assets/notes-dark.webp new file mode 100644 index 00000000..3b187109 Binary files /dev/null and b/.github/assets/notes-dark.webp differ diff --git a/.github/assets/notes-light.webp b/.github/assets/notes-light.webp new file mode 100644 index 00000000..0deef69c Binary files /dev/null and b/.github/assets/notes-light.webp differ diff --git a/.github/assets/soundboard-dark.webp b/.github/assets/soundboard-dark.webp new file mode 100644 index 00000000..517b4ee5 Binary files /dev/null and b/.github/assets/soundboard-dark.webp differ diff --git a/.github/assets/soundboard-light.webp b/.github/assets/soundboard-light.webp new file mode 100644 index 00000000..1e7ac510 Binary files /dev/null and b/.github/assets/soundboard-light.webp differ diff --git a/.github/scripts/linux-canvas-smoke.sh b/.github/scripts/linux-canvas-smoke.sh new file mode 100755 index 00000000..d5ba70a9 --- /dev/null +++ b/.github/scripts/linux-canvas-smoke.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# Linux canvas smoke under Xvfb. +# +# Exercises the Linux gpu_surface software path against system WebKitGTK +# without a display server: builds examples/ui-inbox with -Dplatform=linux +# -Dweb-engine=system -Dautomation=true, runs it under Xvfb, and asserts +# against the automation snapshot: +# +# 1. snapshot ready=true (app booted, automation server live) +# 2. gpu_backend=software (the software present path is active) +# 3. gpu_nonblank=true (real pixels were presented) +# 4. widget-click "Add task" -> '4 open' (automation input mutates state) +# 5. automate screenshot renders a non-empty PNG +# +# Deliberately NOT `set -e` (same as windows-canvas-smoke.sh): grep exits 1 +# on zero matches, and under `set -e` an assignment like `x=$(grep ...)` or +# a swallowed `$(cli 2>&1)` capture dies with NO output — this job failed +# three times with nothing in the log but the exit code. Every assertion +# goes through fail(), which dumps the snapshot and the app log. +set -u + +# WebKitGTK's bubblewrap sandbox needs unprivileged user namespaces, which +# ubuntu-24.04 runners restrict via AppArmor; without this the web process +# dies launching xdg-dbus-proxy and the app never publishes an automation +# snapshot (reproduced in a local container: ready=true never lands; with +# the sandbox disabled the full smoke passes). The sandbox is not what +# this smoke tests. +export WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS="${WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS:-1}" + +# GTK_A11Y=none: under Xvfb there is no session bus providing org.a11y.Bus, +# and GTK4's a11y init blocks ~25 s on the GDBus name lookup before warning +# and continuing — the app's first runtime event landed after the readiness +# window had already expired (reproduced in a local container: without this +# the wait times out at startup; with it the full smoke passes). +# Accessibility is not what this smoke tests. +export GTK_A11Y="${GTK_A11Y:-none}" + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +app_dir="$repo_root/examples/ui-inbox" +snap="$app_dir/.zig-cache/native-sdk-automation/snapshot.txt" +cli="$repo_root/zig-out/bin/native" +app_log="${TMPDIR:-/tmp}/linux-canvas-smoke-app.log" + +# Readiness budget. Even with GTK_A11Y=none, shared ubuntu-24.04 runners +# show a consistent ~27 s stall between EGL init and the app's first +# runtime event (measured in runs 28690855597 pass / 28691951139 fail — +# the SAME stall in both; the old hard 30 s `automate wait` window flipped +# green/red on one or two seconds of runner noise). Local containers show +# no stall at all. Widen the budget here instead of weakening the CLI +# default; every correctness assertion stays strict. +ready_timeout_ms=90000 + +app_pid="" +cleanup() { + [ -n "$app_pid" ] && kill "$app_pid" >/dev/null 2>&1 + # xvfb-run does not forward signals to an already-detached app; reap the + # app and its Xvfb directly so local runs exit clean (CI would otherwise + # rely on the runner's orphan sweep). + pkill -f "$app_dir/zig-out/bin/ui-inbox" >/dev/null 2>&1 +} +trap cleanup EXIT + +diagnostics() { + echo "---- diagnostics ----" + echo "-- snapshot ($snap):" + if [ -f "$snap" ]; then tr '|' '\n' < "$snap" | sed 's/^/ /'; else echo " (missing)"; fi + echo "-- app log head ($app_log):" + head -20 "$app_log" 2>/dev/null | sed 's/^/ /' + echo "-- app log tail ($app_log):" + tail -40 "$app_log" 2>/dev/null | sed 's/^/ /' + echo "---------------------" +} + +fail() { + echo "FAIL: $1" + diagnostics + exit 1 +} + +# ---- build ---------------------------------------------------------------- +(cd "$repo_root" && zig build) || fail "root zig build (CLI) failed" +(cd "$app_dir" && zig build -Dplatform=linux -Dweb-engine=system -Dautomation=true) \ + || fail "ui-inbox Linux build failed" + +# ---- launch --------------------------------------------------------------- +cd "$app_dir" || fail "missing $app_dir" +rm -rf .zig-cache/native-sdk-automation +xvfb-run -a "$app_dir/zig-out/bin/ui-inbox" > "$app_log" 2>&1 & +app_pid=$! + +# ---- 1: automation snapshot becomes ready --------------------------------- +# `automate assert` self-reports on timeout (missing patterns + snapshot +# tail) and prints the measured latency on success, so green logs carry +# the readiness margin. +"$cli" automate assert --timeout-ms "$ready_timeout_ms" 'ready=true' \ + || fail "snapshot never became ready" + +# ---- 2 + 3: software backend presented non-blank pixels -------------------- +"$cli" automate assert --timeout-ms 30000 'gpu_nonblank=true' \ + || fail "gpu_nonblank never became true" +grep -q 'gpu_backend=software' "$snap" || fail "gpu_backend is not software" +echo "== canvas: $(grep -o 'gpu_backend=[a-z]*' "$snap" | head -1)" \ + "$(grep -o 'gpu_nonblank=[a-z]*' "$snap" | head -1)" + +# ---- 4: automation widget-click mutates the model -------------------------- +echo "== open before click: $(grep -oE '[0-9]+ open' "$snap" | head -1)" +add_id=$(grep -o 'widget @w1/inbox-canvas#[0-9]* role=button name="Add task"' "$snap" \ + | grep -o '#[0-9]*' | tr -d '#') +[ -n "$add_id" ] || fail "Add task button not found in snapshot" +"$cli" automate widget-click inbox-canvas "$add_id" || fail "CLI widget-click failed" +"$cli" automate assert --timeout-ms 30000 '4 open' \ + || fail "widget-click did not reach '4 open'" +echo "== open after click: $(grep -oE '[0-9]+ open' "$snap" | head -1)" + +# ---- 5: screenshot renders a non-empty PNG --------------------------------- +"$cli" automate screenshot inbox-canvas || fail "CLI screenshot failed" +test -s .zig-cache/native-sdk-automation/screenshot-inbox-canvas.png \ + || fail "screenshot PNG missing or empty" + +echo "PASS: linux canvas smoke" +exit 0 diff --git a/.github/scripts/windows-canvas-smoke.sh b/.github/scripts/windows-canvas-smoke.sh new file mode 100755 index 00000000..8e3e3321 --- /dev/null +++ b/.github/scripts/windows-canvas-smoke.sh @@ -0,0 +1,172 @@ +#!/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 +# 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) +# 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 -> +# WM_LBUTTONDOWN/WM_CHAR -> runtime) +# +# Step 5 (xdotool) is deliberately included: it is the only coverage of the +# Win32 pointer/keyboard input mapping in webview2_host.cpp. It is also the +# flakiest step (window lookup, focus without a window manager), so every +# failure path dumps the X window list, the snapshot, and the app log. +# +# Deliberately NOT `set -e`: grep exits 1 on zero matches inside the poll +# loops, and we want explicit, diagnosable failures instead of silent early +# exits. Every assertion goes through fail(), which dumps diagnostics. +set -u + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +app_dir="$repo_root/examples/ui-inbox" +snap="$app_dir/.zig-cache/native-sdk-automation/snapshot.txt" +cli="$repo_root/zig-out/bin/native" +app_log="${TMPDIR:-/tmp}/windows-canvas-smoke-app.log" + +# Wine needs an X display; when none is present (CI), re-exec the whole +# script under a private Xvfb server so the app and xdotool share it. The +# explicit screen size beats xvfb-run's 640x480x8 default: the app window is +# 720x520 and Wine wants a 24-bit visual. +if [ -z "${DISPLAY:-}" ]; then + exec xvfb-run -a --server-args="-screen 0 1280x800x24" "$0" "$@" +fi + +export WINEPREFIX="${WINEPREFIX:-$repo_root/.zig-cache/wineprefix}" +export WINEDEBUG="${WINEDEBUG:--all}" + +app_pid="" +cleanup() { + [ -n "$app_pid" ] && kill "$app_pid" >/dev/null 2>&1 + wineserver -k >/dev/null 2>&1 +} +trap cleanup EXIT + +diagnostics() { + echo "---- diagnostics ----" + echo "-- X windows:" + xdotool search --name "." 2>/dev/null | while read -r w; do + echo " $w: $(xdotool getwindowname "$w" 2>/dev/null)" + done + echo "-- snapshot ($snap):" + if [ -f "$snap" ]; then tr '|' '\n' < "$snap" | sed 's/^/ /'; else echo " (missing)"; fi + echo "-- app log tail ($app_log):" + tail -40 "$app_log" 2>/dev/null | sed 's/^/ /' + echo "---------------------" +} + +fail() { + echo "FAIL: $1" + diagnostics + exit 1 +} + +# poll : wait until $snap contains . +poll() { + local deadline=$((SECONDS + $1)) + while [ "$SECONDS" -lt "$deadline" ]; do + [ -f "$snap" ] && grep -q "$2" "$snap" && return 0 + sleep 0.5 + done + return 1 +} + +# ---- build ---------------------------------------------------------------- +(cd "$repo_root" && zig build) || fail "root zig build (CLI) failed" +(cd "$app_dir" && zig build -Dtarget=x86_64-windows-gnu -Dplatform=windows -Dweb-engine=system -Dautomation=true) \ + || fail "ui-inbox Windows cross-compile failed" + +# ---- wineprefix ----------------------------------------------------------- +# First run initializes the prefix (measured ~10-30s on CI-class machines); +# subsequent runs are instant, so no cache step is needed. +start=$SECONDS +wineboot --init >/dev/null 2>&1 +wineserver --wait >/dev/null 2>&1 +echo "== wineprefix ready in $((SECONDS - start))s ($WINEPREFIX)" + +# ---- launch --------------------------------------------------------------- +cd "$app_dir" || fail "missing $app_dir" +rm -rf .zig-cache/native-sdk-automation +mkdir -p .zig-cache/native-sdk-automation +wine zig-out/bin/ui-inbox.exe > "$app_log" 2>&1 & +app_pid=$! + +# ---- 1: automation snapshot becomes ready --------------------------------- +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 -------------------- +poll 60 'gpu_nonblank=true' || fail "gpu_nonblank never became true" +grep -q 'gpu_backend=software' "$snap" || fail "gpu_backend is not software" +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)" \ + "$(grep -o 'gpu_present_mode=[a-z]*' "$snap" | head -1)" + +# ---- 4: automation widget-click mutates the model -------------------------- +echo "== open before click: $(grep -oE '[0-9]+ open' "$snap" | head -1)" +add_id=$(grep -o 'widget @w1/inbox-canvas#[0-9]* role=button name="Add task"' "$snap" \ + | grep -o '#[0-9]*' | tr -d '#') +[ -n "$add_id" ] || fail "Add task button not found in snapshot" +"$cli" automate widget-click inbox-canvas "$add_id" || fail "CLI widget-click failed" +poll 30 '4 open' || fail "widget-click did not reach '4 open'" +echo "== open after click: $(grep -oE '[0-9]+ open' "$snap" | head -1)" + +# ---- 5: real X11 input through the Win32 path ------------------------------ +# Root-coordinate math. Hidden-titlebar windows keep the full overlapped +# frame and reclaim the caption band through WM_NCCALCSIZE, so the Win32 +# client area starts at the very top of the window. Under a WM-less Wine +# the X11 driver still places (and reports) the client X window at the +# DEFAULT frame offset - one caption band lower - so the X origin sits a +# band BELOW where Win32 client coordinates actually map, and the reported +# X height is short by exactly that band (measured: X window 718x489 at +# y=30 for a 718x519 client whose clicks land at y=0). The snapshot knows +# the true client height, so the height shortfall IS the y correction; a +# standard-frame window reports matching heights and corrects by zero. +win="" +for w in $(xdotool search --name "." 2>/dev/null); do + case "$(xdotool getwindowname "$w" 2>/dev/null)" in + *[Ii]nbox*) win="$w" ;; + esac +done +[ -n "$win" ] || fail "app X window not found" +eval "$(xdotool getwindowgeometry --shell "$win")" +client_h=$(grep -o 'window @w1 "[^"]*" bounds=([^)]*)' "$snap" | head -1 \ + | sed -n 's/.*x\([0-9]*\)[^x]*$/\1/p') +[ -n "$client_h" ] || client_h=$HEIGHT +y_off=$((client_h - HEIGHT)) +[ "$y_off" -ge 0 ] 2>/dev/null || y_off=0 +echo "== x window $win: pos=($X,$Y) size=${WIDTH}x${HEIGHT} client_h=$client_h y_off=$y_off" +xdotool windowactivate "$win" >/dev/null 2>&1 || xdotool windowfocus "$win" >/dev/null 2>&1 + +draft_line=$(grep -o 'widget @w1/inbox-canvas#[0-9]* role=textbox[^|]*' "$snap" | head -1) +[ -n "$draft_line" ] || fail "draft textbox not found in snapshot" +bounds=$(echo "$draft_line" | grep -o 'bounds=([^)]*)') +bx=$(echo "$bounds" | sed -n 's/bounds=(\([0-9.]*\),.*/\1/p') +by=$(echo "$bounds" | sed -n 's/bounds=([0-9.]*,\([0-9.]*\) .*/\1/p') +bw=$(echo "$bounds" | sed -n 's/.* \([0-9.]*\)x[0-9.]*).*/\1/p') +bh=$(echo "$bounds" | sed -n 's/.* [0-9.]*x\([0-9.]*\)).*/\1/p') +[ -n "$bx" ] && [ -n "$by" ] && [ -n "$bw" ] && [ -n "$bh" ] || fail "could not parse draft bounds: $draft_line" +cx=$(awk "BEGIN{printf \"%d\", $X + $bx + $bw / 2}") +cy=$(awk "BEGIN{printf \"%d\", $Y - $y_off + $by + $bh / 2}") +echo "== clicking draft field $bounds at ($cx,$cy)" +xdotool mousemove "$cx" "$cy" click 1 +# The click must move widget focus into the textbox before any keys are +# sent: spaces in the typed string would otherwise activate whatever +# widget held focus (a button press adds a task and the real failure - +# input landing in the wrong widget - would read as missing text). +poll 10 'role=textbox[^|]*focused=true' || fail "draft textbox did not take focus from the click" +sleep 1 +xdotool type --delay 120 "hi from wine" +poll 30 'hi from wine' || fail "typed text never appeared in the snapshot" +echo "== draft after typing: $(grep -o 'widget @w1/inbox-canvas#[0-9]* role=textbox[^|]*' "$snap" | head -1 | cut -c1-160)" +echo "== input latency: $(grep -o 'gpu_input_latency_ns=[0-9]*' "$snap" | head -1)" + +echo "PASS: windows canvas smoke" +exit 0 diff --git a/.github/scripts/windows-effects-smoke.sh b/.github/scripts/windows-effects-smoke.sh new file mode 100755 index 00000000..94b62c84 --- /dev/null +++ b/.github/scripts/windows-effects-smoke.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +# Windows effects smoke under Wine. +# +# Exercises the effect system's live Windows path (src/runtime/effects.zig +# worker threads -> PostMessageW wake in src/platform/windows/ +# webview2_host.cpp -> loop-thread drain) without Windows hardware: +# cross-compiles examples/effects-probe for x86_64-windows-gnu, runs the +# .exe under Xvfb + Wine, and asserts against the automation snapshot and +# the app's trace log: +# +# 1. snapshot ready=true (app booted, automation server live) +# 2. gpu_backend=software + 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) +# 4. app log shows event=effects_wake (the worker's PostMessageW wake was +# marshalled through the message +# loop -- the wake path itself, not +# just the frame-tick drain) +# 5. widget-click "Cancel" (fx.cancel terminates the child; +# status shows "cancelled") +# 6. the line count freezes (no lines arrive after cancel, +# sampled across ~5 more would-be +# line intervals) +# +# Known caveat: the timer present mode also drains effect completions on +# every frame tick (ui_app.zig handleFrame), so line delivery alone cannot +# isolate the wake; that is why step 4 checks the trace log for the wake +# events directly instead of inferring the wake from model updates. +# +# Deliberately NOT `set -e`: grep exits 1 on zero matches inside the poll +# loops, and we want explicit, diagnosable failures instead of silent early +# exits. Every assertion goes through fail(), which dumps diagnostics. +set -u + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +app_dir="$repo_root/examples/effects-probe" +snap="$app_dir/.zig-cache/native-sdk-automation/snapshot.txt" +cli="$repo_root/zig-out/bin/native" +app_log="${TMPDIR:-/tmp}/windows-effects-smoke-app.log" + +# Wine needs an X display; when none is present (CI), re-exec the whole +# script under a private Xvfb server. The explicit screen size beats +# xvfb-run's 640x480x8 default: the app window is 560x480 and Wine wants a +# 24-bit visual. +if [ -z "${DISPLAY:-}" ]; then + exec xvfb-run -a --server-args="-screen 0 1280x800x24" "$0" "$@" +fi + +export WINEPREFIX="${WINEPREFIX:-$repo_root/.zig-cache/wineprefix}" +export WINEDEBUG="${WINEDEBUG:--all}" + +app_pid="" +cleanup() { + [ -n "$app_pid" ] && kill "$app_pid" >/dev/null 2>&1 + wineserver -k >/dev/null 2>&1 +} +trap cleanup EXIT + +diagnostics() { + echo "---- diagnostics ----" + echo "-- snapshot ($snap):" + if [ -f "$snap" ]; then tr '|' '\n' < "$snap" | sed 's/^/ /'; else echo " (missing)"; fi + echo "-- app log tail ($app_log):" + tail -40 "$app_log" 2>/dev/null | sed 's/^/ /' + echo "---------------------" +} + +fail() { + echo "FAIL: $1" + diagnostics + exit 1 +} + +# poll : wait until $snap contains . +poll() { + local deadline=$((SECONDS + $1)) + while [ "$SECONDS" -lt "$deadline" ]; do + [ -f "$snap" ] && grep -q "$2" "$snap" && return 0 + sleep 0.5 + done + return 1 +} + +# The status bar renders "{N} lines total · {M} dropped"; extract N. +total_lines() { + grep -o '[0-9]* lines total' "$snap" 2>/dev/null | head -1 | grep -o '^[0-9]*' +} + +# widget_id : find a widget id in the snapshot by accessible name. +widget_id() { + grep -o "widget @w1/probe-canvas#[0-9]* role=button name=\"$1\"" "$snap" \ + | grep -o '#[0-9]*' | tr -d '#' +} + +# ---- build ---------------------------------------------------------------- +(cd "$repo_root" && zig build) || fail "root zig build (CLI) failed" +# effects-probe is a zero-config app (app.zon + src, no build.zig): the CLI +# synthesizes its build graph. -Doptimize=Debug keeps the smoke binary at +# the debug shape (`native build` alone would inject ReleaseFast). +"$cli" build "$app_dir" -Dtarget=x86_64-windows-gnu -Dplatform=windows -Dweb-engine=system -Dautomation=true -Doptimize=Debug \ + || fail "effects-probe Windows cross-compile failed" + +# ---- wineprefix ----------------------------------------------------------- +start=$SECONDS +wineboot --init >/dev/null 2>&1 +wineserver --wait >/dev/null 2>&1 +echo "== wineprefix ready in $((SECONDS - start))s ($WINEPREFIX)" + +# ---- launch --------------------------------------------------------------- +cd "$app_dir" || fail "missing $app_dir" +rm -rf .zig-cache/native-sdk-automation +mkdir -p .zig-cache/native-sdk-automation +wine zig-out/bin/effects-probe.exe > "$app_log" 2>&1 & +app_pid=$! + +# ---- 1: automation snapshot becomes ready --------------------------------- +poll 180 'ready=true' || fail "snapshot never became ready" +echo "== ready: $(head -1 "$snap" | cut -d'|' -f1)" + +# ---- 2: software 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" +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" + +# ---- 3: Start spawns the stream and lines arrive --------------------------- +start_id=$(widget_id "Start stream") +[ -n "$start_id" ] || fail "Start stream button not found in snapshot" +"$cli" automate widget-click probe-canvas "$start_id" || fail "CLI widget-click Start failed" +poll 30 'streaming:' || fail "status never showed streaming (spawn failed under Wine?)" +# The Windows stream paces ~1 line/s (cmd for /L + ping); wait for at +# least 2 visible lines so cancel provably interrupts an active stream. +poll 60 'stream line 2' || fail "stream lines never reached the model" +echo "== streaming: $(grep -o 'streaming: [0-9]* lines' "$snap" | head -1), status-bar: $(total_lines) lines total" + +# ---- 4: the PostMessage wake path fired ------------------------------------ +# handleFrame also drains completions on every frame tick, so lines in the +# model alone cannot isolate the wake. The runner's default -Dtrace=events +# sink prints every runtime event; effects_wake records prove the worker's +# PostMessageW -> kWakeMessage -> kWake -> .effects_wake marshalling ran. +grep -q 'event="effects_wake"' "$app_log" || fail "no effects_wake events in the app log (PostMessage wake never fired)" +echo "== effects_wake events so far: $(grep -c 'event="effects_wake"' "$app_log")" + +# ---- 5: Cancel terminates the child ---------------------------------------- +cancel_id=$(widget_id "Cancel") +[ -n "$cancel_id" ] || fail "Cancel button not found in snapshot" +"$cli" automate widget-click probe-canvas "$cancel_id" || fail "CLI widget-click Cancel failed" +poll 30 'cancelled: code' || fail "status never showed cancelled" +frozen=$(total_lines) +[ -n "$frozen" ] || fail "could not read line count after cancel" +echo "== cancelled at $frozen lines: $(grep -o 'cancelled: code [0-9-]* after [0-9]* lines' "$snap" | head -1)" + +# ---- 6: the line count is frozen ------------------------------------------- +# ~5 more lines would have arrived at the ~1s cadence if the child were +# still alive or queued lines were still draining. +sleep 6 +after=$(total_lines) +[ "$after" = "$frozen" ] || fail "line count moved after cancel ($frozen -> $after)" +grep -q 'streaming:' "$snap" && fail "status went back to streaming after cancel" +echo "== count frozen at $after lines across 6s" + +echo "PASS: windows effects smoke" +exit 0 diff --git a/.github/workflows/cef-runtime.yml b/.github/workflows/cef-runtime.yml index 8a049e9c..e1a4e446 100644 --- a/.github/workflows/cef-runtime.yml +++ b/.github/workflows/cef-runtime.yml @@ -96,13 +96,13 @@ jobs: esac find "$CEF_ROOT/build/libcef_dll_wrapper" -name "$wrapper" -print -quit | xargs -I{} cp "{}" "$CEF_ROOT/libcef_dll_wrapper/$wrapper" - - name: Prepare zero-native runtime archive + - name: Prepare native-sdk runtime archive if: ${{ inputs.source == 'official' }} shell: bash run: | zig build - cli="zig-out/bin/zero-native" - if [[ "${{ runner.os }}" == "Windows" ]]; then cli="zig-out/bin/zero-native.exe"; fi + cli="zig-out/bin/native" + if [[ "${{ runner.os }}" == "Windows" ]]; then cli="zig-out/bin/native.exe"; fi "$cli" cef prepare-release --dir "$CEF_ROOT" --output zig-out/cef --version "${{ inputs.cef_version }}" - name: Build CEF from source and prepare runtime @@ -116,7 +116,7 @@ jobs: --platform "${{ matrix.platform }}" --version "${{ inputs.cef_version }}" --output zig-out/cef - --zero-native-bin zig-out/bin/zero-native + --native-sdk-bin zig-out/bin/native ) if [ -n "${{ inputs.cef_branch }}" ]; then args+=(--cef-branch "${{ inputs.cef_branch }}") @@ -129,5 +129,5 @@ jobs: tag_name: cef-${{ inputs.cef_version }} name: CEF ${{ inputs.cef_version }} files: | - zig-out/cef/zero-native-cef-${{ inputs.cef_version }}-${{ matrix.platform }}.tar.gz - zig-out/cef/zero-native-cef-${{ inputs.cef_version }}-${{ matrix.platform }}.tar.gz.sha256 + zig-out/cef/native-sdk-cef-${{ inputs.cef_version }}-${{ matrix.platform }}.tar.gz + zig-out/cef/native-sdk-cef-${{ inputs.cef_version }}-${{ matrix.platform }}.tar.gz.sha256 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc0ff3ea..55d9fdfe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,44 @@ jobs: version: 0.16.0 - run: zig build test-webview-system-link - run: zig build test-webview-smoke + # Signed-package seal pin: an ad-hoc signed package must pass + # codesign --verify --strict (macOS runners are the only tier with + # codesign; the step skips loudly anywhere else). + - run: zig build test-package-signing + # Shared macos-14 runners are far noisier than a dev box (the second + # CI run measured a 576 ms automation-ready against the 500 ms local + # ceiling), so widen the smoke budgets here instead of weakening the + # local defaults. NATIVE_SDK_SMOKE_BUDGET_MS raises the first-frame latency + # budget and the automation-ready ceiling together; every correctness + # assertion in the smokes stays strict. + - run: zig build test-gpu-dashboard-smoke + env: + NATIVE_SDK_SMOKE_BUDGET_MS: "1500" + - run: zig build test-gpu-components-smoke + env: + NATIVE_SDK_SMOKE_BUDGET_MS: "1500" + + macos-gpu-perf: + name: macOS GPU Perf + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + - uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + # Percentile perf check: 5 cold launches asserting p90 + # first-frame latency under NATIVE_SDK_PERF_BUDGET_MS, then 5 steady-state + # widget clicks asserting p90 input latency under NATIVE_SDK_PERF_INPUT_BUDGET_MS. + # Its own job so a shared-runner slowdown is visible in isolation and + # never blocks the correctness smokes. + # Shared macos-14 runners are far noisier than a dev box (first CI run + # measured a 581 ms cold-start outlier against the 300 ms default), so + # widen the budgets here instead of weakening the local defaults: this + # job exists to catch step-function regressions, not runner noise. + - run: zig build test-gpu-dashboard-perf + env: + NATIVE_SDK_PERF_BUDGET_MS: "1500" + NATIVE_SDK_PERF_INPUT_BUDGET_MS: "500" linux-webkitgtk: name: Linux WebKitGTK @@ -72,8 +110,79 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 24 - - run: npm --prefix packages/zero-native run version:check - - run: npm --prefix packages/zero-native run scripts:check + - run: npm --prefix packages/native-sdk run version:check + - run: npm --prefix packages/native-sdk run scripts:check + + native-examples: + name: Native Examples + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + - name: Install GTK dependencies + run: sudo apt-get update && sudo apt-get install -y libgtk-4-dev libwebkitgtk-6.0-dev + - run: zig build test-examples-native + + linux-canvas-smoke: + name: Linux Canvas Smoke + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + - name: Install GTK and Xvfb + run: sudo apt-get update && sudo apt-get install -y libgtk-4-dev libwebkitgtk-6.0-dev xvfb + # Drives the gpu_surface software path against system WebKitGTK under + # Xvfb: snapshot ready, gpu_backend=software, gpu_nonblank=true, + # automation widget-click, and a rendered screenshot. Sandbox/a11y + # env, the widened cold-start readiness budget (shared runners stall + # ~27 s before the first runtime event), and failure forensics (dump + # snapshot + app log) all live in the script. + - name: Build and drive ui-inbox headless + run: .github/scripts/linux-canvas-smoke.sh + + windows-canvas-smoke: + name: Windows Canvas Smoke (Wine) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + - name: Install Wine, Xvfb, and xdotool + run: sudo apt-get update && sudo apt-get install -y wine xvfb xdotool + # 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, + # 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). + - name: Build and drive ui-inbox.exe under Wine + run: .github/scripts/windows-canvas-smoke.sh + + windows-effects-smoke: + name: Windows Effects Smoke (Wine) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + - name: Install Wine and Xvfb + run: sudo apt-get update && sudo apt-get install -y wine xvfb + # Cross-compiles examples/effects-probe for x86_64-windows-gnu and + # proves the effect system's live Windows path under Wine: fx.spawn + # launches cmd.exe, streamed lines land in the model, the worker's + # PostMessageW wake shows up as effects_wake events in the trace + # log (frame ticks also drain, so the log is the wake's evidence), + # and fx.cancel terminates the child with the line count frozen. + - name: Build and drive effects-probe.exe under Wine + run: .github/scripts/windows-effects-smoke.sh frontend-examples: name: Frontend Examples @@ -104,16 +213,49 @@ jobs: with: version: 0.16.0 - run: zig build + - name: Scaffold and test the zero-config native app + run: | + set -euo pipefail + app=".zig-cache/scaffold-native-slim" + rm -rf "$app" + ./zig-out/bin/native init "$app" + # Slim scaffold: no build files — the CLI's generated graph drives it. + test ! -f "$app/build.zig" + test ! -f "$app/build.zig.zon" + ./zig-out/bin/native test "$app" -Dplatform=null + ./zig-out/bin/native check "$app" + ./zig-out/bin/native eject "$app" + (cd "$app" && zig build test -Dplatform=null) - name: Scaffold and test frontend templates run: | set -euo pipefail - for frontend in next vite react svelte vue; do + for frontend in native next vite react svelte vue; do app=".zig-cache/scaffold-${frontend}" rm -rf "$app" - ./zig-out/bin/zero-native init "$app" --frontend "$frontend" - (cd "$app" && zig build test -Dplatform=null && ../../zig-out/bin/zero-native validate app.zon) + ./zig-out/bin/native init "$app" --frontend "$frontend" --full + (cd "$app" && zig build test -Dplatform=null && ../../zig-out/bin/native validate app.zon) + # Every scaffold ships a CI workflow; parse it as real YAML. + test -s "$app/.github/workflows/ci.yml" + python3 -c 'import sys, yaml; yaml.safe_load(open(sys.argv[1]))' "$app/.github/workflows/ci.yml" done + evals-typecheck: + name: Evals Typecheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - uses: pnpm/action-setup@v4 + with: + version: 10.23.0 + package_json_file: evals/package.json + - run: pnpm install --frozen-lockfile + working-directory: evals + - run: pnpm typecheck + working-directory: evals + docs: name: Docs runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 59410dca..e0ac1caf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,10 +32,10 @@ jobs: - name: Compare package.json version to npm and check GitHub release id: check run: | - LOCAL_VERSION=$(node -p "require('./packages/zero-native/package.json').version") + LOCAL_VERSION=$(node -p "require('./packages/native-sdk/package.json').version") echo "Local version: $LOCAL_VERSION" - NPM_VERSION=$(npm view zero-native version 2>/dev/null || echo "0.0.0") + NPM_VERSION=$(npm view @native-sdk/cli version 2>/dev/null || echo "0.0.0") echo "npm version: $NPM_VERSION" if [ "$LOCAL_VERSION" != "$NPM_VERSION" ]; then @@ -51,14 +51,14 @@ jobs: missing=0 for asset in \ CHECKSUMS.txt \ - zero-native-darwin-arm64 \ - zero-native-darwin-x64 \ - zero-native-linux-arm64 \ - zero-native-linux-x64 \ - zero-native-linux-musl-arm64 \ - zero-native-linux-musl-x64 \ - zero-native-win32-arm64.exe \ - zero-native-win32-x64.exe + native-sdk-darwin-arm64 \ + native-sdk-darwin-x64 \ + native-sdk-linux-arm64 \ + native-sdk-linux-x64 \ + native-sdk-linux-musl-arm64 \ + native-sdk-linux-musl-x64 \ + native-sdk-win32-arm64.exe \ + native-sdk-win32-x64.exe do if ! printf '%s\n' "$EXISTING_ASSETS" | grep -Fx "$asset" >/dev/null; then echo "Missing release asset: $asset" @@ -108,35 +108,11 @@ jobs: fi echo "Extracted release notes for $VERSION ($LINES lines)" - - name: Build native release asset + - name: Build native release assets (all platforms) run: | - mkdir -p /tmp/zero-native-release - - build_asset() { - target="$1" - name="$2" - rm -rf zig-out - zig build -Dtarget="$target" -Doptimize=ReleaseSmall - - src="zig-out/bin/zero-native" - case "$name" in - *.exe) src="${src}.exe" ;; - esac - - cp "$src" "/tmp/zero-native-release/$name" - chmod 755 "/tmp/zero-native-release/$name" - } - - build_asset aarch64-macos zero-native-darwin-arm64 - build_asset x86_64-macos zero-native-darwin-x64 - build_asset aarch64-linux-gnu zero-native-linux-arm64 - build_asset x86_64-linux-gnu zero-native-linux-x64 - build_asset aarch64-linux-musl zero-native-linux-musl-arm64 - build_asset x86_64-linux-musl zero-native-linux-musl-x64 - build_asset aarch64-windows zero-native-win32-arm64.exe - build_asset x86_64-windows zero-native-win32-x64.exe - - (cd /tmp/zero-native-release && shasum -a 256 zero-native-* > CHECKSUMS.txt) + # Cross-compiles the CLI for all eight platforms and writes the + # flat release assets + CHECKSUMS.txt into zig-out/release/. + bash packages/native-sdk/scripts/build-binaries.sh - name: Create GitHub Release run: | @@ -153,8 +129,8 @@ jobs: fi gh release upload "$TAG" \ - /tmp/zero-native-release/zero-native-* \ - /tmp/zero-native-release/CHECKSUMS.txt \ + zig-out/release/native-sdk-* \ + zig-out/release/CHECKSUMS.txt \ --clobber env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -167,7 +143,7 @@ jobs: && needs.check-release.outputs.should_release == 'true' && (needs.github-release.result == 'success' || needs.github-release.result == 'skipped') runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 15 environment: Release permissions: contents: read @@ -182,17 +158,73 @@ jobs: node-version: "24" registry-url: "https://registry.npmjs.org" + # Publishing uses npm trusted publishing (OIDC): the job's id-token + # permission lets npm mint short-lived credentials, so no npm token + # secret exists anywhere in this repo. All nine packages — + # @native-sdk/cli plus the eight @native-sdk/cli-* platform packages + # under packages/native-sdk/npm/ — must each be configured on + # npmjs.com with a GitHub Actions trusted publisher pointing at + # repository vercel-labs/zero-native, workflow release.yml, + # environment Release. If a package is missing that configuration, + # npm publish fails loudly with an OIDC authentication error before + # anything is uploaded for that package. Trusted publishing requires + # npm >= 11.5.1, satisfied by the npm bundled with Node 24. + - name: Check version sync - run: npm --prefix packages/zero-native run version:check + run: npm --prefix packages/native-sdk run version:check - name: Check package scripts - run: npm --prefix packages/zero-native run scripts:check + run: npm --prefix packages/native-sdk run scripts:check + + - name: Download release binaries + run: | + VERSION="${{ needs.check-release.outputs.version }}" + mkdir -p /tmp/native-sdk-release + gh release download "v$VERSION" \ + --pattern 'native-sdk-*' \ + --pattern 'CHECKSUMS.txt' \ + --dir /tmp/native-sdk-release + (cd /tmp/native-sdk-release && sha256sum -c CHECKSUMS.txt) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Stage binaries into the platform packages + run: | + # Release-asset name -> npm platform package (same table as + # packages/native-sdk/scripts/build-binaries.sh). + stage() { + asset="$1"; key="$2"; ext="$3" + mkdir -p "packages/native-sdk/npm/$key/bin" + cp "/tmp/native-sdk-release/$asset" "packages/native-sdk/npm/$key/bin/native$ext" + chmod 755 "packages/native-sdk/npm/$key/bin/native$ext" + } + stage native-sdk-darwin-arm64 darwin-arm64 "" + stage native-sdk-darwin-x64 darwin-x64 "" + stage native-sdk-linux-arm64 linux-arm64-gnu "" + stage native-sdk-linux-x64 linux-x64-gnu "" + stage native-sdk-linux-musl-arm64 linux-arm64-musl "" + stage native-sdk-linux-musl-x64 linux-x64-musl "" + stage native-sdk-win32-arm64.exe win32-arm64 ".exe" + stage native-sdk-win32-x64.exe win32-x64 ".exe" - name: Publish to npm run: | - if [ "${{ github.event.repository.visibility }}" = "public" ]; then - npm publish --provenance --access public - else - npm publish --access public - fi - working-directory: packages/zero-native + VERSION="${{ needs.check-release.outputs.version }}" + + # Platform packages first, so the main package's + # optionalDependencies pins are resolvable the moment it lands. + # Re-runs skip anything already on the registry at this version. + publish_dir() { + dir="$1" + name=$(node -p "require('./$dir/package.json').name") + if npm view "$name@$VERSION" version >/dev/null 2>&1; then + echo "$name@$VERSION already published, skipping" + return 0 + fi + (cd "$dir" && npm publish --provenance --access public) + } + + for dir in packages/native-sdk/npm/*/; do + publish_dir "${dir%/}" + done + publish_dir packages/native-sdk diff --git a/.gitignore b/.gitignore index 1e3d8758..92478f53 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,25 @@ .DS_Store .zig-cache/ zig-out/ +# CLI-generated build graph for zero-config apps (the examples here) — the +# same entry `native init` writes into a new app's .gitignore. +.native/ -# Native binaries in zero-native npm package (built by CI or locally) -packages/zero-native/bin/zero-native-* -packages/zero-native/src/ -packages/zero-native/skills/ -packages/zero-native/skill-data/ +# Native binaries in the @native-sdk/cli npm packages (built by CI or locally) +packages/native-sdk/bin/native-sdk-* +packages/native-sdk/npm/*/bin/ +# SDK payload mirrored into the npm package at pack time (copy-framework.js) +packages/native-sdk/src/ +packages/native-sdk/skills/ +packages/native-sdk/skill-data/ +packages/native-sdk/build/ +packages/native-sdk/build.zig +packages/native-sdk/build.zig.zon +packages/native-sdk/app.zon +packages/native-sdk/LICENSE +# npm pack output +packages/native-sdk/*.tgz +packages/native-sdk/npm/*/*.tgz # Downloaded/prepared CEF runtimes are large local artifacts. third_party/cef/macos/ @@ -18,3 +31,4 @@ third_party/cef/linux/ # TypeScript build info (generated) docs/tsconfig.tsbuildinfo +.claude/ diff --git a/AGENTS.md b/AGENTS.md index e1710fae..6e6ecf39 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,16 +1,31 @@ -# Agent Rules +# Agent Guide -## Releasing +Guidance for agents (and humans) working on this repository. -Releases are manual, single-PR affairs. The maintainer controls the changelog voice and format. +## Build, test, and gate -To prepare a release: +```bash +zig build test # root engine + runtime suites +zig build validate # sample app.zon manifest check +zig build test-example- # one example's suite (e.g. test-example-notes) +scripts/gate.sh fast [ref] # affected-only local gate for your diff (default base: main) +scripts/gate.sh full # everything CI-shaped that runs locally +``` -1. Create a branch (e.g. `prepare-v1.2.0`) -2. Bump the version in `packages/zero-native/package.json` -3. Run `npm --prefix packages/zero-native run version:sync` to update all version references -4. Write the changelog entry in `CHANGELOG.md`, wrapped in `` and `` markers -5. Remove the `` and `` markers from the previous release entry; only the latest release should have markers -6. Open a PR and merge to `main` +Run `scripts/gate.sh fast` before finishing any change; it maps your diff to the suites that cover it. The docs site checks with `pnpm --dir docs check` (the gate runs it only when `docs/` changed). -CI compares the version in `packages/zero-native/package.json` to what's on npm. If it differs, it publishes the CLI package and creates the GitHub release automatically. If npm already has the version but the GitHub release is missing, CI creates the GitHub release from the marked changelog entry. +Pinned goldens (pixel signatures, schema fingerprints, command counts) are updated deliberately: review the rendered output or the counted commands first, and keep the pin's comment a self-contained description of what the value represents. + +## Changelog + +Do not edit `CHANGELOG.md` directly. Each user-visible change ships a fragment in `changelog.d/` — see `changelog.d/README.md` for the format and voice. Internal-only polish needs no fragment. + +## Where things live + +- `src/` — the engine and runtime; `src/primitives/canvas/` holds the widget, markup, and vector core. +- `examples/` — the showcase apps, most zero-config (`app.zon` + `src/`). +- `docs/` — the documentation site; `docs/AGENTS.md` has its MDX conventions. +- `skills/` and `skill-data/` — the agent skills the CLI ships (`native skills list`). +- `tools/` and `scripts/` — dev tooling and the local gate. + +Releases are maintainer-run; see [RELEASING.md](./RELEASING.md). diff --git a/CHANGELOG.md b/CHANGELOG.md index aefc81e1..d60a98bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,110 @@ # Changelog -All notable changes to zero-native will be documented in this file. +All notable changes to the Native SDK (formerly zero-native) will be documented in this file. -## 0.3.0 +## 0.4.0 ### New Features +- **zero-native is now the Native SDK**: The toolkit, CLI, and packages are renamed end to end — the CLI binary is `native`, the Zig module and build helper are `native_sdk` (`native_sdk.addApp`, `native_sdk.addMobileLib`), the embed C ABI prefix is `native_sdk_*`, and the npm CLI package is `@native-sdk/cli`. +- **Native-rendered apps by default**: `native init` scaffolds a native-rendered app — a declarative `.native` markup view plus Zig logic on the `UiApp` runtime (a `Model`, a `Msg` union, `update`, and a view) — with web frontends still available via `--frontend next|vite|react|svelte|vue`. + - Native markup: HTML-inspired views with flex layout, `{bindings}` to model fields and functions, typed `on-*` message dispatch, `for`/`if`/`else` structure tags (multi-child `for` bodies, `` empty states), and keyed identity; a deliberately closed grammar keeps logic in Zig. + - Comptime compilation: views compile at build time into direct field access — release binaries carry no parser, and markup or binding mistakes are compile errors with line and column. + - Hot reload: dev builds watch every `.native` file — imported components and fragments embedded in Zig views included — and update the running window in place, preserving model state, selection, and widget identity. + - Expressions in bindings: arithmetic, comparisons, boolean logic, string concatenation, and a closed 17-function formatting library (`fixed`, `thousands`, `date`/`time`, `pad`, `plural`, ...), evaluated bit-identically by both markup engines; string-producing model functions bind directly through the build arena. + - Cross-file components: `` splices template files (transitively, with cycle and duplicate diagnostics), template args take literal defaults, `` marks where use-site children land, and `native eject component` transfers a library composite's canonical source into your app exactly once. + - `canvas.Ui`, the programmatic builder under the markup: structural widget identity, typed message handlers, flex-first layout, and per-element `opacity`/`transform` render channels for animated composition. +- **The model–view contract, checked in both directions**: `native check` verifies every binding path, iterable, key, message tag, payload type, and expression in every `.native` file against the app's reflected `Model`/`Msg` surface in milliseconds — with did-you-mean suggestions and a dead-state lint for model fields and messages no view uses. +- **Markup tooling**: `native markup check` (instant validation with positions), a language server (diagnostics, completion, hover), a TextMate grammar with editor setup, `native markup dump` over the canonical serialized document format, and the `native-ui` agent skill — the complete authoring reference, served through the skills CLI. +- **Two-way tooling**: `native automate provenance` reports where a live widget was authored (file, byte span, template instantiation chain), and `native automate edit` writes minimal-diff attribute and text edits back into the markup source — validated before anything touches the file, with hot reload closing the loop. +- **Full component catalog**: every built-in component is expressible in markup — tabs, tables, dialogs, drawers, sheets, selects, comboboxes, accordions, menus, badges, avatars, tooltips, inputs, and more — implemented in both engines with parity tests, alongside new composites in markup and Zig: + - Charts (`` / `ui.chart`): line, area, bar, and band series drawn through the vector path pipeline with design-token colors, deterministic downsampling past 256 points, axis labels on a nice-step lattice, and pointer hover details. + - Markdown (`` / `native_sdk.markdown`): a GitHub-flavored subset — headings, inline styles, links, lists, task lists, fenced code, blockquotes, pipe tables, autolinks, and model-driven collapsibles — that degrades malformed input to text and never fails a build. + - Disclosure trees with the full ARIA tree keymap, steppers and timeline items, input groups with focus-within rings, chat bubbles with reaction pills and thread-width caps, and a `ui.nav` push/pop page container with stable per-page state. + - Resizable split panes with model-owned fractions, keyboard and assistive resize, and optional eased animation on model-driven moves. + - Windowed virtual lists: viewport-sized widget budgets at 100,000 items, variable row extents that converge to measured truth without visible jumps, tail anchoring for chat transcripts, and `on_reach_end`/`on_reach_start` for infinite fetch and history loading. + - Anchored floating surfaces (dropdowns, selects, popovers) that float above the tree with edge auto-flip; dismissal (Escape, click-outside, assistive dismiss) is a Msg the model owns, and focused selects get the full open/navigate/commit keymap. + - Vector icons: an SVG stroke-icon subset parser, 50 curated built-in icons, leading or trailing icon slots on buttons, toggle chips, list and menu rows, badges, and timeline items, app-registered icons comptime-parsed from your own SVGs, model-bound icon names, and a loud missing-icon fallback. +- **Text engine**: + - Inline styled spans — weight (resolved to real faces), italic, monospace, color tokens, underline, strikethrough, size scale, per-span backgrounds, and hit-testable links — wrap as one paragraph in Zig and markup alike. + - Honest single-line text: unwrapped text elides with a trailing ellipsis by default, an `overflow` policy knob keeps the deliberate hard cut available, and word wrap is an explicit opt-in — paint always agrees with measurement. + - `heading`/`display` typography rungs on the token ladder, first-class text alignment, and fixed grid column counts. +- **Selection and clipboard**: cmd/ctrl+C/X/V in editable fields through the platform clipboard, click-drag selection with copy on static text (surviving rebuilds, exposed to semantics and automation), and clipboard effects for app code. +- **Interaction model**: + - Presses fall through to the nearest pressable ancestor, so any element with a handler is a real hit target — nested pressables resolve to the deepest one, and text selection still works inside pressable rows. + - Press-and-hold, double-click, Enter as a list row's primary action, and an app-level key fallback (`Options.on_key`) with pinned precedence — quiet list rows stay transparent to app-owned selection models. + - Source-driven `autofocus`, observable typed scroll events (`on_scroll`), a built-in search-field clear affordance, and a quiet-hover style knob for content tiles. +- **Effect system**: the update loop's command half — `update` gains an effects channel of bounded, key-addressed effects that deliver exactly one terminal Msg each and are fully testable against a deterministic fake executor: + - `fx.spawn` runs subprocesses with streamed lines or whole-output collect mode (stderr tail included), raisable per-effect line bounds, and cancellation; `fx.fetch` runs HTTP(S) requests with an explicit failure taxonomy, timeouts, and a streaming response mode for line-oriented endpoints. + - `fx.readFile`/`fx.writeFile` persistence, `fx.startTimer`/`fx.cancelTimer`, `fx.writeClipboard`/`fx.readClipboard`, `fx.registerImageBytes` for runtime images, `fx.closeWindow`/`fx.minimizeWindow`, and the `init_fx` boot hook so loading states are in the very first paint. + - A facade time API (`nowMs`, `monotonicMs`) plus `Clock`/`TestClock` seams for deterministic time-dependent logic. +- **Audio, end to end on five platforms**: `fx.playAudio` with full transport (pause, resume, stop, seek, volume), real decoded durations, position ticks, and honest completion and failure reports — AVFoundation on macOS, Media Foundation on Windows, GStreamer on Linux, and the experimental mobile hosts on iOS (AVFoundation) and Android (MediaPlayer). + - Streaming with a verified track cache: URL sources resolve local file, then size-verified cache, then progressive stream (filling the cache in parallel for the next play), with honest `buffering` states and explicit failures — never a silent stall. + - Real spectrum analysis on macOS, Windows, and Linux: 32 log-spaced bands at ~25 Hz from the app's own playback, journaled at the effect boundary so record/replay repaints identical bars; hosts that cannot analyze report the capability honestly instead of fabricating bands. +- **Images**: a platform decode seam (CGImageSource, gdk-pixbuf, WIC) so the toolkit bundles no image decoders; runtime image registration renders through every path — GPU packets, software presentation, and screenshots — with pixels riding an out-of-band upload channel so image-bearing frames stay on the GPU path; avatars take a bound image with initials fallback. +- **Windowing and chrome**: model-declared secondary windows (presence is visibility; a user close dispatches a Msg), enforced window minimum sizes, and present-before-show so a canvas window never appears blank. + - Titlebar control on all three desktops: `hidden_inset`, a tall unified-toolbar variant, and fully `chromeless` styles; markup `window-drag` regions; and an `on_chrome` hook carrying the real overlay insets and control-cluster frames — with real system window controls preserved on Linux client-side decorations and Windows DWM caption buttons. + - Native context menus, declared per widget in Zig or markup (``): the real OS menu where one exists, an anchored canvas surface elsewhere, editable-text cut/copy/paste defaults, and full automation support for enumerating and invoking items. + - A menu-bar status item with model-driven title and menu; canvas and WebView panes composed in one window; adoption of app-owned native views into the layout (`adoptViewSurface`); and native scroll drivers on macOS that give every scroll region OS momentum, rubber-band overscroll, and the system overlay scrollbar with zero app code. +- **Experimental iOS and Android host tiers — the toolkit owns the entire mobile app**: complete UIKit and Android hosts ship in the SDK over the embed C ABI, an app project carries zero host code, and embedding a hand-written host stays first-class. + - `native dev --target ios|android [--device name]` builds, installs, and launches on a simulator or emulator and streams the app log; `native package --target ios` emits an archive-ready Xcode project and `--target android` a complete generated host project plus a debug-signed APK — no build-system project, no plugin matrix. + - Touch, soft keyboard, and IME forwarding; safe-area and keyboard insets on the window-chrome channel plus host-reported form factor; platform text metrics; platform audio and image decoding; and damage-rect rendering so a keystroke repaints and uploads only the changed region instead of the whole screen. + - Declared platform chrome: apps project a tab set and primary action as a real system tab bar, and a model-owned page stack drives real push/pop transitions with the system edge-swipe back gesture — navigation state stays in the model and replays deterministically from the Msg journal. + - The soundboard ships the proof: one codebase, a desktop composition plus a compact phone shell selected by the host-reported form factor, running on the simulator via `native dev --target ios`. +- **Theme packs and design tokens**: named packs — the default register plus `geist`, the design register of the bundled Geist type family — compose with the live system appearance; interaction-state formulas, control metrics, and focus-ring geometry are all token-stated; new `success`/`warning`/`info` semantic color tokens; the stock theme follows the OS light/dark, high-contrast, and reduce-motion settings live; modal scrims blur the content behind them for real; app-registered TrueType fonts resolve everywhere a font id rides. +- **Deterministic rendering core**: a bounded, std-only TTF parser inks real anti-aliased glyphs (bundled Geist and Geist Mono) on every headless path — screenshots, mobile embeds, pixel goldens — while layout measures exactly what gets inked; an allocation-free vector rasterizer with bit-identical cross-platform coverage draws paths, icons, and charts. +- **Automation and testing**: `native automate` gains `assert` (regex polling against the accessibility snapshot), deterministic PNG screenshots, per-stage frame profiling (`profile on`), and widget verbs for hold, secondary click, context-menu invocation, drag, wheel, and tray actions. + - Deterministic session record and replay: journal every platform event and effect result, then re-run headlessly with checkpoint verification (`native automate record` / `replay --verify`). + - `native init` scaffolds a CI workflow: null-platform tests for every frontend plus a Linux automation smoke that drives the app's real binary under Xvfb. +- **Accessibility as machine checks**: unnamed interactive controls, icon-only controls without labels, and misused roles are validation errors (degradations report as warnings; `--strict` promotes); a deterministic tree-level audit catches labels that resolve empty at runtime, focus-unreachable widgets, and duplicate sibling labels; and assistive actions actuate through the same activation paths keyboard users take instead of reporting success on nothing. +- **Showcase examples**: calculator, notes (folders, trash, context menus), soundboard (a real music library with playback and search), deck (a radically re-skinned sibling proving theme packs and chrome passes), system-monitor (live effects-driven sampling), markdown-viewer (split-pane editor and preview), and feed (a 100,000-post virtual list) — each with a deterministic test suite, and a prepared real-music catalog that streams out of the box. +- **Docs site**: a full Components section (34 pages) where every preview is rendered offscreen by the engine itself and upgrades on hover to a live engine instance running in-page via a ~306 KB (gzip) wasm build; attribute tables generate from the validator's own vocabulary so docs cannot drift; the whole site restructured native-first with new State & Data Flow, App & Runtime, Theming, and Testing in CI pages. +- **Zero-config toolchain and distribution**: `native dev|build|test|check` work in a directory holding only `app.zon` and `src/` (`native eject` writes the build files exactly once when you want to own them); the pinned Zig toolchain downloads on consent with checksum verification; and `@native-sdk/cli` installs from npm with zero scripts — eight platform binaries plus the SDK source, so `native init && native dev` work offline right after install. +- **One-image app icons**: drop a single square PNG or SVG in `assets/`, and `native package` generates everything — a masked, grid-correct macOS `.icns`, a multi-size Windows `.ico`, Linux hicolor PNGs, and iOS/Android catalog icons — with exact linear-light downscales, teaching errors for bad sources, and no external tools. + +### Improvements + +- **Performance — frame cost scales with what changed, not view size**: + - GPU packets ride a compact binary encoding (~10x smaller than JSON, ~40x effective capacity — text-heavy frames no longer silently fall back to software rendering), steady-state frames ship incremental patches (~20x less wire per interaction), and repaints derive per-change dirty-rect lists so pixels between two far-apart changes stay retained. + - Per-command raster caches stop re-rasterizing unchanged content (host draw p50 dropped an order of magnitude on animated views); frame planning and widget reconciliation moved from quadratic scans to indexed lookups (end-to-end interaction p50 improved ~2.3-3.2x on large views); backdrop blur cost no longer scales with radius; a click emits one display list instead of three. + - Launch to glass: the first canvas frame presents before the event loop starts, first paint rasterizes across cores, the main WebView is created lazily, and warm launches measured 150→120 ms on the heaviest showcase app; `NATIVE_SDK_WINDOW_TIMING=1` prints a per-phase launch breakdown. + - Occluded windows throttle to a ~1 Hz heartbeat instead of spinning the frame clock (spectrum reports pause too); accessibility publishes only when the tree actually changed and defer off the input-to-glass path; frame pacing delivers exactly one event per display interval; input latency is measured to the responding present, honestly. + - `zig build bench-render` runs deterministic interaction scenarios against committed per-scenario budgets, and a percentile GPU perf check gates first-frame and input-to-present latency in CI. +- **Component fidelity**: the built-in components land a refined default look, verified pixel-for-pixel in CI under both theme packs. + - Measured control geometry and state washes, ring-offset focus rings, flat buttons with a quiet destructive treatment, segmented button groups rendered as one bar with collapsed seams, compact badges, and hairline tables. + - Reworked accordion, tabs, alert, and card treatments with sensible per-kind layout defaults; skeletons pulse and the caret blinks; select menus read like menus (row highlight, trailing checkmark for the committed option). + - Native cursor conventions (the pointing hand is reserved for true links), flat list rows, axis-aware separators, and edge-pinned scrolling with opt-in rubber-band overscroll. +- **Capacity and honesty**: per-view widget budgets quadrupled to 1024 nodes (command, glyph, and text budgets raised to match) with headroom telemetry in every snapshot; explicit `width`/`height` are definite bounds; layout overflow is diagnosed, dispatch errors degrade and record instead of exiting the app, and every effect-facing type and constant is exported from the `native_sdk` facade. +- **Teaching validation**: handlers on elements that can never receive them, `gap` on stacking containers, `wrap` on non-text elements, and literal glyphs outside the bundled font's coverage are all positioned teaching errors, enforced identically by the validator, both engines, and the language server. +- **Desktop parity**: the Linux and Windows hosts reach the macOS seam contract — app timers, appearance events, window options at create, interactive window moves, IME composition on Windows, and hidden-titlebar fidelity with real system controls; CI gains Windows canvas and effects smokes under Wine, a headless Linux canvas smoke, and a containerized Linux live-truth harness driving every showcase app on real GTK. +- **Observability**: automation snapshots report the live present path and mode, patch sizes, fallback reasons with byte counts, budget headroom, audio state, tray contents, and per-stage frame percentiles while profiling; `NATIVE_SDK_GPU_DRAW_TRACE=1` attributes every present. +- **Docs and skill accuracy**: the code-signing page documents the real ad-hoc Gatekeeper experience, form-control and picker docs match what the engine ships, the keyboard and interaction seams are documented where developers look, and stale commands and API shapes were fixed across the site. +- **Example polish**: showcase headers carry only working controls under hidden-inset titlebars, the soundboard adopts desktop list-selection conventions, notes gains Recently Deleted and dialog autofocus, the deck refined its hardware identity across feedback passes, system-monitor lands the standard settings flow, and every showcase app ships the zero-config scaffold shape with a real neutral default app icon. +- **Contributor workflow**: changelog fragments (`changelog.d/`) end merge conflicts on this file, and `scripts/gate.sh` runs a tiered local gate that scales with the diff. + +### Bug Fixes + +- **Input and focus**: clicked and tabbed-into fields always show a caret (drawn in the field's own ink, readable in every scheme); Escape dismisses surfaces opened from non-focusable triggers; Enter inserts a newline in textareas (the primary chord submits); programmatic focus is quiet on non-editables; composite rows hover, point, and press as one surface; cross-centered overflow distributes evenly. +- **Model-driven control state**: sliders, exclusive selections, and toggle-button chips follow the model when the source moves (a live drag is never yanked); disabled selection controls render disabled; idle disabled buttons no longer wear an accent outline. +- **Rendering correctness**: pixel snapping no longer wraps exact-fit text or elides exact-fit badges; packet text honors engine line breaks; text bounds cover glyph ink; mono runs read as monospace on every headless path; avatar initials center; the spinner actually spins and sizes to the icon register; offscreen screenshots clear with live tokens; render animations invalidate only the affected commands; one invalid UTF-8 byte can no longer hang the renderer; budget overflows apply atomically instead of tearing the retained tree. +- **macOS**: Debug builds no longer abort at launch on an SDK sanitizer trap; `resizable = false` is honored; frames keep pumping during live resize and menu tracking; occluded windows keep presenting and flush instantly on reveal; quitting mid-playback no longer crashes; the Chromium (CEF) host builds and runs again, verified live with child WebViews. +- **Windows and Linux**: Windows apps launch on real Windows (common-controls manifest, dynamic task-dialog resolution) and builds link again; embed input timestamps and network error classification fixed on Windows; Linux audio no longer sticks in a buffering state; a saturated frame loop no longer freezes GTK windows; runtimes heap-allocate in every runner, fixing startup crashes under default stack limits; GTK initial allocation and overlay z-order fixed. +- **Packaging**: signed bundles keep a valid code signature; packaged apps read their bundled assets and show their display name in the menu bar; archives are labeled with the real optimize mode; unbundled dev runs fall back to the embedded default Dock icon. +- **Automation and CLI reliability**: commands queue with delivery acknowledgments instead of overwriting a single slot; a landing command wakes an idle app (~4 ms consumption); CLI and app handshake on a protocol version, and stale publishers or binaries are refused loudly; parseable payloads land on stdout; clicks aim at the rendered control, not its stretched box; `native dev` runs Debug so hot reload is actually compiled in; no CLI verb exits silently, and `--help` exits 0 everywhere. +- **Hardening**: the markdown renderer survives hostile input (three quadratic blowups fixed, a fuzz corpus added); large models neither exhaust the comptime branch quota nor ride the stack (`UiApp.create` constructs in place); mobile embed libraries stage per target so cross-target builds cannot poison each other; oversized inline window sources fail loudly instead of leaving a blank window; docs live previews build, lay out with the selected pack's tokens, animate, and route keyboard shortcuts correctly. +- **Measured-label controls no longer elide under pixel snapping**: a control sized exactly to its measured label — toggle chips (the system monitor's "PID" sort chip painted "PI…"), buttons, segmented controls and tab triggers, menu and list rows, tooltips, checkbox/radio/switch labels, hug-sized status bars — could lose a fraction of a pixel to render-time geometry snapping and swap real glyphs for an ellipsis. Every measured-label intrinsic width now rounds UP to the snap grid (the badge rule from the previous round), the switch additionally reserves its snapped track extent, and themes without geometry snapping stay bit-identical. + +### Contributors + +- @ctate + + +## 0.3.0 + +### New Features + - **Keyboard shortcuts**: Add app-level keyboard shortcuts with manifest and runtime configuration, native delivery to Zig `Event.shortcut`, and typed JavaScript `window.zero` shortcut events (#62). - **Manifest-driven runner shortcuts**: Load `app.zon` shortcuts automatically in generated runners, with a `RunOptions.shortcuts` override for apps that build shortcut lists in Zig (#62). @@ -23,7 +120,6 @@ All notable changes to zero-native will be documented in this file. ### Contributors - @ctate - ## 0.2.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e219f0d0..780cfbe6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -Thanks for helping improve zero-native. This guide is for maintainers and contributors working on the framework repository itself. +Thanks for helping improve the Native SDK. This guide is for maintainers and contributors working on the toolkit repository itself. For app author documentation, start at [zero-native.dev](https://zero-native.dev). @@ -14,7 +14,7 @@ For app author documentation, start at [zero-native.dev](https://zero-native.dev ## Local Checks -Run the framework tests: +Run the toolkit tests: ```bash zig build test @@ -41,8 +41,8 @@ zig build run-webview Check the npm CLI package: ```bash -npm --prefix packages/zero-native run version:check -npm --prefix packages/zero-native run scripts:check +npm --prefix packages/native-sdk run version:check +npm --prefix packages/native-sdk run scripts:check ``` Check the documentation site: @@ -63,7 +63,7 @@ zig build run-webview -Dweb-engine=system For Chromium on macOS, install CEF and run with the Chromium engine: ```bash -zero-native cef install +native cef install zig build run-webview -Dweb-engine=chromium ``` @@ -85,11 +85,17 @@ zig build package Package explicitly through the CLI: ```bash -zero-native package --target macos --manifest app.zon --assets assets --binary zig-out/lib/libzero-native.a +native package --target macos --manifest app.zon --assets assets --binary zig-out/lib/libnative-sdk.a ``` For Chromium packages, configure `.web_engine = "chromium"` and `.cef` in `app.zon`, or use temporary `--web-engine` and `--cef-dir` overrides while testing. +Verify an ad-hoc signed package's code signature survives packaging intact (macOS; skips loudly on hosts without `codesign`): + +```bash +zig build test-package-signing +``` + ## Automation Development Enable automation in a build: @@ -101,19 +107,22 @@ zig build run-webview -Dautomation=true Interact with the running app: ```bash -zero-native automate wait -zero-native automate list -zero-native automate bridge '{"id":"ping","command":"native.ping","payload":null}' +native automate wait +native automate list +native automate bridge '{"id":"ping","command":"native.ping","payload":null}' ``` -Automation writes artifacts under `.zig-cache/zero-native-automation`. +Automation writes artifacts under `.zig-cache/native-sdk-automation`. ## Making a Pull Request -Thank you for your contribution! Please follow these steps to ensure a smooth review process: -1. Fork the repository and create a new branch for your feature or bug fix. -2. Make your changes and commit them with clear, descriptive messages. -3. Push your branch to your forked repository. -4. Open a pull request against the main repository's `main` branch. -Please cryptographically sign your commits so they show as **Verified** on GitHub. This requires a GPG or SSH signing key added to your GitHub account — see [GitHub's guide](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification). Note: the `Signed-off-by` trailer (`git commit -s`) is a DCO attestation and does **not** produce the Verified badge; you need `git commit -S` (uppercase) or `commit.gpgsign = true` in your git config. \ No newline at end of file +Branch from `main` (fork first if you don't have push access), keep the change focused, and run the tiered local gate before opening the PR: + +```bash +scripts/gate.sh fast # root suites + the example suites your diff touches +``` + +If the change is user-visible, add a changelog fragment in `changelog.d/` (see [changelog.d/README.md](./changelog.d/README.md)) instead of editing `CHANGELOG.md`. Open the PR against `main` describing what changed and why; for larger changes, open an issue first so the design can be discussed. + +Commits must be cryptographically signed (`git commit -S`, or set `commit.gpgsign = true`) so they show as **Verified** — the `Signed-off-by` trailer from `git commit -s` is a DCO attestation, not a signature. \ No newline at end of file diff --git a/README.md b/README.md index 6fff9d7e..5eb22f93 100644 --- a/README.md +++ b/README.md @@ -1,139 +1,132 @@ -# zero-native +# Native SDK -Build native desktop apps with Zig, secure WebView surfaces, native controls, and OS capabilities. Tiny binaries. Minimal memory. Instant rebuilds. +**Native SDK is the complete toolkit for building native desktop applications.** -zero-native is a native app framework where WebView content is one first-class surface, not the whole app model. Use native windows, menus, shortcuts, controls, dialogs, and OS services around rich web product UI. Use the platform WebView when you want the smallest possible app, or bundle Chromium through CEF when rendering consistency matters. +Native SDK exists because expressive UI and native performance should not be competing goals. Developers often choose web-based runtimes because they offer freedom, speed and control over the product experience. But that freedom often comes with a heavy runtime. Native SDK keeps the expressive authoring model and replaces the runtime with native rendering. -## Quick Start +Views are declarative markup in `.native` files, logic is plain Zig, and Native SDK's own engine draws every pixel into real OS windows — no browser, no WebView, no interpreter in the binary. + + + + The Soundboard example app rendered by the Native SDK engine: a music library with album cover art, search, and a playback bar + + + + + + + +
+ + + The Notes example app rendered by the Native SDK engine: a three-pane notes manager with folders, a note list, and an open note + + + + + The Calculator example app rendered by the Native SDK engine: a finished calculation above a full keypad + +
+ +Soundboard, Notes, and Calculator from examples/ — every pixel drawn by the Native SDK engine, captured through its deterministic reference renderer. The images follow your color scheme. + +## Quick start Install the CLI: ```bash -npm install -g zero-native +npm install -g @native-sdk/cli ``` Create and run an app: ```bash -zero-native init my_app --frontend next +native init my_app cd my_app -zig build run +native dev ``` -The first run installs frontend dependencies, builds the generated native shell, and opens a desktop window rendering your WebView content. +A native window opens with a working counter. The whole view is `src/app.native` — a markup file that binds values and dispatches messages: -Read the full guide at [zero-native.dev/quick-start](https://zero-native.dev/quick-start). +```html + + + + {count} + + + count: {count} + +``` -## Why zero-native - -### Native shell, web where it fits - -Build app chrome and trusted utility surfaces with native views, while keeping WebViews available for rich product UI, frontend framework workflows, and rendering consistency when you need it. - -### Tiny and fast - -System WebView apps do not bundle a browser runtime, so the native shell stays small and starts quickly. Your app uses WKWebView on macOS and WebKitGTK on Linux. - -### Choose your web engine - -Pick the engine that fits the product. System WebView gives you a lightweight native footprint. Chromium through CEF gives you predictable rendering and a pinned web platform on supported targets. - -### Fast native rebuilds - -The native layer is Zig, so app logic, bridge commands, and platform integrations rebuild quickly. Your frontend can still use the web tooling you already know. - -### OS power without heavy glue - -Zig calls C directly, which keeps platform SDKs, native libraries, codecs, and local system integrations within reach when the WebView layer needs to do real native work. - -### Explicit security model - -The WebView is treated as untrusted by default. Native commands, permissions, navigation, external links, and window APIs are opt-in and policy controlled. - -## Status - -zero-native is pre-release. Desktop support now covers macOS 11+, Linux, and Windows build paths, native controls on system-WebView hosts, and Chromium/CEF distributed as platform-specific runtimes. - -## Core Concepts - -`App` is the small Zig object that describes your application: name, WebView source, lifecycle hooks, an optional native scene, and native services. - -`Runtime` owns the event loop, windows, native views, WebViews, command routing, bridge dispatch, automation hooks, tracing, and platform services. - -`ShellConfig` declares native-first windows and view trees: toolbars, sidebars, status bars, split panes, stacks, controls, WebViews, and future surface kinds. - -`WebViewSource` tells the runtime what a WebView should load: inline HTML, a URL, or packaged frontend assets served from a local app origin. - -`app.zon` is the app manifest. It declares app metadata, icons, windows, native shell views, frontend assets, web engine selection, security policy, bridge permissions, and packaging inputs. - -`window.zero.*` is the guarded JavaScript-to-native bridge for commands, windows, views, WebViews, dialogs, clipboard, credentials, and OS services. Calls are size-limited, origin checked, permission checked, and routed only to allowed handlers. - -## Configuration - -Most project-level behavior lives in `app.zon`: +All logic lives in `src/main.zig`: a `Model` struct, a `Msg` union, and one `update` function — the only place state changes: ```zig -.{ - .id = "com.example.my-app", - .name = "my-app", - .display_name = "My App", - .version = "0.1.0", - .web_engine = "system", - .permissions = .{}, - .capabilities = .{ "webview" }, - .security = .{ - .navigation = .{ - .allowed_origins = .{ "zero://app", "http://127.0.0.1:5173" }, - }, - }, - .windows = .{ - .{ .label = "main", .title = "My App", .width = 960, .height = 640 }, - }, +pub fn update(model: *Model, msg: Msg) void { + switch (msg) { + .increment => model.count += 1, + .decrement => model.count -= 1, + .reset => model.count = 0, + } } ``` -Use `.web_engine = "system"` for the platform WebView. On supported macOS builds, use `.web_engine = "chromium"` with a `.cef` config when you want to bundle Chromium. +Edit `src/app.native` while `native dev` runs and the window updates in place, keeping your state. `native check` validates every view in milliseconds without building, `native test` runs full-loop UI tests headlessly, and `native build` produces an optimized release binary. + +Read the full guide at [zero-native.dev/quick-start](https://zero-native.dev/quick-start). + +## What you get + +**Beautiful by default** — Great software should not start from a blank slate. The built-in component catalog — buttons, tabs, text fields, dialogs, charts, virtual lists, and more — ships with considered typography, spacing, and color, so the app `native init` scaffolds already looks intentional the first time its window opens. + +**Customizable by design** — Your app should have its own identity, not ours. Styling is design tokens end to end: color, radius, and typography resolve by name, re-resolve live when the theme changes, and can be replaced wholesale — `examples/soundboard` and `examples/deck` are the same music player separated only by tokens and a chrome pass. + +**Native from the start** — Every interface is rendered without a browser or WebView. The engine draws into real OS windows while scroll physics, menus, dialogs, the tray, and text input stay with the operating system, and markup compiles into the executable at build time, so a release build carries no parser or interpreter — the scaffolded counter app builds to a single binary a few megabytes small. + +**Predictable state** — State changes should be explicit, inspectable and easy to reason about. Events produce messages, messages update state, and state renders the interface; markup can bind and dispatch but never mutate. The loop is so deterministic that `native automate record` journals a session and `replay` reproduces it headlessly, verified frame by frame against state fingerprints. + +**Simple authoring** — Interfaces should be easy to read, easy to write and easy to generate. Views are elements, flex layout, `{bindings}`, and expressions like `selected="{f == filter}"`, and `native check` validates every view against your app's actual `Model` and `Msg` — bindings, iterables, message tags — in milliseconds, with `file:line:column` errors that teach. + +**AI is part of the workflow** — Native SDK is designed for a world where humans and AI agents build software together. Every app embeds an automation server, so any agent can read accessibility snapshots, drive widgets, assert on live state, and take deterministic screenshots of the running window; accessibility findings are machine-checked in `native check`; and the CLI ships the agent skills that teach all of it (`native skills list`). + +## Examples + +The apps pictured above live in [examples/](./examples), most as zero-config projects — `app.zon` plus `src/`, no build files — run straight from their directory with `native dev`. + +| Example | What it shows | +| --- | --- | +| [`calculator`](./examples/calculator) | A complete small app: markup keypad, keyboard input, chrome shortcuts, theming. | +| [`notes`](./examples/notes) | Persistence through the effects channel: debounced writes, restore on boot, dialogs, search. | +| [`soundboard`](./examples/soundboard) | Album grid with decoded cover art, context menus, timers, and a custom theme. | +| [`deck`](./examples/deck) | The soundboard player rebuilt as a dense hardware chassis: two windows, same widgets, different tokens. | +| [`feed`](./examples/feed) | A 100,000-row list, virtualized with runtime-owned scrolling. | + +The full catalog in [examples/README.md](./examples/README.md) also covers guarded OS capabilities, GPU surfaces, WebView composition, web-frontend shells, and the iOS/Android embed hosts. + +## Platforms + +macOS is the primary development platform and carries the deepest support: Metal presentation, OS scroll physics, native context menus, app menus, tray, and dialogs. Linux runs the full showcase through the deterministic software renderer in real windows, with pointer, keyboard, scroll, IME composition, and HiDPI; Windows runs on a Win32 host with IME composition and is exercised in CI, including real input injection. Mobile support is experimental: iOS is simulator-proven through the embed library and Android cross-compiles with the full embed ABI, but APIs and tooling on both are still evolving — desktop is the mature surface. WebView surfaces coexist on every desktop platform. The [platform support matrix](https://zero-native.dev/platform-support) documents exactly what each host supports today. ## Documentation The full documentation is at [zero-native.dev](https://zero-native.dev). -- [Quick Start](https://zero-native.dev/quick-start) -- [Web Engines](https://zero-native.dev/web-engines) -- [App Model](https://zero-native.dev/app-model) -- [Native Surfaces](https://zero-native.dev/native-surfaces) -- [Native Controls](https://zero-native.dev/native-controls) -- [Commands](https://zero-native.dev/commands) -- [Capabilities](https://zero-native.dev/capabilities) -- [Platform Support](https://zero-native.dev/platform-support) -- [Bridge](https://zero-native.dev/bridge) -- [Security](https://zero-native.dev/security) -- [Packaging](https://zero-native.dev/packaging) +- [Quick Start](https://zero-native.dev/quick-start) — install to a running, tested app +- [Philosophy](https://zero-native.dev/philosophy) — the six principles behind the toolkit +- [App Model](https://zero-native.dev/app-model) — the model/message/update loop, wiring, and hot reload +- [Native UI](https://zero-native.dev/native-ui) — every element, attribute, and pattern in the markup +- [Components](https://zero-native.dev/components) — the component catalog +- [State & Data Flow](https://zero-native.dev/state) — derive-don't-store, bindings, and text editing +- [Testing](https://zero-native.dev/testing) — full-loop UI tests, headless on any machine +- [Automation](https://zero-native.dev/automation) — snapshots, widget driving, record/replay, screenshots +- [Capabilities](https://zero-native.dev/capabilities) — guarded OS services: notifications, clipboard, dialogs, credentials +- [Packaging](https://zero-native.dev/packaging) — from binary to distributable app +- [Platform Support](https://zero-native.dev/platform-support) — what each host supports today -## Examples +## Contributing -Framework-specific starter examples live in `examples/`: +Native SDK is pre-1.0: APIs still move, and the toolkit is evolving quickly. Bug reports and focused pull requests are welcome — for larger changes, open an issue first so the design can be discussed. See [CONTRIBUTING.md](./CONTRIBUTING.md) for the development setup and local checks. -- `examples/next` -- `examples/react` -- `examples/svelte` -- `examples/vue` +## License -Each example is a complete zero-native app with `app.zon`, a Zig shell, and a minimal frontend project. Run one with `zig build run` from its directory. - -Native-first examples are available too: - -- `examples/command-app` - shared command routing across native controls, menus, shortcuts, tray, and bridge calls -- `examples/native-shell` - native toolbar/sidebar/statusbar chrome around WebView content -- `examples/native-panels` - split/stack native panel composition with WebView content -- `examples/capabilities` - guarded OS services such as notifications, clipboard, dialogs, credentials, file drops, and recent documents -- `examples/mobile-shell` - shared metadata for the iOS and Android native shell hosts - -Mobile embedding examples are available too: - -- `examples/ios` -- `examples/android` - -These show how an iOS or Android host app links the zero-native C ABI from `libzero-native.a`. - -For local framework development, see [CONTRIBUTING.md](./CONTRIBUTING.md). +[Apache-2.0](./LICENSE) diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..d8d5ab23 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,17 @@ +# Releasing + +Releases are manual, single-PR affairs. The maintainer controls the changelog voice and format. + +To prepare a release: + +1. Create a branch (e.g. `prepare-v1.2.0`) +2. Bump the version in `packages/native-sdk/package.json` +3. Run `npm --prefix packages/native-sdk run version:sync` to update all version references +4. Run `scripts/changelog-merge.sh` to fold any pending `changelog.d/` fragments into the `## Unreleased` section +5. Write the changelog entry in `CHANGELOG.md`, wrapped in `` and `` markers +6. Remove the `` and `` markers from the previous release entry; only the latest release should have markers +7. Open a PR and merge to `main` + +CI compares the version in `packages/native-sdk/package.json` to what's on npm. If it differs, it cross-builds the CLI for every platform, creates the GitHub release with the binaries, publishes the per-platform binary packages (`packages/native-sdk/npm/*`), and publishes `@native-sdk/cli` last — so the main package only lands once every binary package it pins is live. If npm already has the version but the GitHub release is missing assets, CI recreates the GitHub release from the marked changelog entry. + +Publishing uses npm trusted publishing (OIDC) — there is no npm token secret. One-time setup: on npmjs.com, each of the nine packages (`@native-sdk/cli` plus the eight `@native-sdk/cli-*` platform packages under `packages/native-sdk/npm/*`) must have a GitHub Actions trusted publisher configured with repository `vercel-labs/zero-native`, workflow `release.yml`, and environment `Release`. Every publish runs with `--provenance`. If a package is missing its trusted-publisher configuration, `npm publish` fails loudly with an OIDC authentication error for that package. diff --git a/app.zon b/app.zon index 07bd4baf..954b334c 100644 --- a/app.zon +++ b/app.zon @@ -1,7 +1,7 @@ .{ - .id = "dev.zero_native", - .name = "zero-native", - .display_name = "zero-native", + .id = "dev.native_sdk", + .name = "native-sdk", + .display_name = "Native SDK", .version = "0.1.0", .icons = .{ "assets/icon.icns", "assets/icon.ico" }, .platforms = .{ "macos" }, @@ -10,10 +10,10 @@ .bridge = .{ .commands = .{ .{ .name = "native.ping", .origins = .{ "zero://inline", "zero://app" } }, - .{ .name = "zero-native.window.list", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } }, - .{ .name = "zero-native.window.create", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } }, - .{ .name = "zero-native.window.focus", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } }, - .{ .name = "zero-native.window.close", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } }, + .{ .name = "native-sdk.window.list", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } }, + .{ .name = "native-sdk.window.create", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } }, + .{ .name = "native-sdk.window.focus", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } }, + .{ .name = "native-sdk.window.close", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } }, }, }, .security = .{ @@ -25,6 +25,6 @@ .web_engine = "system", .cef = .{ .dir = "third_party/cef/macos", .auto_install = false }, .windows = .{ - .{ .label = "main", .title = "zero-native", .width = 720, .height = 480, .restore_state = true }, + .{ .label = "main", .title = "native-sdk", .width = 720, .height = 480, .restore_state = true }, }, } diff --git a/assets/icon.icns b/assets/icon.icns index bcc99be1..98d4303d 100644 Binary files a/assets/icon.icns and b/assets/icon.icns differ diff --git a/assets/icon.ico b/assets/icon.ico index b892f4c2..e2804e05 100644 Binary files a/assets/icon.ico and b/assets/icon.ico differ diff --git a/assets/icon.png b/assets/icon.png index 027a83f4..6e8503ef 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/assets/icon.svg b/assets/icon.svg new file mode 100644 index 00000000..f298ddbe --- /dev/null +++ b/assets/icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/zero-native.entitlements b/assets/native-sdk.entitlements similarity index 100% rename from assets/zero-native.entitlements rename to assets/native-sdk.entitlements diff --git a/assets/native-sdk.manifest b/assets/native-sdk.manifest new file mode 100644 index 00000000..a2ef68d8 --- /dev/null +++ b/assets/native-sdk.manifest @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/build.zig b/build.zig index f359c221..1b588363 100644 --- a/build.zig +++ b/build.zig @@ -35,6 +35,14 @@ const SigningMode = enum { identity, }; +pub const AppOptions = @import("build/app.zig").AppOptions; +pub const addApp = @import("build/app.zig").addApp; +pub const AppArtifacts = @import("build/app.zig").AppArtifacts; +pub const addAppArtifacts = @import("build/app.zig").addAppArtifacts; +pub const MobileLibOptions = @import("build/app.zig").MobileLibOptions; +pub const addMobileLib = @import("build/app.zig").addMobileLib; +const mobile_export_symbol_names = @import("build/app.zig").mobile_export_symbol_names; + pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const host_target = b.graph.host; @@ -42,7 +50,7 @@ pub fn build(b: *std.Build) void { const platform_option = b.option(PlatformOption, "platform", "Desktop backend: auto, null, macos, linux, windows") orelse .auto; const trace_option = b.option(TraceOption, "trace", "Trace output: off, events, runtime, all") orelse .events; _ = b.option(bool, "debug-overlay", "Enable debug overlay output") orelse false; - _ = b.option(bool, "automation", "Enable zero-native automation artifacts") orelse false; + _ = b.option(bool, "automation", "Enable Native SDK automation artifacts") orelse false; _ = b.option(bool, "webview", "Deprecated compatibility flag; native surfaces are always enabled") orelse true; const web_engine_override = b.option(WebEngineOption, "web-engine", "Override app.zon web engine: system, chromium"); const cef_dir_override = b.option([]const u8, "cef-dir", "Override CEF root directory for Chromium builds"); @@ -52,8 +60,11 @@ pub fn build(b: *std.Build) void { const signing_mode = b.option(SigningMode, "signing", "Signing mode: none, adhoc, identity") orelse .none; const package_version = packageVersion(b); const optimize_name = @tagName(optimize); - const app_web_engine = web_engine_tool.readManifestConfig(b.allocator, b.graph.io, "app.zon") catch |err| { - std.debug.panic("failed to read app.zon web engine config: {s}", .{@errorName(err)}); + // Resolve against THIS build's root: as a dependency of a user app the + // build runner's cwd is the app project, and a cwd-relative "app.zon" + // would read (and panic on) the user's manifest instead of ours. + const app_web_engine = web_engine_tool.readManifestConfig(b.allocator, b.graph.io, b.pathFromRoot("app.zon")) catch |err| { + std.debug.panic("failed to read the framework's own app.zon web engine config: {s}", .{@errorName(err)}); }; const resolved_web_engine = web_engine_tool.resolve(app_web_engine, .{ .web_engine = if (web_engine_override) |value| webEngineFromBuildOption(value) else null, @@ -91,6 +102,18 @@ pub fn build(b: *std.Build) void { const diagnostics_mod = module(b, target, optimize, "src/primitives/diagnostics/root.zig"); const platform_info_mod = module(b, target, optimize, "src/primitives/platform_info/root.zig"); const json_mod = module(b, target, optimize, "src/primitives/json/root.zig"); + const canvas_mod = module(b, target, optimize, "src/primitives/canvas/root.zig"); + canvas_mod.addImport("geometry", geometry_mod); + canvas_mod.addImport("json", json_mod); + if (target.result.os.tag == .macos) { + // The estimator-vs-CoreText agreement test (text_metrics_tests.zig) + // shapes the bundled face through CoreText; apps already link these + // transitively via AppKit. + canvas_mod.linkFramework("CoreFoundation", .{}); + canvas_mod.linkFramework("CoreGraphics", .{}); + canvas_mod.linkFramework("CoreText", .{}); + canvas_mod.linkSystemLibrary("c", .{}); + } const debug_mod = module(b, target, optimize, "src/debug/root.zig"); debug_mod.addImport("app_dirs", app_dirs_mod); debug_mod.addImport("trace", trace_mod); @@ -103,6 +126,7 @@ pub fn build(b: *std.Build) void { const diagnostics_tests = testArtifact(b, diagnostics_mod); const platform_info_tests = testArtifact(b, platform_info_mod); const json_tests = testArtifact(b, json_mod); + const canvas_tests = testArtifact(b, canvas_mod); const desktop_mod = module(b, target, optimize, "src/root.zig"); desktop_mod.addImport("geometry", geometry_mod); @@ -113,33 +137,43 @@ pub fn build(b: *std.Build) void { desktop_mod.addImport("diagnostics", diagnostics_mod); desktop_mod.addImport("platform_info", platform_info_mod); desktop_mod.addImport("json", json_mod); - desktop_mod.export_symbol_names = &.{ - "zero_native_app_create", - "zero_native_app_destroy", - "zero_native_app_start", - "zero_native_app_activate", - "zero_native_app_deactivate", - "zero_native_app_stop", - "zero_native_app_resize", - "zero_native_app_touch", - "zero_native_app_command", - "zero_native_app_frame", - "zero_native_app_set_asset_root", - "zero_native_app_last_command_count", - "zero_native_app_last_command_name", - "zero_native_app_last_error_name", - }; + desktop_mod.addImport("canvas", canvas_mod); const desktop_tests = testArtifact(b, desktop_mod); + const desktop_test_shards = desktopTestShardArtifacts(b, desktop_mod); + // The embeddable static library's root module carries only the C ABI + // exports (fixed WebView shell host); user-app canvas libraries are + // produced by `addMobileLib` from src/embed/app_exports.zig instead. + const embed_exports_mod = module(b, target, optimize, "src/embed/c_exports.zig"); + embed_exports_mod.addImport("native_sdk", desktop_mod); + embed_exports_mod.export_symbol_names = &mobile_export_symbol_names; const embed_lib = b.addLibrary(.{ .linkage = .static, - .name = "zero-native", - .root_module = desktop_mod, + .name = "native-sdk", + .root_module = embed_exports_mod, + // The embed C ABI (`native_sdk_app_viewport`) is exactly the + // f32-heavy SysV signature Zig 0.16.0's self-hosted x86_64 backend + // miscompiles (see useLlvmWorkaround in build/app.zig): without + // this, Debug x86_64 libs (Android emulators, Intel simulators) + // hand clang hosts corrupted inset/keyboard floats. addMobileLib + // already forces LLVM there; the fixed-shell lib must match. + .use_llvm = @import("build/app.zig").useLlvmWorkaround(target), }); b.installArtifact(embed_lib); const automation_protocol_mod = module(b, target, optimize, "src/automation/protocol.zig"); const automation_protocol_tests = testArtifact(b, automation_protocol_mod); + // The app-icon pipeline as a standalone module: tooling needs only + // the vector core + PNG codec slice of canvas, not the full canvas + // module (which links platform frameworks on macOS and would weigh + // down the cross-compiled CLI). + const app_icon_mod = module(b, target, optimize, "src/primitives/canvas/app_icon.zig"); + app_icon_mod.addImport("geometry", geometry_mod); + // The iOS and Android host sources as embedded bytes: the tooling + // module writes and compiles them for `native dev|package --target + // ios|android`. + const ios_host_mod = module(b, target, optimize, "src/platform/ios/files.zig"); + const android_host_mod = module(b, target, optimize, "src/platform/android/files.zig"); const tooling_mod = module(b, target, optimize, "src/tooling/root.zig"); tooling_mod.addImport("assets", assets_mod); tooling_mod.addImport("app_dirs", app_dirs_mod); @@ -148,17 +182,55 @@ pub fn build(b: *std.Build) void { tooling_mod.addImport("debug", debug_mod); tooling_mod.addImport("platform_info", platform_info_mod); tooling_mod.addImport("trace", trace_mod); + tooling_mod.addImport("app_icon", app_icon_mod); + tooling_mod.addImport("ios_host", ios_host_mod); + tooling_mod.addImport("android_host", android_host_mod); const tooling_tests = testArtifact(b, tooling_mod); - const cli_mod = module(b, target, optimize, "tools/zero-native/main.zig"); + // Ejected-component identity proofs: a separate test module because + // the canonical component sources under src/tooling/components/ + // import `native_sdk` exactly as they will inside an app after + // `native eject component ` copies them there. + const eject_components_mod = module(b, target, optimize, "src/tooling/components/identity_tests.zig"); + eject_components_mod.addImport("native_sdk", desktop_mod); + const eject_components_tests = testArtifact(b, eject_components_mod); + + const ui_markup_mod = module(b, target, optimize, "src/primitives/canvas/ui_markup.zig"); + const markup_lsp_mod = module(b, target, optimize, "tools/native-sdk/markup_lsp.zig"); + markup_lsp_mod.addImport("ui_markup", ui_markup_mod); + const markup_lsp_tests = testArtifact(b, markup_lsp_mod); + + const automation_cli_mod = module(b, target, optimize, "tools/native-sdk/automation.zig"); + automation_cli_mod.addImport("automation_protocol", automation_protocol_mod); + automation_cli_mod.addImport("ui_markup", ui_markup_mod); + const automation_cli_tests = testArtifact(b, automation_cli_mod); + + // `native version` names the commit the binary was built from, so + // binary/framework skew ("your native binary may be stale") is a + // one-command check. Falls back to "unknown" outside a git checkout. + const cli_build_info = b.addOptions(); + cli_build_info.addOption([]const u8, "build_commit", cliBuildCommit(b)); + + const cli_mod = module(b, target, optimize, "tools/native-sdk/main.zig"); cli_mod.addImport("tooling", tooling_mod); cli_mod.addImport("automation_protocol", automation_protocol_mod); + cli_mod.addImport("ui_markup", ui_markup_mod); + cli_mod.addImport("markup_lsp", markup_lsp_mod); + cli_mod.addOptions("cli_build_info", cli_build_info); const cli_exe = b.addExecutable(.{ - .name = "zero-native", + .name = "native", .root_module = cli_mod, }); b.installArtifact(cli_exe); + // `zig build cli` builds and installs ONLY the CLI executable. The + // release pipeline cross-compiles it for every supported platform + // (packages/native-sdk/scripts/build-binaries.sh), and skipping the + // framework libraries, examples, and docs artifacts keeps that + // eight-target loop fast. + const cli_step = b.step("cli", "Build only the native CLI executable (cross-compile friendly)"); + cli_step.dependOn(&b.addInstallArtifact(cli_exe, .{}).step); + const host_assets_mod = module(b, host_target, optimize, "src/primitives/assets/root.zig"); const host_app_dirs_mod = module(b, host_target, optimize, "src/primitives/app_dirs/root.zig"); const host_app_manifest_mod = module(b, host_target, optimize, "src/primitives/app_manifest/root.zig"); @@ -169,6 +241,11 @@ pub fn build(b: *std.Build) void { host_debug_mod.addImport("app_dirs", host_app_dirs_mod); host_debug_mod.addImport("trace", host_trace_mod); const host_automation_protocol_mod = module(b, host_target, optimize, "src/automation/protocol.zig"); + const host_geometry_mod = module(b, host_target, optimize, "src/primitives/geometry/root.zig"); + const host_app_icon_mod = module(b, host_target, optimize, "src/primitives/canvas/app_icon.zig"); + host_app_icon_mod.addImport("geometry", host_geometry_mod); + const host_ios_host_mod = module(b, host_target, optimize, "src/platform/ios/files.zig"); + const host_android_host_mod = module(b, host_target, optimize, "src/platform/android/files.zig"); const host_tooling_mod = module(b, host_target, optimize, "src/tooling/root.zig"); host_tooling_mod.addImport("assets", host_assets_mod); host_tooling_mod.addImport("app_dirs", host_app_dirs_mod); @@ -177,19 +254,128 @@ pub fn build(b: *std.Build) void { host_tooling_mod.addImport("debug", host_debug_mod); host_tooling_mod.addImport("platform_info", host_platform_info_mod); host_tooling_mod.addImport("trace", host_trace_mod); - const host_cli_mod = module(b, host_target, optimize, "tools/zero-native/main.zig"); + host_tooling_mod.addImport("app_icon", host_app_icon_mod); + host_tooling_mod.addImport("ios_host", host_ios_host_mod); + host_tooling_mod.addImport("android_host", host_android_host_mod); + const host_ui_markup_mod = module(b, host_target, optimize, "src/primitives/canvas/ui_markup.zig"); + const host_markup_lsp_mod = module(b, host_target, optimize, "tools/native-sdk/markup_lsp.zig"); + host_markup_lsp_mod.addImport("ui_markup", host_ui_markup_mod); + const host_cli_mod = module(b, host_target, optimize, "tools/native-sdk/main.zig"); host_cli_mod.addImport("tooling", host_tooling_mod); host_cli_mod.addImport("automation_protocol", host_automation_protocol_mod); + host_cli_mod.addImport("ui_markup", host_ui_markup_mod); + host_cli_mod.addImport("markup_lsp", host_markup_lsp_mod); + host_cli_mod.addOptions("cli_build_info", cli_build_info); const host_cli_exe = b.addExecutable(.{ - .name = "zero-native", + .name = "native", .root_module = host_cli_mod, }); + // Docs component-preview generator: renders the built-in component + // catalog offscreen through the deterministic reference renderer and + // writes theme-aware webp pairs plus the markup vocabulary JSON into + // docs/. Regenerate with `zig build docs-component-previews`. + const docs_previews_mod = module(b, target, optimize, "tools/docs_component_previews.zig"); + docs_previews_mod.addImport("native_sdk", desktop_mod); + // The eject registry as its own lean module (its imports stay inside + // src/tooling/), so the vocab JSON's `ejectable` table is written from + // the same rows `native eject component` dispatches on — the docs' + // resolves from that JSON and can never drift. + docs_previews_mod.addImport("eject_components", module(b, target, optimize, "src/tooling/eject_components.zig")); + const docs_previews_exe = b.addExecutable(.{ + .name = "docs-component-previews", + .root_module = docs_previews_mod, + }); + const run_docs_previews = b.addRunArtifact(docs_previews_exe); + run_docs_previews.addArg(b.pathFromRoot("docs/public/components")); + run_docs_previews.addArg(b.pathFromRoot("docs/src/lib/component-vocab.json")); + run_docs_previews.has_side_effects = true; + const docs_previews_step = b.step("docs-component-previews", "Render built-in component previews and vocab JSON into docs/"); + docs_previews_step.dependOn(&run_docs_previews.step); + + // Render macro-benchmark: deterministic scenarios through the REAL + // engine pipeline (UiApp + Runtime + null-platform binary packet + // presents), reporting end-to-end and per-stage p50/p90 per + // interaction. Baselines should come from + // `zig build bench-render -Doptimize=ReleaseFast`. + const bench_render_mod = module(b, target, optimize, "tools/bench_render.zig"); + bench_render_mod.addImport("native_sdk", desktop_mod); + const bench_render_exe = b.addExecutable(.{ + .name = "bench-render", + .root_module = bench_render_mod, + }); + const run_bench_render = b.addRunArtifact(bench_render_exe); + run_bench_render.has_side_effects = true; + // Repo root as cwd so the relative budgets path in the docs/gate + // invocation resolves regardless of where `zig build` ran from. + run_bench_render.setCwd(b.path(".")); + // Ratchet mode: `zig build bench-render -Doptimize=ReleaseFast -- + // --check tools/bench-render-budgets.txt` compares the median e2e + // p50 of three suite passes against the committed budgets (the + // benchmark refuses --check outside ReleaseFast). + if (b.args) |bench_args| run_bench_render.addArgs(bench_args); + const bench_render_step = b.step("bench-render", "Run the render macro-benchmark (deterministic scenarios; pass -Doptimize=ReleaseFast for baselines, `-- --check tools/bench-render-budgets.txt` for the budget ratchet)"); + bench_render_step.dependOn(&run_bench_render.step); + + // Live docs previews: the same scene catalog compiled to + // wasm32-freestanding (tools/docs_wasm_preview.zig) so the docs + // upgrade the static webp tiles to interactive engine instances. + // ReleaseSmall + strip keep the module small enough to lazy-load. + const wasm_target = b.resolveTargetQuery(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }); + const wasm_optimize: std.builtin.OptimizeMode = .ReleaseSmall; + const wasm_geometry_mod = module(b, wasm_target, wasm_optimize, "src/primitives/geometry/root.zig"); + const wasm_json_mod = module(b, wasm_target, wasm_optimize, "src/primitives/json/root.zig"); + const wasm_canvas_mod = module(b, wasm_target, wasm_optimize, "src/primitives/canvas/root.zig"); + wasm_canvas_mod.addImport("geometry", wasm_geometry_mod); + wasm_canvas_mod.addImport("json", wasm_json_mod); + const wasm_native_mod = module(b, wasm_target, wasm_optimize, "src/root.zig"); + wasm_native_mod.addImport("geometry", wasm_geometry_mod); + wasm_native_mod.addImport("json", wasm_json_mod); + wasm_native_mod.addImport("canvas", wasm_canvas_mod); + wasm_native_mod.addImport("app_dirs", module(b, wasm_target, wasm_optimize, "src/primitives/app_dirs/root.zig")); + wasm_native_mod.addImport("assets", module(b, wasm_target, wasm_optimize, "src/primitives/assets/root.zig")); + wasm_native_mod.addImport("trace", module(b, wasm_target, wasm_optimize, "src/primitives/trace/root.zig")); + wasm_native_mod.addImport("app_manifest", module(b, wasm_target, wasm_optimize, "src/primitives/app_manifest/root.zig")); + wasm_native_mod.addImport("diagnostics", module(b, wasm_target, wasm_optimize, "src/primitives/diagnostics/root.zig")); + wasm_native_mod.addImport("platform_info", module(b, wasm_target, wasm_optimize, "src/primitives/platform_info/root.zig")); + const docs_wasm_preview_mod = module(b, wasm_target, wasm_optimize, "tools/docs_wasm_preview.zig"); + docs_wasm_preview_mod.addImport("native_sdk", wasm_native_mod); + docs_wasm_preview_mod.strip = true; + const docs_wasm_preview_exe = b.addExecutable(.{ + .name = "component-preview", + .root_module = docs_wasm_preview_mod, + }); + docs_wasm_preview_exe.entry = .disabled; + docs_wasm_preview_exe.rdynamic = true; + // The engine trades heap for fixed capacity but still builds some + // sizable stack temporaries (NullPlatform alone is ~800 KB); the + // 1 MB wasm default overflows into linear memory silently. + docs_wasm_preview_exe.stack_size = 16 * 1024 * 1024; + const copy_docs_wasm_preview = b.addUpdateSourceFiles(); + copy_docs_wasm_preview.addCopyFileToSource(docs_wasm_preview_exe.getEmittedBin(), "docs/public/wasm/component-preview.wasm"); + const docs_wasm_preview_step = b.step("docs-wasm-preview", "Compile the live component-preview wasm module into docs/public/wasm/"); + docs_wasm_preview_step.dependOn(©_docs_wasm_preview.step); + const file_contains_checker_mod = module(b, host_target, optimize, "tools/check_file_contains.zig"); const file_contains_checker = b.addExecutable(.{ .name = "check-file-contains", .root_module = file_contains_checker_mod, }); + // Registry pin printer: emits the ui_schema table counts and + // fingerprints in the exact form ui_schema_tests.zig pins, plus the + // next free code per table for reserving codes ahead of parallel + // work. `zig build print-pins` after registry additions. + const print_pins_mod = module(b, host_target, optimize, "tools/print_pins.zig"); + print_pins_mod.addImport("ui_schema", module(b, host_target, optimize, "src/primitives/canvas/ui_schema.zig")); + const print_pins_exe = b.addExecutable(.{ + .name = "print-pins", + .root_module = print_pins_mod, + }); + const run_print_pins = b.addRunArtifact(print_pins_exe); + run_print_pins.has_side_effects = true; + const print_pins_step = b.step("print-pins", "Print registry counts and fingerprints in pin-ready form"); + print_pins_step.dependOn(&run_print_pins.step); + const platform_arg = switch (selected_platform) { .auto => unreachable, .null => "null", @@ -207,45 +393,52 @@ pub fn build(b: *std.Build) void { test_step.dependOn(&b.addRunArtifact(diagnostics_tests).step); test_step.dependOn(&b.addRunArtifact(platform_info_tests).step); test_step.dependOn(&b.addRunArtifact(json_tests).step); - test_step.dependOn(&b.addRunArtifact(desktop_tests).step); + test_step.dependOn(&b.addRunArtifact(canvas_tests).step); + for (desktop_test_shards) |shard_tests| { + test_step.dependOn(&b.addRunArtifact(shard_tests).step); + } test_step.dependOn(&b.addRunArtifact(automation_protocol_tests).step); test_step.dependOn(&b.addRunArtifact(tooling_tests).step); + test_step.dependOn(&b.addRunArtifact(eject_components_tests).step); + test_step.dependOn(&b.addRunArtifact(markup_lsp_tests).step); + test_step.dependOn(&b.addRunArtifact(automation_cli_tests).step); addFileContainsCheckStep(b, file_contains_checker, test_step, "test-package-types", "Verify package TypeScript platform feature names", &.{ - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "ZeroNativeCommandInfo" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "list(): Promise" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "ZeroNativeCreateWebViewViewOptions" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "Stable runtime view id" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "update(label: string" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "focus(options: string | ZeroNativeViewSelector)" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "close(options: string | ZeroNativeViewSelector)" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "kind: \"webview\"" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "url: string" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "ZeroNativePlatformFeatureSelector" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "supports(value: ZeroNativePlatformFeature | ZeroNativePlatformFeatureSelector)" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "\"native_control_commands\"" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "\"nativeControlCommands\"" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "\"recent_documents\"" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "\"recentDocuments\"" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "\"file_drops\"" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "\"fileDrops\"" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "\"app_activation_events\"" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "\"appActivationEvents\"" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "\"gpu_surfaces\"" }, - .{ .path = "packages/zero-native/zero-native.d.ts", .pattern = "\"gpuSurfaces\"" }, + .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "NativeSdkCommandInfo" }, + .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "list(): Promise" }, + .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "NativeSdkCreateWebViewViewOptions" }, + .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "Stable runtime view id" }, + .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "update(label: string" }, + .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "focus(options: string | NativeSdkViewSelector)" }, + .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "close(options: string | NativeSdkViewSelector)" }, + .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "kind: \"webview\"" }, + .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "url: string" }, + .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "NativeSdkPlatformFeatureSelector" }, + .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "supports(value: NativeSdkPlatformFeature | NativeSdkPlatformFeatureSelector)" }, + .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "\"native_control_commands\"" }, + .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "\"nativeControlCommands\"" }, + .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "\"recent_documents\"" }, + .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "\"recentDocuments\"" }, + .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "\"file_drops\"" }, + .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "\"fileDrops\"" }, + .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "\"app_activation_events\"" }, + .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "\"appActivationEvents\"" }, + .{ .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" }, }); addFileContainsCheckStep(b, file_contains_checker, test_step, "test-bridge-view-selector-helpers", "Verify injected view helpers accept string selectors", &.{ .{ .path = "src/platform/macos/appkit_host.m", .pattern = "viewSelectorPayload(options)" }, .{ .path = "src/platform/macos/cef_host.mm", .pattern = "viewSelectorPayload(options)" }, .{ .path = "src/platform/linux/gtk_host.c", .pattern = "viewSelectorPayload(options)" }, .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "viewSelectorPayload(options)" }, - .{ .path = "src/platform/macos/appkit_host.m", .pattern = "focus:function(options){return invoke('zero-native.view.focus',viewSelectorPayload(options))" }, - .{ .path = "src/platform/macos/cef_host.mm", .pattern = "focus:function(options){return invoke('zero-native.view.focus',viewSelectorPayload(options))" }, - .{ .path = "src/platform/linux/gtk_host.c", .pattern = "focus:function(options){return invoke('zero-native.view.focus',viewSelectorPayload(options))" }, - .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "focus:function(options){return invoke('zero-native.view.focus',viewSelectorPayload(options))" }, - .{ .path = "src/platform/macos/appkit_host.m", .pattern = "close:function(options){return invoke('zero-native.view.close',viewSelectorPayload(options))" }, - .{ .path = "src/platform/macos/cef_host.mm", .pattern = "close:function(options){return invoke('zero-native.view.close',viewSelectorPayload(options))" }, - .{ .path = "src/platform/linux/gtk_host.c", .pattern = "close:function(options){return invoke('zero-native.view.close',viewSelectorPayload(options))" }, - .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "close:function(options){return invoke('zero-native.view.close',viewSelectorPayload(options))" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "focus:function(options){return invoke('native-sdk.view.focus',viewSelectorPayload(options))" }, + .{ .path = "src/platform/macos/cef_host.mm", .pattern = "focus:function(options){return invoke('native-sdk.view.focus',viewSelectorPayload(options))" }, + .{ .path = "src/platform/linux/gtk_host.c", .pattern = "focus:function(options){return invoke('native-sdk.view.focus',viewSelectorPayload(options))" }, + .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "focus:function(options){return invoke('native-sdk.view.focus',viewSelectorPayload(options))" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "close:function(options){return invoke('native-sdk.view.close',viewSelectorPayload(options))" }, + .{ .path = "src/platform/macos/cef_host.mm", .pattern = "close:function(options){return invoke('native-sdk.view.close',viewSelectorPayload(options))" }, + .{ .path = "src/platform/linux/gtk_host.c", .pattern = "close:function(options){return invoke('native-sdk.view.close',viewSelectorPayload(options))" }, + .{ .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\"" }, @@ -254,6 +447,7 @@ pub fn build(b: *std.Build) void { 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" }, }); 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" }, @@ -264,10 +458,34 @@ pub fn build(b: *std.Build) void { .{ .path = "src/platform/linux/gtk_host.c", .pattern = "update:function(options,patch)" }, .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "update:function(options,patch)" }, }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-linux-gpu-frame-emission-below-paint", "Verify the Linux GPU frame emission is scheduled below layout and paint priorities", &.{ + // A saturated demand-driven frame loop (cycle cost > frame + // interval => every present re-arms at delay 0) scheduled at + // G_PRIORITY_DEFAULT would be ready on every main-loop + // iteration and starve GTK's layout/paint sources: presents + // land and queue_draw keeps inviting a repaint, but the glass + // freezes on stale pixels for as long as the loop stays armed. + // The emission must stay below GDK_PRIORITY_REDRAW so every + // presented frame paints before the next frame event. + .{ .path = "src/platform/linux/gtk_host.c", .pattern = "view->gpu_emit_source = g_timeout_add_full(G_PRIORITY_DEFAULT_IDLE," }, + .{ .path = "src/platform/linux/gtk_host.c", .pattern = "paint (GDK_PRIORITY_REDRAW," }, + .{ .path = "src/platform/linux/gtk_host.c", .pattern = "static void native_sdk_gpu_surface_schedule_frame_emission" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-linux-audio-buffering-clears-on-noop-resume", "Verify the Linux audio buffering flag drops when the 100% resume completes synchronously", &.{ + // The buffering flag normally drops at the PLAYING + // state-changed message. When the refill's earlier PAUSED + // request never completed, the 100% resume is a no-op that + // posts no message; a non-ASYNC set_state result is the only + // signal, and the flag must drop on it or it rides every + // position tick for the rest of the track. + .{ .path = "src/platform/linux/gtk_host.c", .pattern = "const int change = gst->element_set_state(audio->playbin, NATIVE_SDK_GST_STATE_PLAYING);" }, + .{ .path = "src/platform/linux/gtk_host.c", .pattern = "change != NATIVE_SDK_GST_STATE_CHANGE_ASYNC" }, + .{ .path = "src/platform/linux/gtk_host.c", .pattern = "#define NATIVE_SDK_GST_STATE_CHANGE_ASYNC 2" }, + }); addFileContainsCheckStep(b, file_contains_checker, test_step, "test-windows-packaged-assets-webview2", "Verify Windows packaged assets are served through WebView2 request interception", &.{ - .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "constexpr const char *kAssetVirtualOrigin = \"https://zero-native-app.localhost\";" }, + .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "constexpr const char *kAssetVirtualOrigin = \"https://native-sdk-app.localhost\";" }, .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "return virtualAssetEntryUrl(webview.asset_entry);" }, - .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "AddWebResourceRequestedFilter(L\"https://zero-native-app.localhost/*\"" }, + .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "AddWebResourceRequestedFilter(L\"https://native-sdk-app.localhost/*\"" }, .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "assetWebResourceResponse(environment_ref.Get(), found->second, uri)" }, .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "bridgeOriginForWebViewUrl(source_webview->second, source_url)" }, .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "webview.spa_fallback = spa_fallback != 0;" }, @@ -280,14 +498,213 @@ pub fn build(b: *std.Build) void { .{ .path = "src/platform/macos/cef_host.mm", .pattern = "bridgeOriginForWindowId:window_id_ webViewLabel:labelString sourceURL:sourceURLString" }, }); addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-native-accessibility-roles", "Verify AppKit native views publish accessibility roles", &.{ - .{ .path = "src/platform/macos/appkit_host.m", .pattern = "ZeroNativeAccessibilityRoleForNativeViewKind" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkAccessibilityRoleForNativeViewKind" }, .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NSAccessibilityToolbarRole" }, .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NSAccessibilityProgressIndicatorRole" }, - .{ .path = "src/platform/macos/appkit_host.m", .pattern = "view.accessibilityRole = ZeroNativeAccessibilityRoleForNativeViewKind(kind)" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "view.accessibilityRole = NativeSdkAccessibilityRoleForNativeViewKind(kind)" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-input-repaints-retained-canvas", "Verify GPU input wakes retained canvas frames", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "- (void)requestRetainedCanvasFrame" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "[self requestRetainedCanvasFrame];" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-input-paces-retained-canvas", "Verify GPU input frame requests are paced to the display interval", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkRetainedFrameIntervalNanoseconds" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "retainedFrameLastEmitNs" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "queuePointerMotionInputEvent:(NSEvent *)event" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "pendingPointerMotionKind = kind" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "emitQueuedPointerMotionInputEvent" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "queueScrollInputEvent:(NSEvent *)event" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "pendingScrollDeltaY += deltaY" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "emitQueuedScrollInputEvent" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "dispatch_after(dispatch_time(DISPATCH_TIME_NOW" }, + // The single per-surface frame-event scheduler: every producer + // (requests, completions, occluded completions) coalesces into + // one paced emission per display interval. + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "- (void)scheduleFrameEventEmission" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "- (void)emitScheduledFrameEvent" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-gpu-occluded-frame-heartbeat", "Verify occluded windows throttle frame completions to a heartbeat and restore full cadence on reveal", &.{ + // macOS: occluded surfaces pace logical completions on the ~1 Hz + // heartbeat (never stopping — on_frame-driven models stay gently + // current), de-occlusion supersedes the parked emission for an + // immediate return to the display grid, and heartbeat completions + // are flagged so the runtime never stamps input latency from them. + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkOccludedFrameHeartbeatNs = 1000000000ull" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "- (BOOL)occludedFramePacingActive" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "heartbeatPaced ? NativeSdkOccludedFrameHeartbeatNs" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "self.frameEventEmissionGeneration += 1" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "occluded:[self occludedFramePacingActive]" }, + // Exempt producers (a real present's completion, an input's + // responding frame) fire at grid promptness — neither can + // sustain a spin. + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "scheduleFrameEventEmissionForPresentCompletion:YES" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "- (void)noteGpuSurfaceInputActivity" }, + // Windows: the same throttle keyed on the one reliable Win32 + // occlusion signal (minimize); restore re-arms the pending + // one-shot timer at the frame-grid delay, and the input / + // first-present exemptions ride one prompt-frame flag. + .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "kGpuOccludedHeartbeatNs = 1000000000ull" }, + .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "gpuSurfaceOccludedPacingActive" }, + .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "IsIconic(root)" }, + .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "gpu_prompt_frame_pending" }, + .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "native_sdk_windows_note_gpu_surface_input" }, + // The runtime side of the measurement honesty: occluded logical + // completions resolve pending inputs without a latency stamp, + // and every dispatched input notes the host for a prompt + // responding frame. + .{ .path = "src/runtime/gpu_surface_events.zig", .pattern = "resolveGpuSurfaceInputForOccludedFrame" }, + .{ .path = "src/runtime/gpu_surface_events.zig", .pattern = "noteGpuSurfaceInput" }, + // GTK: no reliable cross-backend occlusion signal — the decision + // to leave full cadence is documented at the scheduler, not + // implicit. + .{ .path = "src/platform/linux/gtk_host.c", .pattern = "No occluded/minimized throttle here, DELIBERATELY" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-drawable-integral-pixels", "Verify AppKit GPU surfaces use integral drawable pixels", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "ceil(size.width * scale)" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "ceil(size.height * scale)" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "self.metalLayer.drawableSize = drawableSize" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-resize-repaints-retained-canvas", "Verify AppKit GPU resize requests a correctly sized retained frame", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "_metalLayer.contentsGravity = kCAGravityTopLeft" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "if (changed) {" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "[self requestRetainedCanvasFrame];" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "canvasTextureMatchesDrawable" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-packet-transforms", "Verify AppKit GPU packet presenter applies command transforms", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkPacketApplyTransform(command[@\"transform\"])" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "[affine setTransformStruct:transform]" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "[affine concat]" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-packet-paths", "Verify AppKit GPU packet presenter draws path commands", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "[kind isEqualToString:@\"path\"]" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "[verb isEqualToString:@\"quad_to\"]" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "[kind isEqualToString:@\"fill_path\"]" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "[kind isEqualToString:@\"stroke_path\"]" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-packet-corner-radii", "Verify AppKit GPU packet presenter honors per-corner radii", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkPacketRoundedRectPath" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "CGFloat topRight = NativeSdkPacketRadiusAt(radiusValue, 1, maxRadius)" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "CGFloat bottomLeft = NativeSdkPacketRadiusAt(radiusValue, 3, maxRadius)" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "return NativeSdkPacketRoundedRectPath(rect, shape[@\"radius\"])" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-packet-load-frames", "Verify AppKit GPU packet presenter handles retained load frames", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "canvasPacketPixels" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "[loadAction isEqualToString:@\"load\"]" }, + // Retained-backing discipline: dirty updates require the validity + // flag, and every present that mutates the backing clears it until + // the draw succeeds — a failed draw can never leak stale pixels + // around a later scissor. + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "!self.canvasPacketPixelsValid" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "self.canvasPacketPixelsValid = YES" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "hasDirtyRect:uploadDirtyRect" }, + }); + // Version prose is machine-checked: the wire-format v2/v3 drift (spec + // comments and changelog naming a version the constant had moved past) + // shipped once, so the constant and every prose site that names the + // version are pinned together. Bumping `binary_packet_version` fails + // 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 = 3;" }, + .{ .path = "src/primitives/canvas/serialization.zig", .pattern = "Compact binary gpu-surface packet encoding (wire format v3)." }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "Compact binary gpu-surface packet decoding (wire format v3)." }, + }); + 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" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "CGBitmapContextGetData(context)" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NSIntersectionRect(rect, clipRect)" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkPacketTransformRect(transformValue, NativeSdkPacketRect(effect[@\"rect\"]))" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "return NativeSdkPacketApplyBlur(effect, opacity, context, scale, transformValue, hasClip, clipRect)" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-packet-text-layout", "Verify AppKit GPU packet presenter honors text layout metadata", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NSMutableParagraphStyle" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkPacketTextLineBreakMode" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkPacketTextAlignment" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkPacketNumber(layout[@\"maxWidth\"], 0)" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "[value drawWithRect:NSMakeRect(origin.x, origin.y - size, textWidth, textHeight)" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-packet-font-assets", "Verify AppKit GPU packet text registers bundled font assets", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "#import " }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "CTFontManagerRegisterFontsForURL" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "@[ @\"fonts\", @\"Fonts\", @\"assets/fonts\" ]" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkRegisterBundledFonts();" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkPacketPreferredFont(text, size)" }, + .{ .path = "src/tooling/templates.zig", .pattern = "app_mod.linkFramework(\"CoreText\", .{});" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-packet-span-fonts", "Verify AppKit packet text resolves reserved span font ids to real weighted and italic faces", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "static NSFont *NativeSdkItalicSansFont(NSFont *font)" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "static NSFont *NativeSdkWeightedSansFont(" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkWeightedSansFont(@[ @\"Geist-Medium\", @\"Geist Medium\" ], base, NSFontWeightMedium, NO, size)" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkWeightedSansFont(@[ @\"Geist-Bold\", @\"Geist Bold\" ], base, NSFontWeightBold, YES, size)" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkItalicSansFont(base)" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkItalicSansFont(NativeSdkWeightedSansFont(@[ @\"Geist-Bold\", @\"Geist Bold\" ], base, NSFontWeightBold, YES, size))" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-widget-cursor-bridge", "Verify AppKit GPU widgets apply retained cursor intent", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "native_sdk_appkit_set_view_cursor" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "resetCursorRects" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NSTrackingMouseMoved" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NATIVE_SDK_APPKIT_GPU_INPUT_POINTER_CANCEL" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "_surfaceCursor = cursor ?: [NSCursor arrowCursor]" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NSCursor pointingHandCursor" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-widget-accessibility-actions", "Verify AppKit GPU widget accessibility actions route to the runtime", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "accessibilityPerformPress" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "emitWidgetAccessibilityActionWithId" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NATIVE_SDK_APPKIT_EVENT_WIDGET_ACCESSIBILITY_ACTION" }, + // The per-element action gate: without it AppKit derives the + // advertised action list from the CLASS's selectors, so every + // widget element (a static label included) offers + // press/increment/decrement/cancel and performing an + // unsupported one reports success while actuating nothing. + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "isAccessibilitySelectorAllowed" }, + // An assistive client's AXFocused WRITE must move the app's + // real focus, not just flip a flag on the snapshot element. + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "setAccessibilityFocused" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-widget-accessibility-text-ranges", "Verify AppKit GPU widget accessibility publishes text selection ranges", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "accessibilitySelectedTextRange" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "accessibilitySelectedTextRanges" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "accessibilityVisibleCharacterRange" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-widget-accessibility-grid-metrics", "Verify AppKit GPU widget accessibility publishes grid and scroll metrics", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "accessibilityRowIndexRange" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "accessibilityColumnIndexRange" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "accessibilityRowCount" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "accessibilityMaxValue" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-widget-ime-bridge", "Verify AppKit GPU widgets route native text input and IME composition", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NSTextInputClient" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "insertText:(id)string replacementRange" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "setMarkedText:(id)string selectedRange" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NATIVE_SDK_APPKIT_GPU_INPUT_IME_SET_COMPOSITION" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-windows-gpu-widget-ime-bridge", "Verify the Windows GPU host routes WM_IME composition onto the shared IME event kinds", &.{ + .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "case WM_IME_STARTCOMPOSITION:" }, + .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "case WM_IME_COMPOSITION:" }, + .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "case WM_IME_ENDCOMPOSITION:" }, + .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "kGpuInputImeSetComposition = 8" }, + .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "kGpuInputImeCommitComposition = 9" }, + .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "kGpuInputImeCancelComposition = 10" }, + .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "gpuImeCommitAction(pending, result)" }, + .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "ISC_SHOWUICOMPOSITIONWINDOW" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-gpu-widget-text-command-bridge", "Verify AppKit GPU text widgets route native text commands", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "- (void)selectAll:(id)sender" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "@selector(selectAll:)" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "emitSyntheticKeyDownWithKey:@\"a\" modifiers:(NativeSdkShortcutModifierPrimary | NativeSdkShortcutModifierCommand)" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-appearance-bridge", "Verify AppKit reports system light and dark appearance changes", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "effectiveAppearance" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "accessibilityDisplayShouldReduceMotion" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "accessibilityDisplayShouldIncreaseContrast" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NATIVE_SDK_APPKIT_EVENT_APPEARANCE_CHANGED" }, + .{ .path = "src/platform/macos/root.zig", .pattern = ".reduce_motion = event.reduce_motion != 0" }, + .{ .path = "src/platform/macos/root.zig", .pattern = ".high_contrast = event.high_contrast != 0" }, + .{ .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 = \"zero-native.dialog.saveFile\"" }, - .{ .path = "docs/src/app/bridge/builtin-commands/page.mdx", .pattern = ".{ .name = \"zero-native.dialog.saveFile\"" }, + .{ .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\"" }, }); addTestStep(b, "test-geometry", "Run geometry module tests", geometry_tests); @@ -298,24 +715,30 @@ pub fn build(b: *std.Build) void { addTestStep(b, "test-diagnostics", "Run diagnostics module tests", diagnostics_tests); addTestStep(b, "test-platform-info", "Run platform info module tests", platform_info_tests); addTestStep(b, "test-json", "Run JSON primitive tests", json_tests); - addTestStep(b, "test-desktop", "Run zero-native framework tests", desktop_tests); + addTestStep(b, "test-canvas", "Run canvas display list tests", canvas_tests); + addTestStep(b, "test-desktop", "Run Native SDK framework tests", desktop_tests); + for (desktop_test_shard_specs, desktop_test_shards) |spec, shard_tests| { + addTestStep(b, b.fmt("test-desktop-{s}", .{spec.name}), spec.description, shard_tests); + } addTestStep(b, "test-automation-protocol", "Run automation protocol tests", automation_protocol_tests); - addTestStep(b, "test-tooling", "Run zero-native tooling tests", tooling_tests); + addTestStep(b, "test-automation-cli", "Run native automate CLI tests", automation_cli_tests); + addTestStep(b, "test-tooling", "Run Native SDK tooling tests", tooling_tests); + addTestStep(b, "test-eject-components", "Run ejected-component widget-identity tests", eject_components_tests); const run_hello = b.addSystemCommand(&.{ "zig", "build", "run", b.fmt("-Dplatform={s}", .{platform_arg}), b.fmt("-Dtrace={s}", .{@tagName(trace_option)}) }); run_hello.setCwd(b.path("examples/hello")); - const run_hello_step = b.step("run-hello", "Run the zero-native hello WebView example"); + const run_hello_step = b.step("run-hello", "Run the native-sdk hello WebView example"); run_hello_step.dependOn(&run_hello.step); - const run_webview = b.addSystemCommand(&.{ "zig", "build", "run", b.fmt("-Dplatform={s}", .{platform_arg}), b.fmt("-Dtrace={s}", .{@tagName(trace_option)}), b.fmt("-Dweb-engine={s}", .{@tagName(web_engine)}), b.fmt("-Dcef-dir={s}", .{cef_dir}) }); + const run_webview = b.addSystemCommand(&.{ "zig", "build", "run", b.fmt("-Dplatform={s}", .{platform_arg}), b.fmt("-Dtrace={s}", .{@tagName(trace_option)}), b.fmt("-Dweb-engine={s}", .{@tagName(web_engine)}), b.fmt("-Dcef-dir={s}", .{exampleCefDir(b, cef_dir)}) }); run_webview.setCwd(b.path("examples/webview")); - const run_webview_step = b.step("run-webview", "Run the zero-native WebView example"); + const run_webview_step = b.step("run-webview", "Run the native-sdk WebView example"); run_webview_step.dependOn(&run_webview.step); const browser_cef_dir = cef_dir_override orelse defaultCefDir(selected_platform, "third_party/cef/macos"); - const run_browser = b.addSystemCommand(&.{ "zig", "build", "run", b.fmt("-Dplatform={s}", .{platform_arg}), b.fmt("-Dtrace={s}", .{@tagName(trace_option)}), b.fmt("-Dweb-engine={s}", .{@tagName(browser_web_engine)}), b.fmt("-Dcef-dir={s}", .{browser_cef_dir}) }); + const run_browser = b.addSystemCommand(&.{ "zig", "build", "run", b.fmt("-Dplatform={s}", .{platform_arg}), b.fmt("-Dtrace={s}", .{@tagName(trace_option)}), b.fmt("-Dweb-engine={s}", .{@tagName(browser_web_engine)}), b.fmt("-Dcef-dir={s}", .{exampleCefDir(b, browser_cef_dir)}) }); run_browser.setCwd(b.path("examples/browser")); - const run_browser_step = b.step("run-browser", "Run the zero-native browser example"); + const run_browser_step = b.step("run-browser", "Run the native-sdk browser example"); run_browser_step.dependOn(&run_browser.step); const build_webview_system = b.addSystemCommand(&.{ "zig", "build", b.fmt("-Dplatform={s}", .{platform_arg}), "-Dweb-engine=system" }); @@ -329,10 +752,10 @@ pub fn build(b: *std.Build) void { browser_system_link_step.dependOn(&build_browser_system.step); const frontend_examples_step = b.step("test-examples-frontends", "Run frontend example tests"); - addExampleTestStep(b, frontend_examples_step, "test-example-next", "Run Next example tests", "examples/next"); - addExampleTestStep(b, frontend_examples_step, "test-example-react", "Run React example tests", "examples/react"); - addExampleTestStep(b, frontend_examples_step, "test-example-svelte", "Run Svelte example tests", "examples/svelte"); - addExampleTestStep(b, frontend_examples_step, "test-example-vue", "Run Vue example tests", "examples/vue"); + addExampleTestStep(b, host_cli_exe, frontend_examples_step, "test-example-next", "Run Next example tests", "examples/next", .owned); + addExampleTestStep(b, host_cli_exe, frontend_examples_step, "test-example-react", "Run React example tests", "examples/react", .owned); + addExampleTestStep(b, host_cli_exe, frontend_examples_step, "test-example-svelte", "Run Svelte example tests", "examples/svelte", .owned); + addExampleTestStep(b, host_cli_exe, frontend_examples_step, "test-example-vue", "Run Vue example tests", "examples/vue", .owned); addFileContainsCheckStep(b, file_contains_checker, frontend_examples_step, "test-example-frontend-positioning", "Verify frontend example native shell positioning", &.{ .{ .path = "examples/next/README.md", .pattern = "opens the native app shell with WebView content." }, .{ .path = "examples/react/README.md", .pattern = "opens the native app shell with WebView content." }, @@ -341,23 +764,40 @@ pub fn build(b: *std.Build) void { }); const native_examples_step = b.step("test-examples-native", "Run native-first example tests"); - addExampleTestStep(b, native_examples_step, "test-example-command-app", "Run command app example tests", "examples/command-app"); - addExampleTestStep(b, native_examples_step, "test-example-native-shell", "Run native shell example tests", "examples/native-shell"); - addExampleTestStep(b, native_examples_step, "test-example-native-panels", "Run native panels example tests", "examples/native-panels"); - addExampleTestStep(b, native_examples_step, "test-example-capabilities", "Run capabilities example tests", "examples/capabilities"); + addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-command-app", "Run command app example tests", "examples/command-app", .owned); + addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-native-shell", "Run native shell example tests", "examples/native-shell", .owned); + addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-native-panels", "Run native panels example tests", "examples/native-panels", .owned); + addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-gpu-surface", "Run GPU surface example tests", "examples/gpu-surface", .managed); + addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-gpu-dashboard", "Run GPU dashboard example tests", "examples/gpu-dashboard", .managed); + addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-gpu-components", "Run GPU components example tests", "examples/gpu-components", .managed); + addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-ui-inbox", "Run ui builder inbox example tests", "examples/ui-inbox", .owned); + addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-kanban", "Run ui builder kanban example tests", "examples/kanban", .managed); + addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-habits", "Run markup habits example tests", "examples/habits", .managed); + addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-soundboard", "Run soundboard example tests", "examples/soundboard", .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-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); + addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-system-monitor", "Run system monitor example tests", "examples/system-monitor", .managed); + addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-effects-probe", "Run effects probe example tests", "examples/effects-probe", .managed); + addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-feed", "Run feed example tests", "examples/feed", .managed); + addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-canvas-preview", "Run canvas preview example tests", "examples/canvas-preview", .managed); + addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-capabilities", "Run capabilities example tests", "examples/capabilities", .owned); addFileContainsCheckStep(b, file_contains_checker, native_examples_step, "test-example-capabilities-events", "Verify capabilities example event bridge names", &.{ - .{ .path = "examples/capabilities/src/main.zig", .pattern = "zero-native:drop:files" }, + .{ .path = "examples/capabilities/src/main.zig", .pattern = "native-sdk:drop:files" }, }); const mobile_examples_step = b.step("test-examples-mobile", "Verify mobile example project layouts"); addLayoutCheckStep(b, mobile_examples_step, "test-example-ios-layout", "Verify iOS example layout", &.{ "examples/ios/README.md", "examples/ios/app.zon", - "examples/ios/ZeroNativeIOSExample.xcodeproj/project.pbxproj", - "examples/ios/ZeroNativeIOSExample/AppDelegate.swift", - "examples/ios/ZeroNativeIOSExample/SceneDelegate.swift", - "examples/ios/ZeroNativeIOSExample/ZeroNativeHostViewController.swift", - "examples/ios/ZeroNativeIOSExample/zero_native.h", + "examples/ios/NativeSdkIOSExample.xcodeproj/project.pbxproj", + "examples/ios/NativeSdkIOSExample/AppDelegate.swift", + "examples/ios/NativeSdkIOSExample/SceneDelegate.swift", + "examples/ios/NativeSdkIOSExample/NativeSdkHostViewController.swift", + "examples/ios/NativeSdkIOSExample/native_sdk.h", + "examples/ios/NativeSdkIOSExample/NativeSdkDyldShim.c", }); addLayoutCheckStep(b, mobile_examples_step, "test-example-android-layout", "Verify Android example layout", &.{ "examples/android/README.md", @@ -366,15 +806,108 @@ pub fn build(b: *std.Build) void { "examples/android/build.gradle", "examples/android/app/build.gradle", "examples/android/app/src/main/AndroidManifest.xml", - "examples/android/app/src/main/java/dev/zero_native/examples/android/MainActivity.kt", + "examples/android/app/src/main/java/dev/native_sdk/examples/android/MainActivity.kt", "examples/android/app/src/main/cpp/CMakeLists.txt", - "examples/android/app/src/main/cpp/zero_native_jni.c", - "examples/android/app/src/main/cpp/zero_native.h", + "examples/android/app/src/main/cpp/native_sdk_jni.c", + "examples/android/app/src/main/cpp/native_sdk.h", }); addLayoutCheckStep(b, mobile_examples_step, "test-example-mobile-shell-layout", "Verify shared mobile-shell metadata", &.{ "examples/mobile-shell/README.md", "examples/mobile-shell/app.zon", }); + + // Host-tier packaged project pin: `native package --target ios` must + // keep emitting the complete, deterministic Xcode project (toolkit + // host sources, Info.plist, asset catalog, scheme) — drift is caught + // here. --binary stubs the embed library with the fixed-shell lib so + // the check needs no iOS cross-compile. + const package_ios_layout_run = b.addRunArtifact(host_cli_exe); + package_ios_layout_run.setCwd(b.path("examples/calculator")); + package_ios_layout_run.setEnvironmentVariable("NATIVE_SDK_PATH", b.pathFromRoot(".")); + package_ios_layout_run.addArgs(&.{ "package", "--target", "ios", "--output", "zig-out/package/test-ios-layout", "--binary" }); + package_ios_layout_run.addFileArg(embed_lib.getEmittedBin()); + package_ios_layout_run.has_side_effects = true; + const package_ios_layout_step = b.step("test-package-ios-layout", "Verify the generated iOS host project layout"); + const package_ios_layout_paths = [_][]const u8{ + "examples/calculator/zig-out/package/test-ios-layout/calculator.xcodeproj/project.pbxproj", + "examples/calculator/zig-out/package/test-ios-layout/calculator.xcodeproj/xcshareddata/xcschemes/calculator.xcscheme", + "examples/calculator/zig-out/package/test-ios-layout/Host/uikit_host.m", + "examples/calculator/zig-out/package/test-ios-layout/Host/native_sdk_app.h", + "examples/calculator/zig-out/package/test-ios-layout/Host/Info.plist", + "examples/calculator/zig-out/package/test-ios-layout/Assets.xcassets/AppIcon.appiconset/AppIcon.png", + "examples/calculator/zig-out/package/test-ios-layout/Assets.xcassets/AppIcon.appiconset/Contents.json", + "examples/calculator/zig-out/package/test-ios-layout/Libraries/libnative-sdk.a", + "examples/calculator/zig-out/package/test-ios-layout/README.md", + "examples/calculator/zig-out/package/test-ios-layout/package-manifest.zon", + }; + for (package_ios_layout_paths) |path| { + const check = b.addSystemCommand(&.{ "test", "-f", path }); + check.step.dependOn(&package_ios_layout_run.step); + package_ios_layout_step.dependOn(&check.step); + mobile_examples_step.dependOn(&check.step); + } + const package_ios_layout_contents = [_]FileContainsCheck{ + .{ .path = "examples/calculator/zig-out/package/test-ios-layout/calculator.xcodeproj/project.pbxproj", .pattern = "PRODUCT_BUNDLE_IDENTIFIER = \"dev.native-sdk.calculator\";" }, + .{ .path = "examples/calculator/zig-out/package/test-ios-layout/Host/Info.plist", .pattern = "UILaunchScreen" }, + .{ .path = "examples/calculator/zig-out/package/test-ios-layout/Host/uikit_host.m", .pattern = "native_sdk_app_render_pixels" }, + }; + for (package_ios_layout_contents) |check_value| { + const check = b.addRunArtifact(file_contains_checker); + check.setCwd(b.path(".")); + check.addArg(check_value.path); + check.addArg(check_value.pattern); + check.step.dependOn(&package_ios_layout_run.step); + package_ios_layout_step.dependOn(&check.step); + mobile_examples_step.dependOn(&check.step); + } + // Host-tier packaged project pin, Android: `native package --target + // android` must keep emitting the complete generated host project + // (toolkit host sources, app.zon-derived manifest, launcher icons) — + // drift is caught here. --binary stubs the embed library with the + // fixed-shell lib so the check needs no Android cross-compile, and + // ANDROID_HOME points at a nonexistent SDK so the deterministic + // project-only path runs everywhere (the APK assembly is exercised + // by the live loops, not CI). + const package_android_layout_run = b.addRunArtifact(host_cli_exe); + package_android_layout_run.setCwd(b.path("examples/calculator")); + package_android_layout_run.setEnvironmentVariable("NATIVE_SDK_PATH", b.pathFromRoot(".")); + package_android_layout_run.setEnvironmentVariable("ANDROID_HOME", b.pathFromRoot("zig-out/no-android-sdk")); + package_android_layout_run.addArgs(&.{ "package", "--target", "android", "--output", "zig-out/package/test-android-layout", "--binary" }); + package_android_layout_run.addFileArg(embed_lib.getEmittedBin()); + package_android_layout_run.has_side_effects = true; + const package_android_layout_step = b.step("test-package-android-layout", "Verify the generated Android host project layout"); + const package_android_layout_paths = [_][]const u8{ + "examples/calculator/zig-out/package/test-android-layout/AndroidManifest.xml", + "examples/calculator/zig-out/package/test-android-layout/Host/NativeSdkActivity.java", + "examples/calculator/zig-out/package/test-android-layout/Host/android_host.c", + "examples/calculator/zig-out/package/test-android-layout/Host/native_sdk_app.h", + "examples/calculator/zig-out/package/test-android-layout/res/mipmap-mdpi/ic_launcher.png", + "examples/calculator/zig-out/package/test-android-layout/res/mipmap-xxxhdpi/ic_launcher.png", + "examples/calculator/zig-out/package/test-android-layout/Libraries/libnative-sdk.a", + "examples/calculator/zig-out/package/test-android-layout/README.md", + "examples/calculator/zig-out/package/test-android-layout/package-manifest.zon", + }; + for (package_android_layout_paths) |path| { + const check = b.addSystemCommand(&.{ "test", "-f", path }); + check.step.dependOn(&package_android_layout_run.step); + package_android_layout_step.dependOn(&check.step); + mobile_examples_step.dependOn(&check.step); + } + const package_android_layout_contents = [_]FileContainsCheck{ + .{ .path = "examples/calculator/zig-out/package/test-android-layout/AndroidManifest.xml", .pattern = "package=\"dev.native_sdk.calculator\"" }, + .{ .path = "examples/calculator/zig-out/package/test-android-layout/AndroidManifest.xml", .pattern = "dev.native_sdk.host.NativeSdkActivity" }, + .{ .path = "examples/calculator/zig-out/package/test-android-layout/Host/NativeSdkActivity.java", .pattern = "InputMethodManager" }, + .{ .path = "examples/calculator/zig-out/package/test-android-layout/Host/android_host.c", .pattern = "native_sdk_app_render_pixels" }, + }; + for (package_android_layout_contents) |check_value| { + const check = b.addRunArtifact(file_contains_checker); + check.setCwd(b.path(".")); + check.addArg(check_value.path); + check.addArg(check_value.pattern); + check.step.dependOn(&package_android_layout_run.step); + package_android_layout_step.dependOn(&check.step); + mobile_examples_step.dependOn(&check.step); + } addFileContainsCheckStep(b, file_contains_checker, mobile_examples_step, "test-example-mobile-shell-metadata", "Verify shared mobile-shell metadata values", &.{ .{ .path = "examples/mobile-shell/app.zon", .pattern = ".platforms = .{ \"ios\", \"android\" }" }, .{ .path = "examples/mobile-shell/app.zon", .pattern = ".capabilities = .{ \"webview\", \"native_views\", \"native_module\" }" }, @@ -391,13 +924,54 @@ pub fn build(b: *std.Build) void { .{ .path = "examples/android/app.zon", .pattern = ".id = \"mobile.refresh\"" }, .{ .path = "examples/android/app.zon", .pattern = ".label = \"mobile-header\"" }, }); + addFileContainsCheckStep(b, file_contains_checker, mobile_examples_step, "test-example-android-widget-ime", "Verify Android retained widget IME and action bridge", &.{ + .{ .path = "examples/android/app/src/main/java/dev/native_sdk/examples/android/MainActivity.kt", .pattern = "override fun onCreateInputConnection" }, + .{ .path = "examples/android/app/src/main/java/dev/native_sdk/examples/android/MainActivity.kt", .pattern = "nativeIme(nativeApp, kind, text, cursor)" }, + .{ .path = "examples/android/app/src/main/java/dev/native_sdk/examples/android/MainActivity.kt", .pattern = "WIDGET_ACTION_KIND_SET_COMPOSITION = 7" }, + .{ .path = "examples/android/app/src/main/java/dev/native_sdk/examples/android/MainActivity.kt", .pattern = "WIDGET_ACTION_DRAG = 1 shl 8" }, + .{ .path = "examples/android/app/src/main/java/dev/native_sdk/examples/android/MainActivity.kt", .pattern = "WIDGET_ACTION_DROP_FILES = 1 shl 9" }, + }); + addFileContainsCheckStep(b, file_contains_checker, mobile_examples_step, "test-example-mobile-widget-abi", "Verify mobile examples use stable widget ABI lookups", &.{ + .{ .path = "examples/ios/NativeSdkIOSExample/native_sdk.h", .pattern = "native_sdk_viewport_state_t" }, + .{ .path = "examples/ios/NativeSdkIOSExample/native_sdk.h", .pattern = "native_sdk_app_scroll" }, + .{ .path = "examples/ios/NativeSdkIOSExample/native_sdk.h", .pattern = "native_sdk_app_set_text_measure" }, + .{ .path = "examples/android/app/src/main/cpp/native_sdk.h", .pattern = "native_sdk_app_set_text_measure" }, + .{ .path = "examples/mobile-canvas/ios/native_sdk_app.h", .pattern = "native_sdk_app_set_text_measure" }, + .{ .path = "examples/ios/NativeSdkIOSExample/NativeSdkHostViewController.swift", .pattern = "native_sdk_app_widget_semantics_by_id" }, + .{ .path = "examples/android/app/src/main/cpp/native_sdk.h", .pattern = "native_sdk_app_widget_semantics_by_id" }, + .{ .path = "examples/android/app/src/main/java/dev/native_sdk/examples/android/MainActivity.kt", .pattern = "nativeScroll(nativeApp" }, + .{ .path = "examples/android/app/src/main/java/dev/native_sdk/examples/android/MainActivity.kt", .pattern = "nativeWidgetSemanticsByIdFields" }, + .{ .path = "examples/android/app/src/main/cpp/native_sdk_jni.c", .pattern = "native_sdk_app_widget_semantics_by_id" }, + .{ .path = "examples/android/app/src/main/cpp/native_sdk_jni.c", .pattern = "native_sdk_app_scroll" }, + }); + addFileContainsCheckStep(b, file_contains_checker, mobile_examples_step, "test-example-mobile-canvas-span-fonts", "Verify the iOS embed shim measures reserved span font ids with the macOS face mapping", &.{ + .{ .path = "examples/mobile-canvas/ios/main.m", .pattern = "static UIFont *NativeSdkItalicSansFont(UIFont *font)" }, + .{ .path = "examples/mobile-canvas/ios/main.m", .pattern = "static UIFont *NativeSdkWeightedSansFont(" }, + .{ .path = "examples/mobile-canvas/ios/main.m", .pattern = "NativeSdkWeightedSansFont(@[ @\"Geist-Medium\", @\"Geist Medium\" ], UIFontWeightMedium, size)" }, + .{ .path = "examples/mobile-canvas/ios/main.m", .pattern = "NativeSdkWeightedSansFont(@[ @\"Geist-Bold\", @\"Geist Bold\" ], UIFontWeightBold, size)" }, + .{ .path = "examples/mobile-canvas/ios/main.m", .pattern = "NativeSdkItalicSansFont(NativeSdkWeightedSansFont(@[ @\"Geist-Bold\", @\"Geist Bold\" ], UIFontWeightBold, size))" }, + }); + + const build_mobile_canvas_lib = b.addSystemCommand(&.{ "zig", "build", "lib" }); + build_mobile_canvas_lib.setCwd(b.path("examples/mobile-canvas")); + const mobile_canvas_lib_step = b.step("test-example-mobile-canvas-lib", "Build the mobile-canvas embed static library through addMobileLib"); + mobile_canvas_lib_step.dependOn(&build_mobile_canvas_lib.step); + mobile_examples_step.dependOn(&build_mobile_canvas_lib.step); + + // Android cross-compile proof: pure Zig (no NDK sysroot — the static + // lib links no libc), PIC so the objects can land in the shim's .so. + const build_mobile_canvas_lib_android = b.addSystemCommand(&.{ "zig", "build", "lib", "-Dtarget=aarch64-linux-android" }); + build_mobile_canvas_lib_android.setCwd(b.path("examples/mobile-canvas")); + const mobile_canvas_lib_android_step = b.step("test-example-mobile-canvas-lib-android", "Cross-compile the mobile-canvas embed static library for aarch64-linux-android"); + mobile_canvas_lib_android_step.dependOn(&build_mobile_canvas_lib_android.step); + mobile_examples_step.dependOn(&build_mobile_canvas_lib_android.step); const examples_step = b.step("test-examples", "Run all example tests and layout checks"); examples_step.dependOn(frontend_examples_step); examples_step.dependOn(native_examples_step); examples_step.dependOn(mobile_examples_step); - const build_webview_cef = b.addSystemCommand(&.{ "zig", "build", "-Dplatform=macos", "-Dweb-engine=chromium", b.fmt("-Dcef-dir={s}", .{cef_dir}) }); + const build_webview_cef = b.addSystemCommand(&.{ "zig", "build", "-Dplatform=macos", "-Dweb-engine=chromium", b.fmt("-Dcef-dir={s}", .{exampleCefDir(b, cef_dir)}) }); build_webview_cef.setCwd(b.path("examples/webview")); const webview_cef_link_step = b.step("test-webview-cef-link", "Build the WebView example with Chromium/CEF"); webview_cef_link_step.dependOn(&build_webview_cef.step); @@ -413,40 +987,44 @@ pub fn build(b: *std.Build) void { \\cli="$1" \\case "$cli" in /*) ;; *) cli="../../$cli" ;; esac \\request='{"id":"smoke","command":"native.ping","payload":{"source":"smoke"}}' - \\response_file=".zig-cache/zero-native-automation/bridge-response.txt" - \\mkdir -p .zig-cache/zero-native-automation - \\rm -f .zig-cache/zero-native-automation/snapshot.txt .zig-cache/zero-native-automation/windows.txt .zig-cache/zero-native-automation/command.txt "$response_file" - \\printf 'bridge %s\n' "$request" > .zig-cache/zero-native-automation/command.txt - \\"$app" > .zig-cache/zero-native-webview-smoke.log 2>&1 & + \\response_file=".zig-cache/native-sdk-automation/bridge-response.txt" + \\mkdir -p .zig-cache/native-sdk-automation + \\rm -f .zig-cache/native-sdk-automation/snapshot.txt .zig-cache/native-sdk-automation/windows.txt .zig-cache/native-sdk-automation/command*.txt "$response_file" + \\printf 'bridge %s\n' "$request" > .zig-cache/native-sdk-automation/command-1.txt + \\"$app" > .zig-cache/native-sdk-webview-smoke.log 2>&1 & \\pid=$! - \\trap 'kill "$pid" >/dev/null 2>&1 || true; wait "$pid" >/dev/null 2>&1 || true' EXIT + \\trap 'status=$?; kill "$pid" >/dev/null 2>&1 || true; wait "$pid" >/dev/null 2>&1 || true; if [ "$status" -ne 0 ]; then echo "---- app log (.zig-cache/native-sdk-webview-smoke.log) ----" >&2; cat .zig-cache/native-sdk-webview-smoke.log >&2 2>/dev/null || true; fi' EXIT \\snapshot="$("$cli" automate wait 2>&1)" \\case "$snapshot" in *"ready=true"*) ;; *) echo "automation snapshot was not ready" >&2; exit 1 ;; esac \\attempts=0 + \\while [ "$attempts" -lt 50 ] && ! grep -q 'name="webview.load"' .zig-cache/native-sdk-webview-smoke.log; do attempts=$((attempts + 1)); sleep 0.1; done + \\grep -q 'name="webview.load"' .zig-cache/native-sdk-webview-smoke.log || { echo "main window never loaded its webview source (blank window)" >&2; exit 1; } + \\if grep -q 'name="dispatch.error"' .zig-cache/native-sdk-webview-smoke.log; then echo "runtime recorded a dispatch error during startup" >&2; exit 1; fi + \\attempts=0 \\while [ "$attempts" -lt 50 ] && [ ! -s "$response_file" ]; do attempts=$((attempts + 1)); sleep 0.1; done \\response="$(cat "$response_file" 2>/dev/null || true)" \\case "$response" in *'"ok":true'*) ;; *) echo "native.ping did not succeed: $response" >&2; exit 1 ;; esac \\case "$response" in *'pong from Zig'*) ;; *) echo "native.ping response was unexpected: $response" >&2; exit 1 ;; esac \\rm -f "$response_file" - \\printf 'bridge %s\n' '{"id":"webview-create","command":"zero-native.webview.create","payload":{"label":"smoke","url":"https://example.com","frame":{"x":24,"y":24,"width":320,"height":220}}}' > .zig-cache/zero-native-automation/command.txt + \\printf 'bridge %s\n' '{"id":"webview-create","command":"native-sdk.webview.create","payload":{"label":"smoke","url":"https://example.com","frame":{"x":24,"y":24,"width":320,"height":220}}}' > .zig-cache/native-sdk-automation/command-1.txt \\attempts=0 \\while [ "$attempts" -lt 50 ] && [ ! -s "$response_file" ]; do attempts=$((attempts + 1)); sleep 0.1; done \\response="$(cat "$response_file" 2>/dev/null || true)" \\case "$response" in *'"ok":true'*) ;; *) echo "webview create did not succeed: $response" >&2; exit 1 ;; esac \\rm -f "$response_file" - \\printf 'bridge %s\n' '{"id":"webview-resize","command":"zero-native.webview.setFrame","payload":{"label":"smoke","frame":{"x":36,"y":36,"width":420,"height":260}}}' > .zig-cache/zero-native-automation/command.txt + \\printf 'bridge %s\n' '{"id":"webview-resize","command":"native-sdk.webview.setFrame","payload":{"label":"smoke","frame":{"x":36,"y":36,"width":420,"height":260}}}' > .zig-cache/native-sdk-automation/command-1.txt \\attempts=0 \\while [ "$attempts" -lt 50 ] && [ ! -s "$response_file" ]; do attempts=$((attempts + 1)); sleep 0.1; done \\response="$(cat "$response_file" 2>/dev/null || true)" \\case "$response" in *'"ok":true'*) ;; *) echo "webview resize did not succeed: $response" >&2; exit 1 ;; esac \\rm -f "$response_file" - \\printf 'bridge %s\n' '{"id":"webview-navigate","command":"zero-native.webview.navigate","payload":{"label":"smoke","url":"https://example.com/?smoke=1"}}' > .zig-cache/zero-native-automation/command.txt + \\printf 'bridge %s\n' '{"id":"webview-navigate","command":"native-sdk.webview.navigate","payload":{"label":"smoke","url":"https://example.com/?smoke=1"}}' > .zig-cache/native-sdk-automation/command-1.txt \\attempts=0 \\while [ "$attempts" -lt 50 ] && [ ! -s "$response_file" ]; do attempts=$((attempts + 1)); sleep 0.1; done \\response="$(cat "$response_file" 2>/dev/null || true)" \\case "$response" in *'"ok":true'*) ;; *) echo "webview navigate did not succeed: $response" >&2; exit 1 ;; esac \\rm -f "$response_file" - \\printf 'bridge %s\n' '{"id":"webview-close","command":"zero-native.webview.close","payload":{"label":"smoke"}}' > .zig-cache/zero-native-automation/command.txt + \\printf 'bridge %s\n' '{"id":"webview-close","command":"native-sdk.webview.close","payload":{"label":"smoke"}}' > .zig-cache/native-sdk-automation/command-1.txt \\attempts=0 \\while [ "$attempts" -lt 50 ] && [ ! -s "$response_file" ]; do attempts=$((attempts + 1)); sleep 0.1; done \\response="$(cat "$response_file" 2>/dev/null || true)" @@ -470,17 +1048,17 @@ pub fn build(b: *std.Build) void { \\app="zig-out/bin/native-shell" \\cli="$1" \\case "$cli" in /*) ;; *) cli="../../$cli" ;; esac - \\automation_dir=".zig-cache/zero-native-automation" + \\automation_dir=".zig-cache/native-sdk-automation" \\response_file="$automation_dir/bridge-response.txt" \\mkdir -p "$automation_dir" - \\rm -f "$automation_dir/snapshot.txt" "$automation_dir/accessibility.txt" "$automation_dir/windows.txt" "$automation_dir/command.txt" "$response_file" - \\"$app" > .zig-cache/zero-native-native-shell-smoke.log 2>&1 & + \\rm -f "$automation_dir/snapshot.txt" "$automation_dir/accessibility.txt" "$automation_dir/windows.txt" "$automation_dir"/command*.txt "$response_file" + \\"$app" > .zig-cache/native-sdk-native-shell-smoke.log 2>&1 & \\pid=$! - \\trap 'kill "$pid" >/dev/null 2>&1 || true; wait "$pid" >/dev/null 2>&1 || true' EXIT + \\trap 'status=$?; kill "$pid" >/dev/null 2>&1 || true; wait "$pid" >/dev/null 2>&1 || true; if [ "$status" -ne 0 ]; then echo "---- app log (.zig-cache/native-sdk-native-shell-smoke.log) ----" >&2; cat .zig-cache/native-sdk-native-shell-smoke.log >&2 2>/dev/null || true; fi' EXIT \\ready="$("$cli" automate wait 2>&1)" \\case "$ready" in *"ready=true"*) ;; *) echo "native-shell automation snapshot was not ready" >&2; exit 1 ;; esac \\snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" - \\case "$snapshot" in *'window @w1 "zero-native Native Shell"'*) ;; *) echo "native-shell window was missing from snapshot" >&2; exit 1 ;; esac + \\case "$snapshot" in *'window @w1 "Native SDK Native Shell"'*) ;; *) echo "native-shell window was missing from snapshot" >&2; exit 1 ;; esac \\case "$snapshot" in *'view @w1/toolbar kind=toolbar'*) ;; *) echo "toolbar view was missing from snapshot" >&2; exit 1 ;; esac \\case "$snapshot" in *'view @w1/sidebar kind=sidebar'*) ;; *) echo "sidebar view was missing from snapshot" >&2; exit 1 ;; esac \\case "$snapshot" in *'view @w1/main kind=webview'*) ;; *) echo "main WebView was missing from snapshot" >&2; exit 1 ;; esac @@ -520,16 +1098,16 @@ pub fn build(b: *std.Build) void { \\attempts=0 \\while [ "$attempts" -lt 50 ]; do \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" - \\ case "$snapshot" in *'window @w1 "zero-native Native Shell" bounds=('*' 900x640)'*) break ;; esac + \\ case "$snapshot" in *'window @w1 "Native SDK Native Shell" bounds=('*' 900x640)'*) break ;; esac \\ attempts=$((attempts + 1)) \\ sleep 0.1 \\done - \\case "$snapshot" in *'window @w1 "zero-native Native Shell" bounds=('*' 900x640)'*) ;; *) echo "native-shell window resize was not reflected in snapshot" >&2; exit 1 ;; esac + \\case "$snapshot" in *'window @w1 "Native SDK Native Shell" bounds=('*' 900x640)'*) ;; *) echo "native-shell window resize was not reflected in snapshot" >&2; exit 1 ;; esac \\case "$snapshot" in *'view @w1/toolbar kind=toolbar'*'bounds=(0,0 900x52)'*) ;; *) echo "native-shell toolbar did not relayout after resize" >&2; exit 1 ;; esac \\case "$snapshot" in *'view @w1/main kind=webview'*'bounds=(240,52 660x548)'*) ;; *) echo "native-shell main WebView did not relayout after resize" >&2; exit 1 ;; esac \\case "$snapshot" in *'view @w1/statusbar kind=statusbar'*'bounds=(240,600 660x40)'*) ;; *) echo "native-shell statusbar did not relayout after resize" >&2; exit 1 ;; esac \\rm -f "$response_file" - \\printf 'bridge %s\n' '{"id":"native-shell-refresh","command":"zero-native.command.invoke","payload":{"name":"app.refresh"}}' > "$automation_dir/command.txt" + \\printf 'bridge %s\n' '{"id":"native-shell-refresh","command":"native-sdk.command.invoke","payload":{"name":"app.refresh"}}' > "$automation_dir/command-1.txt" \\attempts=0 \\while [ "$attempts" -lt 50 ] && [ ! -s "$response_file" ]; do attempts=$((attempts + 1)); sleep 0.1; done \\response="$(cat "$response_file" 2>/dev/null || true)" @@ -596,8 +1174,809 @@ pub fn build(b: *std.Build) void { native_shell_smoke_run.step.dependOn(&cli_exe.step); native_shell_smoke_step.dependOn(&native_shell_smoke_run.step); + const gpu_surface_smoke_step = b.step("test-gpu-surface-smoke", "Run macOS GPU surface automation smoke test"); + // The GPU smoke apps are managed examples (no build.zig of their own), + // so their binaries come from the CLI verb. -Doptimize=Debug keeps the + // smoke binary at the debug shape the in-dir builds used (`native + // build` injects ReleaseFast when no optimize flag is passed). + const gpu_surface_smoke_build = managedExampleRun(b, cli_exe, &.{ "build", "-Dplatform=macos", "-Dweb-engine=system", "-Dautomation=true", "-Doptimize=Debug" }); + gpu_surface_smoke_build.setCwd(b.path("examples/gpu-surface")); + const gpu_surface_smoke_run = b.addSystemCommand(&.{ + "sh", "-c", + \\set -eu + \\cd examples/gpu-surface + \\app="zig-out/bin/gpu-surface" + \\cli="$1" + \\case "$cli" in /*) ;; *) cli="../../$cli" ;; esac + \\automation_dir=".zig-cache/native-sdk-automation" + \\mkdir -p "$automation_dir" + \\# Startup latencies are load-sensitive on shared CI/agent machines. + \\# NATIVE_SDK_SMOKE_BUDGET_MS raises the first-frame latency budget (default + \\# stays 150 ms) and the automation-ready ceiling (default stays + \\# 500 ms; the ceiling never drops below it) without weakening the + \\# local defaults; every correctness assertion stays strict. + \\smoke_budget_ms="${NATIVE_SDK_SMOKE_BUDGET_MS:-150}" + \\case "$smoke_budget_ms" in ''|*[!0-9]*) echo "NATIVE_SDK_SMOKE_BUDGET_MS must be a positive integer of milliseconds: $smoke_budget_ms" >&2; exit 1 ;; esac + \\if [ "$smoke_budget_ms" -le 0 ]; then echo "NATIVE_SDK_SMOKE_BUDGET_MS must be a positive integer of milliseconds: $smoke_budget_ms" >&2; exit 1; fi + \\smoke_budget_ns=$((smoke_budget_ms * 1000000)) + \\ready_budget_ms="$smoke_budget_ms" + \\if [ "$ready_budget_ms" -lt 500 ]; then ready_budget_ms=500; fi + \\ready_budget_ns=$((ready_budget_ms * 1000000)) + \\rm -f "$automation_dir/snapshot.txt" "$automation_dir/accessibility.txt" "$automation_dir/windows.txt" "$automation_dir"/command*.txt + \\"$app" > .zig-cache/native-sdk-gpu-surface-smoke.log 2>&1 & + \\pid=$! + \\trap 'status=$?; kill "$pid" >/dev/null 2>&1 || true; wait "$pid" >/dev/null 2>&1 || true; if [ "$status" -ne 0 ]; then echo "---- app log (.zig-cache/native-sdk-gpu-surface-smoke.log) ----" >&2; cat .zig-cache/native-sdk-gpu-surface-smoke.log >&2 2>/dev/null || true; fi' EXIT + \\ready="$("$cli" automate wait 2>&1)" + \\case "$ready" in *"ready=true"*) ;; *) echo "gpu-surface automation snapshot was not ready" >&2; exit 1 ;; esac + \\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-surface automation ready uptime was missing" >&2; exit 1 ;; esac + \\if [ "$ready_uptime" -le 0 ] || [ "$ready_uptime" -gt "$ready_budget_ns" ]; then echo "gpu-surface automation ready exceeded $ready_budget_ms ms: $ready_uptime ns" >&2; exit 1; fi + \\snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\case "$snapshot" in *'window @w1 "Native SDK GPU Surface"'*) ;; *) echo "gpu-surface window was missing from snapshot" >&2; exit 1 ;; esac + \\case "$snapshot" in *'view @w1/canvas kind=gpu_surface'*'accessibility_label="Animated GPU surface"'*) ;; *) echo "gpu_surface view was missing from snapshot" >&2; exit 1 ;; esac + \\case "$snapshot" in *'view @w1/inspector kind=webview'*) ;; *) echo "inspector WebView was missing from snapshot" >&2; exit 1 ;; esac + \\case "$snapshot" in *'view @w1/toolbar kind=toolbar'*) ;; *) echo "toolbar view was missing from snapshot" >&2; exit 1 ;; esac + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ case "$snapshot" in *'GPU frame 1 from canvas.'*) break ;; esac + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\case "$snapshot" in *'view @w1/status-label kind=label'*'GPU frame 1 from canvas.'*) ;; *) echo "gpu-surface frame event did not reach the runtime" >&2; exit 1 ;; esac + \\case "$snapshot" in *'view @w1/canvas kind=gpu_surface'*'gpu_nonblank=true'*) ;; *) echo "gpu-surface frame was not verified as nonblank" >&2; exit 1 ;; esac + \\first_frame_latency="$(printf '%s\n' "$snapshot" | sed -n 's/.*view @w1\/canvas kind=gpu_surface.* gpu_first_frame_latency_ns=\([0-9][0-9]*\).*/\1/p')" + \\case "$first_frame_latency" in ''|*[!0-9]*) echo "gpu-surface first frame latency was missing" >&2; exit 1 ;; esac + \\if [ "$first_frame_latency" -le 0 ] || [ "$first_frame_latency" -gt "$smoke_budget_ns" ]; then echo "gpu-surface first frame exceeded $smoke_budget_ms ms: $first_frame_latency ns" >&2; exit 1; fi + \\# The runtime publishes its own fixed 150 ms budget verdict. Within that + \\# budget the verdict must agree exactly; beyond it (reachable only when + \\# NATIVE_SDK_SMOKE_BUDGET_MS > 150) the runtime must report the overrun honestly. + \\if [ "$first_frame_latency" -le 150000000 ]; then + \\ case "$snapshot" in *'view @w1/canvas kind=gpu_surface'*'gpu_first_frame_latency_budget_ns=150000000'*'gpu_first_frame_latency_budget_exceeded=0'*'gpu_first_frame_latency_budget_ok=true'*) ;; *) echo "gpu-surface first frame exceeded the latency budget" >&2; exit 1 ;; esac + \\else + \\ case "$snapshot" in *'view @w1/canvas kind=gpu_surface'*'gpu_first_frame_latency_budget_ns=150000000'*'gpu_first_frame_latency_budget_ok=false'*) ;; *) echo "gpu-surface runtime did not report the first-frame budget overrun" >&2; exit 1 ;; esac + \\fi + \\"$cli" automate native-command gpu.refresh refresh >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ case "$snapshot" in *'GPU surface refreshed from toolbar. Count 1.'*) break ;; esac + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\case "$snapshot" in *'view @w1/status-label kind=label'*'GPU surface refreshed from toolbar. Count 1.'*) ;; *) echo "gpu-surface refresh command did not update status" >&2; exit 1 ;; esac + \\"$cli" automate resize 960 620 >/dev/null 2>&1 + \\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 Surface" bounds=('*' 960x620)'*) break ;; esac + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\case "$snapshot" in *'window @w1 "Native SDK GPU Surface" bounds=('*' 960x620)'*) ;; *) echo "gpu-surface window resize was not reflected in snapshot" >&2; exit 1 ;; esac + \\case "$snapshot" in *'view @w1/canvas kind=gpu_surface'*'bounds=(0,0 680x534)'*) ;; *) echo "gpu_surface view did not relayout after resize" >&2; exit 1 ;; esac + \\case "$snapshot" in *'view @w1/inspector kind=webview'*'bounds=(680,52 280x534)'*) ;; *) echo "inspector WebView did not relayout after resize" >&2; exit 1 ;; esac + \\echo "gpu-surface smoke ok" + , + "sh", + }); + gpu_surface_smoke_run.addFileArg(cli_exe.getEmittedBin()); + gpu_surface_smoke_run.step.dependOn(&gpu_surface_smoke_build.step); + gpu_surface_smoke_run.step.dependOn(&cli_exe.step); + gpu_surface_smoke_step.dependOn(&gpu_surface_smoke_run.step); + + const writeback_smoke_step = b.step("test-writeback-smoke", "Run macOS provenance + write-back automation smoke test"); + // Debug on purpose: the write-back loop lives on the markup + // interpreter + hot-reload watch, which apps enable in Debug + // (kanban's dev_markup_reload) - the release engine is compiled and + // watchless by design. + const writeback_smoke_build = managedExampleRun(b, cli_exe, &.{ "build", "-Dplatform=macos", "-Dweb-engine=system", "-Dautomation=true", "-Doptimize=Debug" }); + writeback_smoke_build.setCwd(b.path("examples/kanban")); + const writeback_smoke_run = b.addSystemCommand(&.{ + "sh", "-c", + \\set -eu + \\cd examples/kanban + \\app="zig-out/bin/kanban" + \\cli="$1" + \\case "$cli" in /*) ;; *) cli="../../$cli" ;; esac + \\automation_dir=".zig-cache/native-sdk-automation" + \\mkdir -p "$automation_dir" + \\rm -f "$automation_dir/snapshot.txt" "$automation_dir"/command*.txt "$automation_dir/provenance.txt" + \\# The smoke edits src/board.native through the write-back verb and restores + \\# it through the same verb; the trap restores from the backup on ANY + \\# failure so an aborted run never leaves the example dirty. + \\cp src/board.native .zig-cache/board.native.smoke-backup + \\"$app" > .zig-cache/native-sdk-writeback-smoke.log 2>&1 & + \\pid=$! + \\trap 'status=$?; kill "$pid" >/dev/null 2>&1 || true; wait "$pid" >/dev/null 2>&1 || true; cp .zig-cache/board.native.smoke-backup src/board.native; if [ "$status" -ne 0 ]; then echo "---- app log (.zig-cache/native-sdk-writeback-smoke.log) ----" >&2; cat .zig-cache/native-sdk-writeback-smoke.log >&2 2>/dev/null || true; fi' EXIT + \\"$cli" automate assert --timeout-ms 30000 'ready=true' >/dev/null + \\snapshot="$(cat "$automation_dir/snapshot.txt")" + \\button_id="$(printf '%s\n' "$snapshot" | sed -n 's/.*widget @w1\/kanban-canvas#\([0-9][0-9]*\) role=button name="Add card".*/\1/p' | head -n 1)" + \\case "$button_id" in ''|*[!0-9]*) echo "writeback smoke: Add card button id was missing from the snapshot" >&2; exit 1 ;; esac + \\# 1. Provenance: the button reports its authored span in the root file. + \\provenance="$("$cli" automate provenance kanban-canvas "$button_id" 2>/dev/null)" + \\case "$provenance" in *"authored=markup"*"root=src/board.native"*) ;; *) echo "writeback smoke: button provenance was not markup-authored: $provenance" >&2; exit 1 ;; esac + \\case "$provenance" in *"node file=src/board.native"*) ;; *) echo "writeback smoke: button provenance named the wrong file: $provenance" >&2; exit 1 ;; esac + \\# 2. Template + import chain: a card title reports its definition site in + \\# the component file plus the site in the root file, with the + \\# for-loop iteration key. + \\card_id="$(printf '%s\n' "$snapshot" | sed -n 's/.*widget @w1\/kanban-canvas#\([0-9][0-9]*\) role=text name="Sketch the board layout".*/\1/p' | head -n 1)" + \\case "$card_id" in ''|*[!0-9]*) echo "writeback smoke: card text id was missing from the snapshot" >&2; exit 1 ;; esac + \\card_provenance="$("$cli" automate provenance kanban-canvas "$card_id" 2>/dev/null)" + \\case "$card_provenance" in *"node file=src/components/board-column.native"*) ;; *) echo "writeback smoke: card provenance missed the component file: $card_provenance" >&2; exit 1 ;; esac + \\case "$card_provenance" in *"use file=src/board.native"*) ;; *) echo "writeback smoke: card provenance missed the use-site chain: $card_provenance" >&2; exit 1 ;; esac + \\case "$card_provenance" in *"keys="*) ;; *) echo "writeback smoke: card provenance missed the iteration key: $card_provenance" >&2; exit 1 ;; esac + \\# 3. Write-back: flip the button label through the verb; the app's own + \\# hot-reload watch picks the file change up and repaints. + \\"$cli" automate edit kanban-canvas "$button_id" set-text "Add task" >/dev/null 2>&1 + \\"$cli" automate assert --timeout-ms 15000 'role=button name="Add task"' >/dev/null + \\# The file diff is byte-exact: exactly the label bytes changed. + \\sed 's/>Add cardAdd task .zig-cache/board.native.smoke-expected + \\cmp -s .zig-cache/board.native.smoke-expected src/board.native || { echo "writeback smoke: the edit was not minimal-diff" >&2; exit 1; } + \\# 4. Flip it back through the same verb: the structural id survived the + \\# reload (text is not identity), and the file restores byte-identical. + \\"$cli" automate edit kanban-canvas "$button_id" set-text "Add card" >/dev/null 2>&1 + \\"$cli" automate assert --timeout-ms 15000 'role=button name="Add card"' >/dev/null + \\cmp -s .zig-cache/board.native.smoke-backup src/board.native || { echo "writeback smoke: the flip-back did not restore the file byte-identically" >&2; exit 1; } + \\# 5. Refusal: an edit that fails validation leaves the file untouched. + \\if "$cli" automate edit kanban-canvas "$button_id" set-attr bogus 1 >/dev/null 2>&1; then echo "writeback smoke: an invalid edit was not refused" >&2; exit 1; fi + \\cmp -s .zig-cache/board.native.smoke-backup src/board.native || { echo "writeback smoke: a refused edit touched the file" >&2; exit 1; } + \\echo "writeback smoke ok" + , + "sh", + }); + writeback_smoke_run.addFileArg(cli_exe.getEmittedBin()); + writeback_smoke_run.step.dependOn(&writeback_smoke_build.step); + writeback_smoke_run.step.dependOn(&cli_exe.step); + writeback_smoke_step.dependOn(&writeback_smoke_run.step); + + const gpu_dashboard_smoke_step = b.step("test-gpu-dashboard-smoke", "Run macOS GPU dashboard automation smoke test"); + const gpu_dashboard_smoke_build = managedExampleRun(b, cli_exe, &.{ "build", "-Dplatform=macos", "-Dweb-engine=system", "-Dautomation=true", "-Doptimize=Debug" }); + gpu_dashboard_smoke_build.setCwd(b.path("examples/gpu-dashboard")); + const gpu_dashboard_smoke_run = b.addSystemCommand(&.{ + "sh", "-c", + \\set -eu + \\cd examples/gpu-dashboard + \\app="zig-out/bin/gpu-dashboard" + \\cli="$1" + \\case "$cli" in /*) ;; *) cli="../../$cli" ;; esac + \\automation_dir=".zig-cache/native-sdk-automation" + \\mkdir -p "$automation_dir" + \\# First-frame latency is load-sensitive: a cold file cache or CI/agent + \\# machine contention can blow the 150 ms budget while the frame itself + \\# is presented and correct. Load tolerance without weakening the proof: + \\# (a) NATIVE_SDK_SMOKE_BUDGET_MS raises the smoke's latency budget (default + \\# stays 150 ms) and the automation-ready ceiling (default stays + \\# 500 ms; the ceiling never drops below it), and + \\# (b) a budget-only overrun relaunches the app once and re-measures. + \\# Every correctness assertion (frame presented, packet-representable, + \\# retained content, widget semantics) stays strict on whichever launch + \\# survives, and the runtime's own fixed 150 ms budget verdict is still + \\# asserted verbatim whenever the measured latency is within 150 ms. + \\smoke_budget_ms="${NATIVE_SDK_SMOKE_BUDGET_MS:-150}" + \\case "$smoke_budget_ms" in ''|*[!0-9]*) echo "NATIVE_SDK_SMOKE_BUDGET_MS must be a positive integer of milliseconds: $smoke_budget_ms" >&2; exit 1 ;; esac + \\if [ "$smoke_budget_ms" -le 0 ]; then echo "NATIVE_SDK_SMOKE_BUDGET_MS must be a positive integer of milliseconds: $smoke_budget_ms" >&2; exit 1; fi + \\smoke_budget_ns=$((smoke_budget_ms * 1000000)) + \\ready_budget_ms="$smoke_budget_ms" + \\if [ "$ready_budget_ms" -lt 500 ]; then ready_budget_ms=500; fi + \\ready_budget_ns=$((ready_budget_ms * 1000000)) + \\pid="" + \\trap 'status=$?; kill "$pid" >/dev/null 2>&1 || true; wait "$pid" >/dev/null 2>&1 || true; if [ "$status" -ne 0 ]; then echo "---- app log (.zig-cache/native-sdk-gpu-dashboard-smoke.log) ----" >&2; cat .zig-cache/native-sdk-gpu-dashboard-smoke.log >&2 2>/dev/null || true; fi' EXIT + \\stop_app() { + \\ kill "$pid" >/dev/null 2>&1 || true + \\ wait "$pid" >/dev/null 2>&1 || true + \\ pid="" + \\} + \\launch_and_measure_first_frame() { + \\ rm -f "$automation_dir/snapshot.txt" "$automation_dir/accessibility.txt" "$automation_dir/windows.txt" "$automation_dir"/command*.txt + \\ "$app" > .zig-cache/native-sdk-gpu-dashboard-smoke.log 2>&1 & + \\ pid=$! + \\ ready="$("$cli" automate wait 2>&1)" + \\ case "$ready" in *"ready=true"*) ;; *) echo "gpu-dashboard automation snapshot was not ready" >&2; exit 1 ;; esac + \\ 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)" + \\ 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 + \\ case "$snapshot" in *'view @w1/dashboard-canvas kind=gpu_surface'*'accessibility_label="Native-rendered product dashboard canvas"'*) ;; *) echo "dashboard GPU canvas was missing from snapshot" >&2; exit 1 ;; esac + \\ attempts=0 + \\ while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ case "$snapshot" in *'view @w1/dashboard-canvas kind=gpu_surface'*'gpu_nonblank=true'*'canvas_commands=68'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) break ;; esac + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\ done + \\ case "$snapshot" in *'view @w1/dashboard-canvas kind=gpu_surface'*'gpu_nonblank=true'*'canvas_commands=68'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) ;; *) echo "dashboard GPU canvas did not present the retained display list as a packet" >&2; exit 1 ;; esac + \\ case "$snapshot" in *'view @w1/dashboard-canvas kind=gpu_surface'*'canvas_commands=68'*'widget_semantics=48'*) ;; *) echo "dashboard GPU canvas was missing retained commands or widget semantics" >&2; exit 1 ;; esac + \\ first_frame_latency="$(printf '%s\n' "$snapshot" | sed -n 's/.*view @w1\/dashboard-canvas kind=gpu_surface.* gpu_first_frame_latency_ns=\([0-9][0-9]*\).*/\1/p')" + \\ case "$first_frame_latency" in ''|*[!0-9]*) echo "dashboard GPU first frame latency was missing" >&2; exit 1 ;; esac + \\ if [ "$first_frame_latency" -le 0 ]; then echo "dashboard GPU first frame latency was not recorded" >&2; exit 1; fi + \\} + \\launch_and_measure_first_frame + \\if [ "$first_frame_latency" -gt "$smoke_budget_ns" ]; then + \\ echo "dashboard GPU first frame exceeded $smoke_budget_ms ms ($first_frame_latency ns); relaunching once to rule out machine load" >&2 + \\ stop_app + \\ launch_and_measure_first_frame + \\fi + \\if [ "$first_frame_latency" -gt "$smoke_budget_ns" ]; then echo "dashboard GPU first frame exceeded $smoke_budget_ms ms on both launches: $first_frame_latency ns" >&2; exit 1; fi + \\# The runtime publishes its own fixed 150 ms budget verdict. Within that + \\# budget the verdict must agree exactly; beyond it (reachable only when + \\# NATIVE_SDK_SMOKE_BUDGET_MS > 150) the runtime must report the overrun honestly. + \\if [ "$first_frame_latency" -le 150000000 ]; then + \\ case "$snapshot" in *'view @w1/dashboard-canvas kind=gpu_surface'*'gpu_first_frame_latency_budget_ns=150000000'*'gpu_first_frame_latency_budget_exceeded=0'*'gpu_first_frame_latency_budget_ok=true'*) ;; *) echo "dashboard GPU first frame exceeded the latency budget" >&2; exit 1 ;; esac + \\else + \\ case "$snapshot" in *'view @w1/dashboard-canvas kind=gpu_surface'*'gpu_first_frame_latency_budget_ns=150000000'*'gpu_first_frame_latency_budget_ok=false'*) ;; *) echo "dashboard runtime did not report the first-frame budget overrun" >&2; exit 1 ;; esac + \\fi + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ case "$snapshot" in *'role=text name="Canvas frame:'*) break ;; esac + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\widget_line() { + \\ printf '%s\n' "$snapshot" | grep -F "$1" | head -1 + \\} + \\line="$(widget_line 'role=tab name="Dashboard mode"')" + \\case "$line" in *'actions=[focus,press,select]'*) ;; *) echo "dashboard toolbar mode semantics were missing" >&2; exit 1 ;; esac + \\line="$(widget_line 'role=button name="Refresh dashboard"')" + \\case "$line" in *'actions=[focus,press]'*) ;; *) echo "dashboard toolbar refresh semantics were missing" >&2; exit 1 ;; esac + \\# CoreText-backed layout metrics: the refresh button must be sized by the + \\# platform text measure provider, not the deterministic estimator + \\# (estimateTextWidthForFont sizes "Refresh" at 14px to 51.842 -> 75.841995 wide). + \\case "$line" in *'75.841995x34'*) echo "dashboard refresh button was sized by the estimator; platform text measurement is inactive" >&2; exit 1 ;; esac + \\refresh_width="$(printf '%s\n' "$line" | sed -n 's/.*bounds=([0-9.,-]* \([0-9.]*\)x[0-9.]*).*/\1/p')" + \\case "$refresh_width" in ''|*[!0-9.]*) echo "dashboard refresh button width was missing" >&2; exit 1 ;; esac + \\if [ "$(printf '%s\n' "$refresh_width < 50 || $refresh_width > 110" | bc)" -eq 1 ]; then echo "dashboard refresh button width was implausible: $refresh_width" >&2; exit 1; fi + \\line="$(widget_line 'role=button name="Live render status"')" + \\case "$line" in *'actions=[focus,press]'*) ;; *) echo "dashboard live render button semantics were missing" >&2; exit 1 ;; esac + \\line="$(widget_line 'role=textbox name="Forecast amount"')" + \\case "$line" in *'text="$13.4M"'*) ;; *) echo "dashboard forecast textbox semantics were missing" >&2; exit 1 ;; esac + \\line="$(widget_line 'role=dialog name="Revenue filter popover"')" + \\case "$line" in '') echo "dashboard popover semantics were missing" >&2; exit 1 ;; esac + \\line="$(widget_line 'role=text name="Canvas frame:')" + \\case "$line" in *'packet ok'*) ;; *) echo "dashboard canvas status semantics were missing" >&2; exit 1 ;; esac + \\gpu_frame_from_snapshot() { + \\ printf '%s\n' "$snapshot" | sed -n 's/.*view @w1\/dashboard-canvas kind=gpu_surface.* gpu_frame=\([0-9][0-9]*\).*/\1/p' + \\} + \\canvas_revision_from_snapshot() { + \\ printf '%s\n' "$snapshot" | sed -n 's/.*view @w1\/dashboard-canvas kind=gpu_surface.* canvas_revision=\([0-9][0-9]*\).*/\1/p' + \\} + \\snapshot_contains() { + \\ case "$snapshot" in *"$1"*) return 0 ;; *) return 1 ;; esac + \\} + \\canvas_revision_before_resize="$(canvas_revision_from_snapshot)" + \\case "$canvas_revision_before_resize" in ''|*[!0-9]*) echo "dashboard canvas revision was missing before resize" >&2; exit 1 ;; esac + \\"$cli" automate resize 1120 700 >/dev/null 2>&1 + \\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" bounds=('*' 1120x700)'*'view @w1/dashboard-canvas kind=gpu_surface'*'bounds=(0,0 1120x700)'*'gpu_nonblank=true'*'canvas_commands=68'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) break ;; esac + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\case "$snapshot" in *'window @w1 "Native SDK GPU Dashboard" bounds=('*' 1120x700)'*) ;; *) echo "dashboard window resize was not reflected in snapshot" >&2; exit 1 ;; esac + \\case "$snapshot" in *'view @w1/dashboard-canvas kind=gpu_surface'*'bounds=(0,0 1120x700)'*) ;; *) echo "dashboard GPU canvas did not relayout after resize" >&2; exit 1 ;; esac + \\case "$snapshot" in *'view @w1/dashboard-canvas kind=gpu_surface'*'gpu_nonblank=true'*'canvas_commands=68'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) ;; *) echo "dashboard GPU canvas did not remain packet-renderable after resize" >&2; exit 1 ;; esac + \\canvas_revision_after_resize="$(canvas_revision_from_snapshot)" + \\case "$canvas_revision_after_resize" in ''|*[!0-9]*) echo "dashboard canvas revision was missing after resize" >&2; exit 1 ;; esac + \\if [ "$canvas_revision_after_resize" -lt "$canvas_revision_before_resize" ]; then echo "dashboard canvas revision went backwards after resize: $canvas_revision_before_resize -> $canvas_revision_after_resize" >&2; exit 1; fi + \\gpu_frame_before="$(gpu_frame_from_snapshot)" + \\case "$gpu_frame_before" in ''|*[!0-9]*) gpu_frame_before=0 ;; esac + \\gpu_frame_after="$gpu_frame_before" + \\switch_id="$(printf '%s\n' "$snapshot" | sed -n 's/.*widget @w1\/dashboard-canvas#\([0-9][0-9]*\) role=switch name="Auto refresh".*/\1/p' | head -1)" + \\case "$switch_id" in ''|*[!0-9]*) echo "dashboard auto refresh switch id was missing from snapshot" >&2; exit 1 ;; esac + \\"$cli" automate widget-click dashboard-canvas "$switch_id" >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ gpu_frame_after="$(gpu_frame_from_snapshot)" + \\ case "$gpu_frame_after" in ''|*[!0-9]*) gpu_frame_after=0 ;; esac + \\ if [ "$gpu_frame_after" -gt "$gpu_frame_before" ]; then + \\ if snapshot_contains 'Auto refresh off.' && snapshot_contains 'view @w1/dashboard-canvas kind=gpu_surface' && snapshot_contains 'canvas_frame_full_repaint=false' && snapshot_contains 'canvas_frame_pipeline_uploads=0' && snapshot_contains 'canvas_frame_glyph_uploads=0' && snapshot_contains 'canvas_frame_text_uploads=0' && snapshot_contains 'canvas_frame_gpu_packet_unsupported=0' && snapshot_contains 'canvas_frame_gpu_packet_representable=true'; then + \\ switch_line="$(printf '%s\n' "$snapshot" | grep -F 'role=switch name="Auto refresh"' | head -1)" + \\ case "$switch_line" in *'value=0'*) break ;; esac + \\ fi + \\ fi + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\if [ "$gpu_frame_after" -le "$gpu_frame_before" ]; then echo "dashboard switch click did not request a GPU frame" >&2; exit 1; fi + \\case "$snapshot" in *'Auto refresh off.'*) ;; *) echo "dashboard switch click did not update status" >&2; exit 1 ;; esac + \\switch_line="$(printf '%s\n' "$snapshot" | grep -F 'role=switch name="Auto refresh"' | head -1)" + \\case "$switch_line" in *'value=0'*) ;; *) echo "dashboard switch click did not route through pointer input" >&2; exit 1 ;; esac + \\case "$snapshot" in *'view @w1/dashboard-canvas kind=gpu_surface'*'canvas_frame_full_repaint=false'*'canvas_frame_pipeline_uploads=0'*'canvas_frame_glyph_uploads=0'*'canvas_frame_text_uploads=0'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) ;; *) echo "dashboard switch click did not present an incremental GPU packet without interaction-time uploads" >&2; exit 1 ;; esac + \\input_timestamp="$(printf '%s\n' "$snapshot" | sed -n 's/.*view @w1\/dashboard-canvas kind=gpu_surface.* gpu_input_timestamp_ns=\([0-9][0-9]*\).*/\1/p')" + \\case "$input_timestamp" in ''|*[!0-9]*) echo "dashboard GPU input timestamp was missing after widget interaction" >&2; exit 1 ;; esac + \\if [ "$input_timestamp" -le 0 ]; then echo "dashboard GPU input timestamp was not recorded" >&2; exit 1; fi + \\input_latency="$(printf '%s\n' "$snapshot" | sed -n 's/.*view @w1\/dashboard-canvas kind=gpu_surface.* gpu_input_latency_ns=\([0-9][0-9]*\).*/\1/p')" + \\case "$input_latency" in ''|*[!0-9]*) echo "dashboard GPU input latency was missing after widget interaction" >&2; exit 1 ;; esac + \\# --- Incremental pixel equivalence: relaunch under the host's verify + \\# mode, where every scissored dirty update is byte-compared against a + \\# from-scratch full redraw of the retained command list. Clicks, the + \\# live-pulse animation, and a mid-animation resize drive scissored + \\# patches; the verify counters must show checks with zero mismatches. + \\stop_app + \\rm -f "$automation_dir/snapshot.txt" "$automation_dir"/command*.txt + \\verify_log=".zig-cache/native-sdk-gpu-dashboard-verify.log" + \\NATIVE_SDK_GPU_VERIFY_INCREMENTAL=1 "$app" > "$verify_log" 2>&1 & + \\pid=$! + \\ready="$("$cli" automate wait 2>&1)" + \\case "$ready" in *"ready=true"*) ;; *) echo "gpu-dashboard verify relaunch was not ready" >&2; exit 1 ;; esac + \\snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\switch_id="$(printf '%s\n' "$snapshot" | sed -n 's/.*widget @w1\/dashboard-canvas#\([0-9][0-9]*\) role=switch name="Auto refresh".*/\1/p' | head -1)" + \\live_id="$(printf '%s\n' "$snapshot" | sed -n 's/.*widget @w1\/dashboard-canvas#\([0-9][0-9]*\) role=button name="Live render status".*/\1/p' | head -1)" + \\case "$switch_id" in ''|*[!0-9]*) echo "dashboard verify switch id was missing" >&2; exit 1 ;; esac + \\case "$live_id" in ''|*[!0-9]*) echo "dashboard verify live button id was missing" >&2; exit 1 ;; esac + \\"$cli" automate widget-click dashboard-canvas "$switch_id" >/dev/null 2>&1 + \\sleep 0.4 + \\"$cli" automate widget-click dashboard-canvas "$live_id" >/dev/null 2>&1 + \\sleep 0.3 + \\"$cli" automate resize 1120 700 >/dev/null 2>&1 + \\sleep 0.5 + \\"$cli" automate widget-click dashboard-canvas "$live_id" >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ if grep -q "gpu incremental verify view=dashboard-canvas checks=" "$verify_log" 2>/dev/null; then break; fi + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\if ! grep -q "gpu incremental verify view=dashboard-canvas checks=" "$verify_log" 2>/dev/null; then echo "dashboard verify mode recorded no incremental checks" >&2; exit 1; fi + \\if grep -q "verify MISMATCH" "$verify_log" 2>/dev/null; then echo "dashboard incremental present diverged from a full redraw:" >&2; grep "verify MISMATCH" "$verify_log" >&2; exit 1; fi + \\if grep "gpu incremental verify view=dashboard-canvas checks=" "$verify_log" | grep -qv "mismatches=0"; then echo "dashboard incremental verify reported mismatches" >&2; exit 1; fi + \\echo "gpu-dashboard smoke ok" + , + "sh", + }); + gpu_dashboard_smoke_run.addFileArg(cli_exe.getEmittedBin()); + gpu_dashboard_smoke_run.step.dependOn(&gpu_dashboard_smoke_build.step); + gpu_dashboard_smoke_run.step.dependOn(&cli_exe.step); + gpu_dashboard_smoke_step.dependOn(&gpu_dashboard_smoke_run.step); + + // Percentile performance check — a single-sample gate was noise-dominated, + // so this asserts p90 over N launches. Deliberately NOT part of + // `zig build test` or the fast gate tier: N launches are slow and the + // numbers only mean something on a controlled machine. Runs via + // `scripts/gate.sh full --perf` locally and a dedicated macos-14 CI job. + // Knobs: NATIVE_SDK_PERF_LAUNCHES, NATIVE_SDK_PERF_INTERACTIONS, NATIVE_SDK_PERF_BUDGET_MS, + // NATIVE_SDK_PERF_INPUT_BUDGET_MS — see scripts/perf-gpu-dashboard.sh. + const gpu_dashboard_perf_step = b.step("test-gpu-dashboard-perf", "Run macOS GPU dashboard percentile performance check (N launches; slow)"); + const gpu_dashboard_perf_build = managedExampleRun(b, cli_exe, &.{ "build", "-Dplatform=macos", "-Dweb-engine=system", "-Dautomation=true", "-Doptimize=Debug" }); + gpu_dashboard_perf_build.setCwd(b.path("examples/gpu-dashboard")); + const gpu_dashboard_perf_run = b.addSystemCommand(&.{ "sh", "scripts/perf-gpu-dashboard.sh" }); + gpu_dashboard_perf_run.addFileArg(cli_exe.getEmittedBin()); + gpu_dashboard_perf_run.step.dependOn(&gpu_dashboard_perf_build.step); + gpu_dashboard_perf_run.step.dependOn(&cli_exe.step); + gpu_dashboard_perf_step.dependOn(&gpu_dashboard_perf_run.step); + + const canvas_preview_smoke_step = b.step("test-canvas-preview-smoke", "Run macOS canvas + webview (both-in-one-window) automation smoke test"); + const canvas_preview_smoke_build = managedExampleRun(b, cli_exe, &.{ "build", "-Dplatform=macos", "-Dweb-engine=system", "-Dautomation=true", "-Doptimize=Debug" }); + canvas_preview_smoke_build.setCwd(b.path("examples/canvas-preview")); + const canvas_preview_smoke_run = b.addSystemCommand(&.{ + "sh", "-c", + \\set -eu + \\cd examples/canvas-preview + \\app="zig-out/bin/canvas-preview" + \\cli="$1" + \\case "$cli" in /*) ;; *) cli="../../$cli" ;; esac + \\automation_dir=".zig-cache/native-sdk-automation" + \\mkdir -p "$automation_dir" + \\rm -f "$automation_dir/snapshot.txt" "$automation_dir/accessibility.txt" "$automation_dir/windows.txt" "$automation_dir"/command*.txt "$automation_dir/screenshot-preview-canvas.png" + \\"$app" > .zig-cache/native-sdk-canvas-preview-smoke.log 2>&1 & + \\pid=$! + \\trap 'status=$?; kill "$pid" >/dev/null 2>&1 || true; wait "$pid" >/dev/null 2>&1 || true; if [ "$status" -ne 0 ]; then echo "---- app log (.zig-cache/native-sdk-canvas-preview-smoke.log) ----" >&2; cat .zig-cache/native-sdk-canvas-preview-smoke.log >&2 2>/dev/null || true; fi' EXIT + \\ready="$("$cli" automate wait 2>&1)" + \\case "$ready" in *"ready=true"*) ;; *) echo "canvas-preview automation snapshot was not ready" >&2; exit 1 ;; esac + \\# Both architectures live in window 1: a presenting Metal canvas and + \\# a live WKWebView on a real https URL — with no implicit main webview. + \\"$cli" automate assert 'view @w1/preview-canvas kind=gpu_surface.*gpu_nonblank=true' 'view @w1/preview kind=webview.*url="https://example.com/"' + \\"$cli" automate assert --absent 'view @w1/main kind=webview' 'source kind=html bytes=0' + \\# The webview pane is snapped to the canvas anchor widget: right of + \\# the 224pt sidebar, below the 56pt toolbar. + \\preview_line="$(grep 'view @w1/preview kind=webview' "$automation_dir/snapshot.txt" | head -1)" + \\preview_x="$(printf '%s\n' "$preview_line" | sed -n 's/.*bounds=(\([0-9.]*\),.*/\1/p')" + \\case "$preview_x" in ''|*[!0-9.]*) echo "canvas-preview webview x was missing" >&2; exit 1 ;; esac + \\if [ "$(printf '%s\n' "$preview_x < 224" | bc)" -eq 1 ]; then echo "canvas-preview webview was not snapped right of the sidebar: x=$preview_x" >&2; exit 1; fi + \\# Navigation from a Msg: the app command switches the model URL and + \\# the runtime navigates the platform webview. + \\"$cli" automate native-command app.docs >/dev/null 2>&1 + \\"$cli" automate assert 'view @w1/preview kind=webview.*url="https://zero-native.dev/"' 'name="URL: https://zero-native.dev/"' + \\# Resize keeps the pane snapped to the anchor widget's new frame: + \\# the panel right of the 224pt sidebar and below the 56pt toolbar. + \\"$cli" automate resize 1200 800 >/dev/null 2>&1 + \\"$cli" automate assert 'window @w1 "Native SDK Canvas Preview" bounds=.*1200x800' 'view @w1/preview kind=webview.*bounds=.224,56 976x744' + \\# Canvas screenshot evidence (reference-rendered PNG of the chrome). + \\"$cli" automate screenshot preview-canvas >/dev/null 2>&1 + \\test -s "$automation_dir/screenshot-preview-canvas.png" || { echo "canvas-preview screenshot was empty" >&2; exit 1; } + \\echo "canvas-preview smoke ok" + , + "sh", + }); + canvas_preview_smoke_run.addFileArg(cli_exe.getEmittedBin()); + canvas_preview_smoke_run.step.dependOn(&canvas_preview_smoke_build.step); + canvas_preview_smoke_run.step.dependOn(&cli_exe.step); + canvas_preview_smoke_step.dependOn(&canvas_preview_smoke_run.step); + + const gpu_components_smoke_step = b.step("test-gpu-components-smoke", "Run macOS GPU components automation smoke test"); + const gpu_components_smoke_build = managedExampleRun(b, cli_exe, &.{ "build", "-Dplatform=macos", "-Dweb-engine=system", "-Dautomation=true", "-Doptimize=Debug" }); + gpu_components_smoke_build.setCwd(b.path("examples/gpu-components")); + const gpu_components_smoke_run = b.addSystemCommand(&.{ + "sh", "-c", + \\set -eu + \\cd examples/gpu-components + \\app="zig-out/bin/gpu-components" + \\cli="$1" + \\case "$cli" in /*) ;; *) cli="../../$cli" ;; esac + \\automation_dir=".zig-cache/native-sdk-automation" + \\mkdir -p "$automation_dir" + \\# Startup latencies are load-sensitive on shared CI/agent machines. + \\# NATIVE_SDK_SMOKE_BUDGET_MS raises the first-frame latency budget (default + \\# stays 150 ms) and the automation-ready ceiling (default stays + \\# 500 ms; the ceiling never drops below it) without weakening the + \\# local defaults; every correctness assertion stays strict. + \\smoke_budget_ms="${NATIVE_SDK_SMOKE_BUDGET_MS:-150}" + \\case "$smoke_budget_ms" in ''|*[!0-9]*) echo "NATIVE_SDK_SMOKE_BUDGET_MS must be a positive integer of milliseconds: $smoke_budget_ms" >&2; exit 1 ;; esac + \\if [ "$smoke_budget_ms" -le 0 ]; then echo "NATIVE_SDK_SMOKE_BUDGET_MS must be a positive integer of milliseconds: $smoke_budget_ms" >&2; exit 1; fi + \\smoke_budget_ns=$((smoke_budget_ms * 1000000)) + \\ready_budget_ms="$smoke_budget_ms" + \\if [ "$ready_budget_ms" -lt 500 ]; then ready_budget_ms=500; fi + \\ready_budget_ns=$((ready_budget_ms * 1000000)) + \\rm -f "$automation_dir/snapshot.txt" "$automation_dir/accessibility.txt" "$automation_dir/windows.txt" "$automation_dir"/command*.txt + \\"$app" > .zig-cache/native-sdk-gpu-components-smoke.log 2>&1 & + \\pid=$! + \\trap 'status=$?; kill "$pid" >/dev/null 2>&1 || true; wait "$pid" >/dev/null 2>&1 || true; if [ "$status" -ne 0 ]; then echo "---- app log (.zig-cache/native-sdk-gpu-components-smoke.log) ----" >&2; cat .zig-cache/native-sdk-gpu-components-smoke.log >&2 2>/dev/null || true; fi' EXIT + \\ready="$("$cli" automate wait 2>&1)" + \\case "$ready" in *"ready=true"*) ;; *) echo "gpu-components automation snapshot was not ready" >&2; exit 1 ;; esac + \\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-components automation ready uptime was missing" >&2; exit 1 ;; esac + \\if [ "$ready_uptime" -le 0 ] || [ "$ready_uptime" -gt "$ready_budget_ns" ]; then echo "gpu-components automation ready exceeded $ready_budget_ms ms: $ready_uptime ns" >&2; exit 1; fi + \\snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\case "$snapshot" in *'window @w1 "Native SDK GPU Components"'*) ;; *) echo "gpu-components window was missing from snapshot" >&2; exit 1 ;; esac + \\case "$snapshot" in *'view @w1/main kind=webview'*) echo "components should not create an implicit main WebView" >&2; exit 1 ;; *) ;; esac + \\case "$snapshot" in *'source kind=html bytes=0'*) echo "components should not publish an empty default WebView source" >&2; exit 1 ;; *) ;; esac + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'gpu_nonblank=true'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) break ;; esac + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'gpu_nonblank=true'*) ;; *) echo "components GPU surface was not ready and nonblank" >&2; exit 1 ;; esac + \\case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) ;; *) echo "components GPU frame was not packet-representable" >&2; exit 1 ;; esac + \\first_frame_latency="$(printf '%s\n' "$snapshot" | sed -n 's/.*view @w1\/components-canvas kind=gpu_surface.* gpu_first_frame_latency_ns=\([0-9][0-9]*\).*/\1/p')" + \\case "$first_frame_latency" in ''|*[!0-9]*) echo "components GPU first frame latency was missing" >&2; exit 1 ;; esac + \\if [ "$first_frame_latency" -le 0 ] || [ "$first_frame_latency" -gt "$smoke_budget_ns" ]; then echo "components GPU first frame exceeded $smoke_budget_ms ms: $first_frame_latency ns" >&2; exit 1; fi + \\# The runtime publishes its own fixed 150 ms budget verdict. Within that + \\# budget the verdict must agree exactly; beyond it (reachable only when + \\# NATIVE_SDK_SMOKE_BUDGET_MS > 150) the runtime must report the overrun honestly. + \\if [ "$first_frame_latency" -le 150000000 ]; then + \\ case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'gpu_first_frame_latency_budget_ns=150000000'*'gpu_first_frame_latency_budget_exceeded=0'*'gpu_first_frame_latency_budget_ok=true'*) ;; *) echo "components GPU first frame exceeded the latency budget" >&2; exit 1 ;; esac + \\else + \\ case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'gpu_first_frame_latency_budget_ns=150000000'*'gpu_first_frame_latency_budget_ok=false'*) ;; *) echo "components runtime did not report the first-frame budget overrun" >&2; exit 1 ;; esac + \\fi + \\gpu_frame_from_snapshot() { + \\ printf '%s\n' "$snapshot" | sed -n 's/.*view @w1\/components-canvas kind=gpu_surface.* gpu_frame=\([0-9][0-9]*\).*/\1/p' + \\} + \\canvas_revision_from_snapshot() { + \\ printf '%s\n' "$snapshot" | sed -n 's/.*view @w1\/components-canvas kind=gpu_surface.* canvas_revision=\([0-9][0-9]*\).*/\1/p' + \\} + \\snapshot_contains() { + \\ case "$snapshot" in *"$1"*) return 0 ;; *) return 1 ;; esac + \\} + \\case "$snapshot" in *'widget @w1/components-canvas#113 role=checkbox'*'value=1'*'actions=[focus,toggle]'*) ;; *) echo "checkbox widget was not initially selected" >&2; exit 1 ;; esac + \\case "$snapshot" in *'widget @w1/components-canvas#114 role=switch'*'value=1'*'actions=[focus,toggle]'*) ;; *) echo "switch widget was not initially selected" >&2; exit 1 ;; esac + \\case "$snapshot" in *'widget @w1/components-canvas#116 role=progressbar'*'value=1'*) ;; *) echo "progress widget was missing from the initial snapshot" >&2; exit 1 ;; esac + \\case "$snapshot" in *'widget @w1/components-canvas#117 role=tab'*'value=1'*'actions=[focus,select]'*) ;; *) echo "small segmented control was not initially selected" >&2; exit 1 ;; esac + \\case "$snapshot" in *'widget @w1/components-canvas#119 role=tab'*'value=0'*'actions=[focus,select]'*) ;; *) echo "large segmented control was not initially available" >&2; exit 1 ;; esac + \\case "$snapshot" in *'widget @w1/components-canvas#142 role=menuitem'*'actions=[focus,press,select]'*) ;; *) echo "menu item widget was not initially actionable" >&2; exit 1 ;; esac + \\case "$snapshot" in *'widget @w1/components-canvas#86 role=tab'*'actions=[focus,press,select]'*) ;; *) echo "theme toolbar trigger was not initially actionable" >&2; exit 1 ;; esac + \\case "$snapshot" in *'widget @w1/components-canvas#83 role=button'*'actions=[focus,press]'*) ;; *) echo "refresh toolbar widget was not initially actionable" >&2; exit 1 ;; esac + \\gpu_frame_before="$(gpu_frame_from_snapshot)" + \\case "$gpu_frame_before" in ''|*[!0-9]*) gpu_frame_before=0 ;; esac + \\gpu_frame_after="$gpu_frame_before" + \\canvas_revision_before="$(canvas_revision_from_snapshot)" + \\case "$canvas_revision_before" in ''|*[!0-9]*) canvas_revision_before=0 ;; esac + \\canvas_revision_after="$canvas_revision_before" + \\"$cli" automate widget-click components-canvas 86 >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ gpu_frame_after="$(gpu_frame_from_snapshot)" + \\ case "$gpu_frame_after" in ''|*[!0-9]*) gpu_frame_after=0 ;; esac + \\ canvas_revision_after="$(canvas_revision_from_snapshot)" + \\ case "$canvas_revision_after" in ''|*[!0-9]*) canvas_revision_after=0 ;; esac + \\ if [ "$gpu_frame_after" -gt "$gpu_frame_before" ] || [ "$canvas_revision_after" -gt "$canvas_revision_before" ]; then + \\ if snapshot_contains 'GPU component theme: ' && snapshot_contains ' from native_view. Count 1.' && snapshot_contains 'view @w1/components-canvas kind=gpu_surface' && snapshot_contains 'canvas_frame_gpu_packet_unsupported=0' && snapshot_contains 'canvas_frame_gpu_packet_representable=true'; then break; fi + \\ fi + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\if [ "$gpu_frame_after" -le "$gpu_frame_before" ] && [ "$canvas_revision_after" -le "$canvas_revision_before" ]; then echo "theme automation command did not update the retained GPU canvas" >&2; exit 1; fi + \\case "$snapshot" in *'GPU component theme: '*' from native_view. Count 1.'*) ;; *) echo "theme automation command did not update status" >&2; exit 1 ;; esac + \\case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) ;; *) echo "theme automation command did not present a packet-renderable GPU frame" >&2; exit 1 ;; esac + \\"$cli" automate widget-action components-canvas 111 focus >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ case "$snapshot" in *'widget @w1/components-canvas#111 role=textbox'*'focused=true'*'text="native-sdk"'*) break ;; esac + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\case "$snapshot" in *'widget @w1/components-canvas#111 role=textbox'*'focused=true'*'text="native-sdk"'*) ;; *) echo "widget focus automation did not focus retained text" >&2; exit 1 ;; esac + \\"$cli" automate widget-key components-canvas z z >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ case "$snapshot" in *'widget @w1/components-canvas#111 role=textbox'*'focused=true'*'text="native-sdkz"'*) break ;; esac + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\case "$snapshot" in *'widget @w1/components-canvas#111 role=textbox'*'focused=true'*'text="native-sdkz"'*) ;; *) echo "widget keyboard automation did not update retained text" >&2; exit 1 ;; esac + \\"$cli" automate widget-key components-canvas tab >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ case "$snapshot" in *'widget @w1/components-canvas#112 role=textbox'*'focused=true'*) break ;; esac + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\case "$snapshot" in *'widget @w1/components-canvas#112 role=textbox'*'focused=true'*) ;; *) echo "widget keyboard automation did not move focus" >&2; exit 1 ;; esac + \\"$cli" automate widget-action components-canvas 105 press >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ if snapshot_contains 'Keyed icon_button #105.' && snapshot_contains 'widget @w1/components-canvas#105 role=button' && snapshot_contains 'actions=[focus,press]'; then break; fi + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\if ! snapshot_contains 'Keyed icon_button #105.' || ! snapshot_contains 'widget @w1/components-canvas#105 role=button' || ! snapshot_contains 'actions=[focus,press]'; then echo "icon button automation press did not update status" >&2; exit 1; fi + \\"$cli" automate widget-action components-canvas 111 set-text native-engine >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ case "$snapshot" in *'widget @w1/components-canvas#111 role=textbox'*'text="native-engine"'*) break ;; esac + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\case "$snapshot" in *'widget @w1/components-canvas#111 role=textbox'*'text="native-engine"'*) ;; *) echo "text field automation did not update retained text" >&2; exit 1 ;; esac + \\"$cli" automate widget-action components-canvas 115 increment >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ case "$snapshot" in *'Keyed slider #115: value '*) + \\ case "$snapshot" in *'widget @w1/components-canvas#115 role=slider'*'value=0.62'*) ;; *'widget @w1/components-canvas#115 role=slider'*) break ;; esac + \\ ;; + \\ esac + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\case "$snapshot" in *'Keyed slider #115: value '*) ;; *) echo "slider automation increment did not update status" >&2; exit 1 ;; esac + \\case "$snapshot" in *'widget @w1/components-canvas#115 role=slider'*'value=0.62'*) echo "slider automation increment did not change value" >&2; exit 1 ;; *'widget @w1/components-canvas#115 role=slider'*) ;; *) echo "slider widget was missing after increment" >&2; exit 1 ;; esac + \\gpu_frame_before="$(gpu_frame_from_snapshot)" + \\case "$gpu_frame_before" in ''|*[!0-9]*) gpu_frame_before=0 ;; esac + \\gpu_frame_after="$gpu_frame_before" + \\"$cli" automate widget-drag components-canvas 115 0.25 0.82 >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ gpu_frame_after="$(gpu_frame_from_snapshot)" + \\ case "$gpu_frame_after" in ''|*[!0-9]*) gpu_frame_after=0 ;; esac + \\ if [ "$gpu_frame_after" -gt "$gpu_frame_before" ]; then + \\ if snapshot_contains 'Clicked slider #115: value 0.82.' && snapshot_contains 'widget @w1/components-canvas#115 role=slider' && { snapshot_contains 'value=0.82' || snapshot_contains 'value=0.819'; }; then + \\ case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'canvas_frame_full_repaint=false'*'canvas_frame_pipeline_uploads=0'*'canvas_frame_image_uploads=0'*'canvas_frame_glyph_uploads=0'*'canvas_frame_text_uploads=0'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) break ;; esac + \\ fi + \\ fi + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\if [ "$gpu_frame_after" -le "$gpu_frame_before" ]; then echo "slider automation drag did not request a GPU frame" >&2; exit 1; fi + \\if ! snapshot_contains 'Clicked slider #115: value 0.82.' || ! snapshot_contains 'widget @w1/components-canvas#115 role=slider' || { ! snapshot_contains 'value=0.82' && ! snapshot_contains 'value=0.819'; }; then echo "slider automation drag did not update retained slider state" >&2; exit 1; fi + \\case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'canvas_frame_full_repaint=false'*'canvas_frame_pipeline_uploads=0'*'canvas_frame_image_uploads=0'*'canvas_frame_glyph_uploads=0'*'canvas_frame_text_uploads=0'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) ;; *) echo "slider automation drag did not present an incremental GPU packet without interaction-time uploads" >&2; exit 1 ;; esac + \\"$cli" automate widget-action components-canvas 156 press >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ if snapshot_contains 'Keyed data_cell #156: selected.' && snapshot_contains 'widget @w1/components-canvas#156 role=gridcell' && snapshot_contains 'focused=true' && snapshot_contains 'value=1'; then break; fi + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\if ! snapshot_contains 'Keyed data_cell #156: selected.' || ! snapshot_contains 'widget @w1/components-canvas#156 role=gridcell' || ! snapshot_contains 'focused=true' || ! snapshot_contains 'value=1'; then echo "grid cell automation press did not focus and report status" >&2; exit 1; fi + \\gpu_frame_before="$(gpu_frame_from_snapshot)" + \\case "$gpu_frame_before" in ''|*[!0-9]*) gpu_frame_before=0 ;; esac + \\gpu_frame_after="$gpu_frame_before" + \\"$cli" automate widget-click components-canvas 119 >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ gpu_frame_after="$(gpu_frame_from_snapshot)" + \\ case "$gpu_frame_after" in ''|*[!0-9]*) gpu_frame_after=0 ;; esac + \\ if [ "$gpu_frame_after" -gt "$gpu_frame_before" ]; then + \\ if snapshot_contains 'Clicked segmented_control #119: selected.' && snapshot_contains 'widget @w1/components-canvas#117 role=tab' && snapshot_contains 'value=0' && snapshot_contains 'widget @w1/components-canvas#119 role=tab' && snapshot_contains 'focused=true' && snapshot_contains 'value=1'; then + \\ case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'canvas_frame_full_repaint=false'*'canvas_frame_pipeline_uploads=0'*'canvas_frame_image_uploads=0'*'canvas_frame_glyph_uploads=0'*'canvas_frame_text_uploads=0'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) break ;; esac + \\ fi + \\ fi + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\if [ "$gpu_frame_after" -le "$gpu_frame_before" ]; then echo "segmented control automation click did not request a GPU frame" >&2; exit 1; fi + \\if ! snapshot_contains 'Clicked segmented_control #119: selected.' || ! snapshot_contains 'widget @w1/components-canvas#117 role=tab' || ! snapshot_contains 'value=0' || ! snapshot_contains 'widget @w1/components-canvas#119 role=tab' || ! snapshot_contains 'focused=true' || ! snapshot_contains 'value=1'; then echo "segmented control automation click did not update retained selection state" >&2; exit 1; fi + \\case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'canvas_frame_full_repaint=false'*'canvas_frame_pipeline_uploads=0'*'canvas_frame_image_uploads=0'*'canvas_frame_glyph_uploads=0'*'canvas_frame_text_uploads=0'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) ;; *) echo "segmented control automation click did not present an incremental GPU packet without interaction-time uploads" >&2; exit 1 ;; esac + \\# The catalog's project menu is an ACTIONS menu (no sibling declares a + \\# committed row), so automation select mints NO selection: value + \\# stays 0. Automation focus follows the pointer contract (rows take + \\# focus QUIETLY, no visible affordance), so nothing repaints and no + \\# frame is requested — the republished snapshot alone carries the + \\# focus move. A picker menu would mint value=1 and repaint; the + \\# select specimen's committed-row tests cover that register. + \\"$cli" automate widget-action components-canvas 142 select >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ case "$(printf '%s\\n' "$snapshot" | grep -F 'components-canvas#142 role=menuitem' | head -1)" in *'focused=true'*'value=0'*) break ;; esac + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\menu_item_line="$(printf '%s\\n' "$snapshot" | grep -F 'components-canvas#142 role=menuitem' | head -1)" + \\case "$menu_item_line" in *'focused=true'*'value=0'*) ;; *) echo "menu item automation select did not move quiet focus without minting a selection" >&2; exit 1 ;; esac + \\"$cli" automate widget-action components-canvas 113 toggle >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ if snapshot_contains 'Keyed checkbox #113: off.' && snapshot_contains 'widget @w1/components-canvas#113 role=checkbox' && snapshot_contains 'value=0'; then break; fi + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\if ! snapshot_contains 'Keyed checkbox #113: off.' || ! snapshot_contains 'widget @w1/components-canvas#113 role=checkbox' || ! snapshot_contains 'value=0'; then echo "checkbox automation toggle did not update the retained widget snapshot" >&2; exit 1; fi + \\gpu_frame_before="$(gpu_frame_from_snapshot)" + \\case "$gpu_frame_before" in ''|*[!0-9]*) gpu_frame_before=0 ;; esac + \\gpu_frame_after="$gpu_frame_before" + \\"$cli" automate widget-click components-canvas 113 >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ gpu_frame_after="$(gpu_frame_from_snapshot)" + \\ case "$gpu_frame_after" in ''|*[!0-9]*) gpu_frame_after=0 ;; esac + \\ if [ "$gpu_frame_after" -gt "$gpu_frame_before" ]; then + \\ if snapshot_contains 'Clicked checkbox #113: on.' && snapshot_contains 'widget @w1/components-canvas#113 role=checkbox' && snapshot_contains 'value=1'; then + \\ case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'canvas_frame_full_repaint=false'*'canvas_frame_pipeline_uploads=0'*'canvas_frame_image_uploads=0'*'canvas_frame_glyph_uploads=0'*'canvas_frame_text_uploads=0'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) break ;; esac + \\ fi + \\ fi + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\if [ "$gpu_frame_after" -le "$gpu_frame_before" ]; then echo "checkbox automation click did not request a GPU frame" >&2; exit 1; fi + \\if ! snapshot_contains 'Clicked checkbox #113: on.' || ! snapshot_contains 'widget @w1/components-canvas#113 role=checkbox' || ! snapshot_contains 'value=1'; then echo "checkbox automation click did not route through pointer input" >&2; exit 1; fi + \\case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'canvas_frame_full_repaint=false'*'canvas_frame_pipeline_uploads=0'*'canvas_frame_image_uploads=0'*'canvas_frame_glyph_uploads=0'*'canvas_frame_text_uploads=0'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) ;; *) echo "checkbox automation click did not present an incremental GPU packet without interaction-time uploads" >&2; exit 1 ;; esac + \\"$cli" automate widget-action components-canvas 114 toggle >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ if snapshot_contains 'Keyed switch_control #114: off.' && snapshot_contains 'widget @w1/components-canvas#114 role=switch' && snapshot_contains 'value=0'; then break; fi + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\if ! snapshot_contains 'Keyed switch_control #114: off.' || ! snapshot_contains 'widget @w1/components-canvas#114 role=switch' || ! snapshot_contains 'value=0'; then echo "switch automation toggle did not wake the idle app" >&2; exit 1; fi + \\gpu_frame_before="$(gpu_frame_from_snapshot)" + \\case "$gpu_frame_before" in ''|*[!0-9]*) gpu_frame_before=0 ;; esac + \\gpu_frame_after="$gpu_frame_before" + \\"$cli" automate widget-click components-canvas 114 >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ gpu_frame_after="$(gpu_frame_from_snapshot)" + \\ case "$gpu_frame_after" in ''|*[!0-9]*) gpu_frame_after=0 ;; esac + \\ if [ "$gpu_frame_after" -gt "$gpu_frame_before" ]; then + \\ if snapshot_contains 'Clicked switch_control #114: on.' && snapshot_contains 'widget @w1/components-canvas#114 role=switch' && snapshot_contains 'value=1'; then + \\ case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'canvas_frame_full_repaint=false'*'canvas_frame_pipeline_uploads=0'*'canvas_frame_image_uploads=0'*'canvas_frame_glyph_uploads=0'*'canvas_frame_text_uploads=0'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) break ;; esac + \\ fi + \\ fi + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\if [ "$gpu_frame_after" -le "$gpu_frame_before" ]; then echo "switch automation click did not request a GPU frame" >&2; exit 1; fi + \\if ! snapshot_contains 'Clicked switch_control #114: on.' || ! snapshot_contains 'widget @w1/components-canvas#114 role=switch' || ! snapshot_contains 'value=1'; then echo "switch automation click did not route through pointer input" >&2; exit 1; fi + \\case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'canvas_frame_full_repaint=false'*'canvas_frame_pipeline_uploads=0'*'canvas_frame_image_uploads=0'*'canvas_frame_glyph_uploads=0'*'canvas_frame_text_uploads=0'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) ;; *) echo "switch automation click did not present an incremental GPU packet without interaction-time uploads" >&2; exit 1 ;; esac + \\gpu_frame_before="$(gpu_frame_from_snapshot)" + \\case "$gpu_frame_before" in ''|*[!0-9]*) gpu_frame_before=0 ;; esac + \\gpu_frame_after="$gpu_frame_before" + \\canvas_revision_before="$(canvas_revision_from_snapshot)" + \\case "$canvas_revision_before" in ''|*[!0-9]*) canvas_revision_before=0 ;; esac + \\canvas_revision_after="$canvas_revision_before" + \\"$cli" automate widget-action components-canvas 120 increment >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ gpu_frame_after="$(gpu_frame_from_snapshot)" + \\ case "$gpu_frame_after" in ''|*[!0-9]*) gpu_frame_after=0 ;; esac + \\ canvas_revision_after="$(canvas_revision_from_snapshot)" + \\ case "$canvas_revision_after" in ''|*[!0-9]*) canvas_revision_after=0 ;; esac + \\ if [ "$gpu_frame_after" -gt "$gpu_frame_before" ] || [ "$canvas_revision_after" -gt "$canvas_revision_before" ]; then + \\ if snapshot_contains 'Keyed list #120: offset 56.' && snapshot_contains 'widget @w1/components-canvas#120 role=list' && snapshot_contains 'scroll=[offset=56,viewport=56,content=168]'; then + \\ case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'canvas_frame_full_repaint=false'*'canvas_frame_pipeline_uploads=0'*'canvas_frame_image_uploads=0'*'canvas_frame_glyph_uploads=0'*'canvas_frame_text_uploads=0'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) break ;; esac + \\ fi + \\ fi + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\if [ "$gpu_frame_after" -le "$gpu_frame_before" ] && [ "$canvas_revision_after" -le "$canvas_revision_before" ]; then echo "list automation increment did not update the retained GPU canvas" >&2; exit 1; fi + \\if ! snapshot_contains 'Keyed list #120: offset 56.' || ! snapshot_contains 'widget @w1/components-canvas#120 role=list' || ! snapshot_contains 'scroll=[offset=56,viewport=56,content=168]'; then echo "list automation increment did not update retained scroll semantics" >&2; exit 1; fi + \\case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'canvas_frame_full_repaint=false'*'canvas_frame_pipeline_uploads=0'*'canvas_frame_image_uploads=0'*'canvas_frame_glyph_uploads=0'*'canvas_frame_text_uploads=0'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) ;; *) echo "list automation increment did not present an incremental GPU packet without interaction-time uploads" >&2; exit 1 ;; esac + \\gpu_frame_before="$(gpu_frame_from_snapshot)" + \\case "$gpu_frame_before" in ''|*[!0-9]*) gpu_frame_before=0 ;; esac + \\gpu_frame_after="$gpu_frame_before" + \\canvas_revision_before="$(canvas_revision_from_snapshot)" + \\case "$canvas_revision_before" in ''|*[!0-9]*) canvas_revision_before=0 ;; esac + \\canvas_revision_after="$canvas_revision_before" + \\"$cli" automate widget-action components-canvas 150 increment >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ gpu_frame_after="$(gpu_frame_from_snapshot)" + \\ case "$gpu_frame_after" in ''|*[!0-9]*) gpu_frame_after=0 ;; esac + \\ canvas_revision_after="$(canvas_revision_from_snapshot)" + \\ case "$canvas_revision_after" in ''|*[!0-9]*) canvas_revision_after=0 ;; esac + \\ if [ "$gpu_frame_after" -gt "$gpu_frame_before" ] || [ "$canvas_revision_after" -gt "$canvas_revision_before" ]; then + \\ if snapshot_contains 'Keyed table #150: offset 56.' && snapshot_contains 'widget @w1/components-canvas#150 role=grid' && snapshot_contains 'scroll=[offset=56,viewport=28,content=140]'; then + \\ case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'canvas_frame_full_repaint=false'*'canvas_frame_pipeline_uploads=0'*'canvas_frame_image_uploads=0'*'canvas_frame_glyph_uploads=0'*'canvas_frame_text_uploads=0'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) break ;; esac + \\ fi + \\ fi + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\if [ "$gpu_frame_after" -le "$gpu_frame_before" ] && [ "$canvas_revision_after" -le "$canvas_revision_before" ]; then echo "data grid automation increment did not update the retained GPU canvas" >&2; exit 1; fi + \\if ! snapshot_contains 'Keyed table #150: offset 56.' || ! snapshot_contains 'widget @w1/components-canvas#150 role=grid' || ! snapshot_contains 'scroll=[offset=56,viewport=28,content=140]'; then echo "data grid automation increment did not update retained scroll semantics" >&2; exit 1; fi + \\case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'canvas_frame_full_repaint=false'*'canvas_frame_pipeline_uploads=0'*'canvas_frame_image_uploads=0'*'canvas_frame_glyph_uploads=0'*'canvas_frame_text_uploads=0'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) ;; *) echo "data grid automation increment did not present an incremental GPU packet without interaction-time uploads" >&2; exit 1 ;; esac + \\gpu_frame_before="$(gpu_frame_from_snapshot)" + \\case "$gpu_frame_before" in ''|*[!0-9]*) gpu_frame_before=0 ;; esac + \\gpu_frame_after="$gpu_frame_before" + \\canvas_revision_before="$(canvas_revision_from_snapshot)" + \\case "$canvas_revision_before" in ''|*[!0-9]*) canvas_revision_before=0 ;; esac + \\canvas_revision_after="$canvas_revision_before" + \\"$cli" automate widget-wheel components-canvas 130 20 >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ gpu_frame_after="$(gpu_frame_from_snapshot)" + \\ case "$gpu_frame_after" in ''|*[!0-9]*) gpu_frame_after=0 ;; esac + \\ canvas_revision_after="$(canvas_revision_from_snapshot)" + \\ case "$canvas_revision_after" in ''|*[!0-9]*) canvas_revision_after=0 ;; esac + \\ if [ "$gpu_frame_after" -gt "$gpu_frame_before" ] || [ "$canvas_revision_after" -gt "$canvas_revision_before" ]; then + \\ case "$snapshot" in *'widget @w1/components-canvas#130 role=group'*'scroll=[offset=84,viewport=56,content=140]'*) + \\ case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'canvas_frame_full_repaint=false'*'canvas_frame_pipeline_uploads=0'*'canvas_frame_image_uploads=0'*'canvas_frame_glyph_uploads=0'*'canvas_frame_text_uploads=0'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) break ;; esac + \\ ;; + \\ esac + \\ fi + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\if [ "$gpu_frame_after" -le "$gpu_frame_before" ] && [ "$canvas_revision_after" -le "$canvas_revision_before" ]; then echo "scroll automation wheel did not update the retained GPU canvas" >&2; exit 1; fi + \\case "$snapshot" in *'widget @w1/components-canvas#130 role=group'*'scroll=[offset=84,viewport=56,content=140]'*) ;; *) echo "scroll automation wheel did not update retained scroll semantics" >&2; exit 1 ;; esac + \\case "$snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'canvas_frame_full_repaint=false'*'canvas_frame_pipeline_uploads=0'*'canvas_frame_image_uploads=0'*'canvas_frame_glyph_uploads=0'*'canvas_frame_text_uploads=0'*'canvas_frame_gpu_packet_unsupported=0'*'canvas_frame_gpu_packet_representable=true'*) ;; *) echo "scroll automation wheel did not present an incremental GPU packet without interaction-time uploads" >&2; exit 1 ;; esac + \\input_timestamp="$(printf '%s\n' "$snapshot" | sed -n 's/.*view @w1\/components-canvas kind=gpu_surface.* gpu_input_timestamp_ns=\([0-9][0-9]*\).*/\1/p')" + \\case "$input_timestamp" in ''|*[!0-9]*) echo "components GPU input timestamp was missing after widget interaction" >&2; exit 1 ;; esac + \\if [ "$input_timestamp" -le 0 ]; then echo "components GPU input timestamp was not recorded" >&2; exit 1; fi + \\input_latency="$(printf '%s\n' "$snapshot" | sed -n 's/.*view @w1\/components-canvas kind=gpu_surface.* gpu_input_latency_ns=\([0-9][0-9]*\).*/\1/p')" + \\case "$input_latency" in ''|*[!0-9]*) echo "components GPU input latency was missing after widget interaction" >&2; exit 1 ;; esac + \\# gpu_input_latency now stamps at the RESPONDING present's completion + \\# (input to glass), which legitimately spans the paced render wait — + \\# up to a couple of display intervals while animations hold the paced + \\# loop. Assert an explicit input-to-glass bound (the perf harness + \\# budgets the same channel at 100 ms) instead of the one-interval + \\# budget flag the old completion-channel stamp happened to satisfy. + \\if [ "$input_latency" -le 0 ] || [ "$input_latency" -gt 100000000 ]; then echo "components GPU input-to-glass latency was implausible: $input_latency ns" >&2; exit 1; fi + \\echo "gpu-components smoke ok" + , + "sh", + }); + gpu_components_smoke_run.addFileArg(cli_exe.getEmittedBin()); + gpu_components_smoke_run.step.dependOn(&gpu_components_smoke_build.step); + gpu_components_smoke_run.step.dependOn(&cli_exe.step); + gpu_components_smoke_step.dependOn(&gpu_components_smoke_run.step); + const webview_cef_smoke_step = b.step("test-webview-cef-smoke", "Run macOS Chromium WebView automation smoke test"); - const webview_cef_smoke_build = b.addSystemCommand(&.{ "zig", "build", "-Dplatform=macos", "-Dweb-engine=chromium", b.fmt("-Dcef-dir={s}", .{cef_dir}), "-Dautomation=true", "-Djs-bridge=true" }); + const webview_cef_smoke_build = b.addSystemCommand(&.{ "zig", "build", "-Dplatform=macos", "-Dweb-engine=chromium", b.fmt("-Dcef-dir={s}", .{exampleCefDir(b, cef_dir)}), "-Dautomation=true", "-Djs-bridge=true" }); webview_cef_smoke_build.setCwd(b.path("examples/webview")); const webview_cef_smoke_run = b.addSystemCommand(&.{ "sh", "-c", @@ -607,39 +1986,43 @@ pub fn build(b: *std.Build) void { \\cli="$1" \\case "$cli" in /*) ;; *) cli="../../$cli" ;; esac \\request='{"id":"ping","command":"native.ping","payload":{"source":"cef-smoke"}}' - \\response_file=".zig-cache/zero-native-automation/bridge-response.txt" - \\mkdir -p .zig-cache/zero-native-automation - \\rm -f .zig-cache/zero-native-automation/snapshot.txt .zig-cache/zero-native-automation/windows.txt .zig-cache/zero-native-automation/command.txt "$response_file" - \\printf 'bridge %s\n' "$request" > .zig-cache/zero-native-automation/command.txt - \\"$app" > .zig-cache/zero-native-webview-cef-smoke.log 2>&1 & + \\response_file=".zig-cache/native-sdk-automation/bridge-response.txt" + \\mkdir -p .zig-cache/native-sdk-automation + \\rm -f .zig-cache/native-sdk-automation/snapshot.txt .zig-cache/native-sdk-automation/windows.txt .zig-cache/native-sdk-automation/command*.txt "$response_file" + \\printf 'bridge %s\n' "$request" > .zig-cache/native-sdk-automation/command-1.txt + \\"$app" > .zig-cache/native-sdk-webview-cef-smoke.log 2>&1 & \\pid=$! - \\trap 'kill "$pid" >/dev/null 2>&1 || true; wait "$pid" >/dev/null 2>&1 || true' EXIT + \\trap 'status=$?; kill "$pid" >/dev/null 2>&1 || true; wait "$pid" >/dev/null 2>&1 || true; if [ "$status" -ne 0 ]; then echo "---- app log (.zig-cache/native-sdk-webview-cef-smoke.log) ----" >&2; cat .zig-cache/native-sdk-webview-cef-smoke.log >&2 2>/dev/null || true; fi' EXIT \\snapshot="$("$cli" automate wait 2>&1)" \\case "$snapshot" in *"ready=true"*) ;; *) echo "automation snapshot was not ready" >&2; exit 1 ;; esac \\attempts=0 + \\while [ "$attempts" -lt 50 ] && ! grep -q 'name="webview.load"' .zig-cache/native-sdk-webview-cef-smoke.log; do attempts=$((attempts + 1)); sleep 0.1; done + \\grep -q 'name="webview.load"' .zig-cache/native-sdk-webview-cef-smoke.log || { echo "main window never loaded its webview source (blank window)" >&2; exit 1; } + \\if grep -q 'name="dispatch.error"' .zig-cache/native-sdk-webview-cef-smoke.log; then echo "runtime recorded a dispatch error during startup" >&2; exit 1; fi + \\attempts=0 \\while [ "$attempts" -lt 50 ] && [ ! -s "$response_file" ]; do attempts=$((attempts + 1)); sleep 0.1; done \\response="$(cat "$response_file" 2>/dev/null || true)" \\case "$response" in *'"ok":true'*'pong from Zig'*) ;; *) echo "native.ping response was unexpected: $response" >&2; exit 1 ;; esac \\rm -f "$response_file" - \\printf 'bridge %s\n' '{"id":"webview-create","command":"zero-native.webview.create","payload":{"label":"smoke","url":"https://example.com","frame":{"x":24,"y":24,"width":320,"height":220}}}' > .zig-cache/zero-native-automation/command.txt + \\printf 'bridge %s\n' '{"id":"webview-create","command":"native-sdk.webview.create","payload":{"label":"smoke","url":"https://example.com","frame":{"x":24,"y":24,"width":320,"height":220}}}' > .zig-cache/native-sdk-automation/command-1.txt \\attempts=0 \\while [ "$attempts" -lt 50 ] && [ ! -s "$response_file" ]; do attempts=$((attempts + 1)); sleep 0.1; done \\response="$(cat "$response_file" 2>/dev/null || true)" \\case "$response" in *'"ok":true'*) ;; *) echo "cef webview create did not succeed: $response" >&2; exit 1 ;; esac \\rm -f "$response_file" - \\printf 'bridge %s\n' '{"id":"webview-resize","command":"zero-native.webview.setFrame","payload":{"label":"smoke","frame":{"x":36,"y":36,"width":420,"height":260}}}' > .zig-cache/zero-native-automation/command.txt + \\printf 'bridge %s\n' '{"id":"webview-resize","command":"native-sdk.webview.setFrame","payload":{"label":"smoke","frame":{"x":36,"y":36,"width":420,"height":260}}}' > .zig-cache/native-sdk-automation/command-1.txt \\attempts=0 \\while [ "$attempts" -lt 50 ] && [ ! -s "$response_file" ]; do attempts=$((attempts + 1)); sleep 0.1; done \\response="$(cat "$response_file" 2>/dev/null || true)" \\case "$response" in *'"ok":true'*) ;; *) echo "cef webview resize did not succeed: $response" >&2; exit 1 ;; esac \\rm -f "$response_file" - \\printf 'bridge %s\n' '{"id":"webview-navigate","command":"zero-native.webview.navigate","payload":{"label":"smoke","url":"https://example.com/?smoke=1"}}' > .zig-cache/zero-native-automation/command.txt + \\printf 'bridge %s\n' '{"id":"webview-navigate","command":"native-sdk.webview.navigate","payload":{"label":"smoke","url":"https://example.com/?smoke=1"}}' > .zig-cache/native-sdk-automation/command-1.txt \\attempts=0 \\while [ "$attempts" -lt 50 ] && [ ! -s "$response_file" ]; do attempts=$((attempts + 1)); sleep 0.1; done \\response="$(cat "$response_file" 2>/dev/null || true)" \\case "$response" in *'"ok":true'*) ;; *) echo "cef webview navigate did not succeed: $response" >&2; exit 1 ;; esac \\rm -f "$response_file" - \\printf 'bridge %s\n' '{"id":"webview-close","command":"zero-native.webview.close","payload":{"label":"smoke"}}' > .zig-cache/zero-native-automation/command.txt + \\printf 'bridge %s\n' '{"id":"webview-close","command":"native-sdk.webview.close","payload":{"label":"smoke"}}' > .zig-cache/native-sdk-automation/command-1.txt \\attempts=0 \\while [ "$attempts" -lt 50 ] && [ ! -s "$response_file" ]; do attempts=$((attempts + 1)); sleep 0.1; done \\response="$(cat "$response_file" 2>/dev/null || true)" @@ -658,12 +2041,12 @@ pub fn build(b: *std.Build) void { const dev_step = b.step("dev", "Run managed frontend dev server and native shell"); dev_step.dependOn(&dev_run.step); - const lib_step = b.step("lib", "Build zero-native embeddable static library"); + const lib_step = b.step("lib", "Build native-sdk embeddable static library"); lib_step.dependOn(&b.addInstallArtifact(embed_lib, .{}).step); const doctor_run = b.addRunArtifact(host_cli_exe); doctor_run.addArg("doctor"); - const doctor_step = b.step("doctor", "Print zero-native platform diagnostics"); + const doctor_step = b.step("doctor", "Print native-sdk platform diagnostics"); doctor_step.dependOn(&doctor_run.step); const validate_run = b.addRunArtifact(host_cli_exe); @@ -682,7 +2065,7 @@ pub fn build(b: *std.Build) void { "--target", @tagName(package_target), "--output", - b.fmt("zig-out/package/zero-native-{s}-{s}-{s}{s}", .{ package_version, @tagName(package_target), optimize_name, packageSuffix(package_target) }), + b.fmt("zig-out/package/native-sdk-{s}-{s}-{s}{s}", .{ package_version, @tagName(package_target), optimize_name, packageSuffix(package_target) }), "--binary", }); package_run.addFileArg(embed_lib.getEmittedBin()); @@ -699,7 +2082,7 @@ pub fn build(b: *std.Build) void { "--target", "macos", "--output", - b.fmt("zig-out/package/zero-native-cef-smoke-{s}.app", .{optimize_name}), + b.fmt("zig-out/package/native-sdk-cef-smoke-{s}.app", .{optimize_name}), "--binary", }); package_cef_run.addFileArg(embed_lib.getEmittedBin()); @@ -712,7 +2095,7 @@ pub fn build(b: *std.Build) void { "sh", "-c", b.fmt( \\set -e - \\app="zig-out/package/zero-native-cef-smoke-{s}.app" + \\app="zig-out/package/native-sdk-cef-smoke-{s}.app" \\test -d "$app/Contents/Frameworks/Chromium Embedded Framework.framework" \\test -f "$app/Contents/Frameworks/Chromium Embedded Framework.framework/Resources/icudtl.dat" \\test -f "$app/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libGLESv2.dylib" @@ -724,44 +2107,86 @@ pub fn build(b: *std.Build) void { const package_cef_smoke_step = b.step("test-package-cef-layout", "Verify macOS Chromium package layout"); package_cef_smoke_step.dependOn(&package_cef_check.step); + // Signed-package seal pin: package an ad-hoc signed bundle and prove + // the signature survives packaging intact with codesign's own strict + // verifier. This is the regression gate for the ordering bug where a + // file written into Contents/Resources AFTER signing invalidated the + // resource seal ("file added"), turning every quarantined install + // into Gatekeeper's "damaged — move to Trash" dialog. The bundle + // carries the CLI as its executable (packaging only needs a real + // Mach-O to sign); the check skips loudly on hosts without codesign + // (any non-macOS machine) instead of pretending to have verified. + const package_signing_run = b.addRunArtifact(host_cli_exe); + package_signing_run.addArgs(&.{ "package", "--target", "macos", "--output", "zig-out/package/native-sdk-signing-verify.app", "--binary" }); + package_signing_run.addFileArg(host_cli_exe.getEmittedBin()); + package_signing_run.addArgs(&.{ "--manifest", "app.zon", "--assets", "assets", "--optimize", optimize_name, "--signing", "adhoc" }); + package_signing_run.has_side_effects = true; + const package_signing_check = b.addSystemCommand(&.{ + "sh", "-c", + \\set -e + \\app="zig-out/package/native-sdk-signing-verify.app" + \\if ! command -v codesign >/dev/null 2>&1; then + \\ echo "codesign unavailable on this host; skipping signed-package verification" + \\ exit 0 + \\fi + \\codesign --verify --strict --deep "$app" + \\grep -q "ad-hoc signed" "$app/Contents/Resources/signing-plan.txt" + \\echo "signed package verify ok" + , + }); + package_signing_check.step.dependOn(&package_signing_run.step); + const package_signing_step = b.step("test-package-signing", "Verify an ad-hoc signed macOS package passes codesign --verify --strict"); + package_signing_step.dependOn(&package_signing_check.step); + const package_windows_run = b.addRunArtifact(host_cli_exe); - package_windows_run.addArgs(&.{ "package-windows", "--output", b.fmt("zig-out/package/zero-native-{s}-windows-Debug", .{package_version}), "--manifest", "app.zon", "--assets", "assets" }); + package_windows_run.addArgs(&.{ "package-windows", "--output", b.fmt("zig-out/package/native-sdk-{s}-windows-Debug", .{package_version}), "--manifest", "app.zon", "--assets", "assets" }); const package_windows_step = b.step("package-windows", "Create local Windows artifact directory"); package_windows_step.dependOn(&package_windows_run.step); const package_linux_run = b.addRunArtifact(host_cli_exe); - package_linux_run.addArgs(&.{ "package-linux", "--output", b.fmt("zig-out/package/zero-native-{s}-linux-Debug", .{package_version}), "--manifest", "app.zon", "--assets", "assets" }); + package_linux_run.addArgs(&.{ "package-linux", "--output", b.fmt("zig-out/package/native-sdk-{s}-linux-Debug", .{package_version}), "--manifest", "app.zon", "--assets", "assets" }); const package_linux_step = b.step("package-linux", "Create local Linux artifact directory"); package_linux_step.dependOn(&package_linux_run.step); const package_ios_run = b.addRunArtifact(host_cli_exe); - package_ios_run.addArgs(&.{ "package-ios", "--output", b.fmt("zig-out/mobile/zero-native-{s}-ios-Debug", .{package_version}), "--manifest", "app.zon", "--assets", "assets" }); + package_ios_run.addArgs(&.{ "package-ios", "--output", b.fmt("zig-out/mobile/native-sdk-{s}-ios-Debug", .{package_version}), "--manifest", "app.zon", "--assets", "assets" }); const package_ios_step = b.step("package-ios", "Create local iOS host skeleton"); package_ios_step.dependOn(&package_ios_run.step); const package_android_run = b.addRunArtifact(host_cli_exe); - package_android_run.addArgs(&.{ "package-android", "--output", b.fmt("zig-out/mobile/zero-native-{s}-android-Debug", .{package_version}), "--manifest", "app.zon", "--assets", "assets" }); - const package_android_step = b.step("package-android", "Create local Android host skeleton"); + package_android_run.addArgs(&.{ "package-android", "--output", b.fmt("zig-out/mobile/native-sdk-{s}-android-Debug", .{package_version}), "--manifest", "app.zon", "--assets", "assets" }); + const package_android_step = b.step("package-android", "Create local Android host project"); package_android_step.dependOn(&package_android_run.step); - const generate_icon_step = b.step("generate-icon", "Generate .icns and .ico from assets/icon.png"); - const iconset_script = b.addSystemCommand(&.{ - "sh", "-c", - \\set -e - \\command -v python3 >/dev/null || { echo "python3 required for icon generation" >&2; exit 1; } - \\python3 -c " - \\from PIL import Image; import os - \\img = Image.open('assets/icon.png') - \\iconset = 'zig-out/icon.iconset' - \\os.makedirs(iconset, exist_ok=True) - \\for name, sz in {'icon_16x16.png':16,'icon_16x16@2x.png':32,'icon_32x32.png':32,'icon_32x32@2x.png':64,'icon_128x128.png':128,'icon_128x128@2x.png':256,'icon_256x256.png':256,'icon_256x256@2x.png':512,'icon_512x512.png':512,'icon_512x512@2x.png':1024}.items(): - \\ img.resize((sz,sz),Image.LANCZOS).save(os.path.join(iconset,name),'PNG') - \\img.save('assets/icon.ico',format='ICO',sizes=[(16,16),(32,32),(48,48),(64,64),(128,128),(256,256)]) - \\" - \\iconutil -c icns zig-out/icon.iconset -o assets/icon.icns - \\echo "generated assets/icon.icns and assets/icon.ico" + // Default app icon: rendered from vector geometry (tools/ + // generate_app_icon.zig) through the SDK's own path rasterizer, so + // the checked-in .icns/.ico/.png/.svg all regenerate from source. + // The built-in app-icon pipeline assembles and round-trip-validates + // the containers itself (no external tools, any host OS), and the + // CLI's embedded scaffold copies are kept in sync. + const generate_icon_step = b.step("generate-icon", "Regenerate the default app icon (.icns/.ico/.png/.svg) from vector source"); + const generate_icon_mod = module(b, target, optimize, "tools/generate_app_icon.zig"); + generate_icon_mod.addImport("native_sdk", desktop_mod); + const generate_icon_exe = b.addExecutable(.{ + .name = "generate-app-icon", + .root_module = generate_icon_mod, }); - generate_icon_step.dependOn(&iconset_script.step); + const generate_icon_run = b.addRunArtifact(generate_icon_exe); + generate_icon_run.addArgs(&.{ + "assets/icon.icns", + "assets/icon.png", + "assets/icon.ico", + "assets/icon.svg", + "src/tooling/default_icon.icns", + "src/tooling/default_icon.png", + "zig-out/icon-full-bleed.png", + // The one committed full-bleed copy: the notes example's raw + // one-image icon source (it demos the packaging mask + inset), + // regenerated here so it can never drift from the default. + "examples/notes/assets/icon.png", + }); + generate_icon_run.has_side_effects = true; + generate_icon_step.dependOn(&generate_icon_run.step); const notarize_run = b.addRunArtifact(host_cli_exe); notarize_run.addArgs(&.{ @@ -769,7 +2194,7 @@ pub fn build(b: *std.Build) void { "--target", "macos", "--output", - b.fmt("zig-out/package/zero-native-{s}-macos-{s}.app", .{ package_version, optimize_name }), + b.fmt("zig-out/package/native-sdk-{s}-macos-{s}.app", .{ package_version, optimize_name }), "--binary", }); notarize_run.addFileArg(embed_lib.getEmittedBin()); @@ -783,10 +2208,10 @@ pub fn build(b: *std.Build) void { const dmg_script = b.addSystemCommand(&.{ "sh", "-c", b.fmt( - \\APP="zig-out/package/zero-native-{s}-macos-{s}.app" - \\DMG="zig-out/package/zero-native-{s}-macos-{s}.dmg" + \\APP="zig-out/package/native-sdk-{s}-macos-{s}.app" + \\DMG="zig-out/package/native-sdk-{s}-macos-{s}.dmg" \\test -d "$APP" || {{ echo "run 'zig build package' first" >&2; exit 1; }} - \\hdiutil create -volname "zero-native" -srcfolder "$APP" -ov -format UDZO "$DMG" + \\hdiutil create -volname "native-sdk" -srcfolder "$APP" -ov -format UDZO "$DMG" \\echo "created $DMG" , .{ package_version, optimize_name, package_version, optimize_name }), }); @@ -827,8 +2252,206 @@ fn module(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin }); } +/// The short commit hash of the framework checkout the CLI is built +/// from, for `native version` staleness checks. "unknown" when +/// git is unavailable (e.g. building from a package tarball). +fn cliBuildCommit(b: *std.Build) []const u8 { + var code: u8 = undefined; + const output = b.runAllowFail(&.{ "git", "rev-parse", "--short", "HEAD" }, &code, .ignore) catch return "unknown"; + const trimmed = std.mem.trim(u8, output, " \n\r\t"); + if (trimmed.len == 0) return "unknown"; + return trimmed; +} + fn testArtifact(b: *std.Build, mod: *std.Build.Module) *std.Build.Step.Compile { - return b.addTest(.{ .root_module = mod }); + return filteredTestArtifact(b, mod, "test", &.{}); +} + +fn filteredTestArtifact(b: *std.Build, mod: *std.Build.Module, name: []const u8, filters: []const []const u8) *std.Build.Step.Compile { + // use_llvm: Zig 0.16.0's self-hosted x86_64 backend miscompiles the + // SysV C ABI for f32-heavy signatures (native_sdk_app_viewport); see + // useLlvmWorkaround in build/app.zig for the full story and repro. + const use_llvm = if (mod.resolved_target) |target| @import("build/app.zig").useLlvmWorkaround(target) else null; + return b.addTest(.{ .name = name, .root_module = mod, .filters = filters, .use_llvm = use_llvm }); +} + +/// One slice of the framework test suite, selected by test-name filters. +/// The framework module compiles into a single test binary whose ~580 +/// tests run serially in one process, which made that binary the longest +/// step in `zig build test` by a wide margin — the rest of the suite is +/// dozens of binaries that each finish in well under a second. The +/// aggregate `test` step therefore runs the framework module as one +/// filtered binary per family below, so the build runner executes the +/// families concurrently and the suite finishes with the slowest family +/// instead of the sum of all of them. `zig build test-desktop` still runs +/// the unfiltered binary when a single process is easier to debug, and +/// each family has its own `test-desktop-` step. +const DesktopTestShard = struct { + /// Suffix for the artifact name and the `test-desktop-` step. + name: []const u8, + description: []const u8, + /// A test file's namespace is routed to the first shard in + /// `desktop_test_shard_specs` with a matching prefix, so earlier + /// entries carve their family out of the later, broader ones and + /// the final empty prefix collects every namespace left over. + prefixes: []const []const u8, +}; + +const desktop_test_shard_specs = [_]DesktopTestShard{ + .{ + .name = "canvas-widget", + .description = "Run framework canvas widget tests", + .prefixes = &.{"runtime.canvas_widget"}, + }, + .{ + .name = "canvas-frame", + .description = "Run framework canvas frame, budget, image, and font tests", + .prefixes = &.{"runtime.canvas"}, + }, + .{ + .name = "ui-shell", + .description = "Run framework UI app and shell layout tests", + .prefixes = &.{ "runtime.ui_app", "runtime.shell" }, + }, + .{ + .name = "runtime-core", + .description = "Run framework runtime core, effects, session, and bridge tests", + .prefixes = &.{"runtime."}, + }, + .{ + .name = "platform", + .description = "Run framework platform, automation, embed, and remaining tests", + .prefixes = &.{""}, + }, +}; + +/// A framework source file that declares top-level tests, described by +/// the dotted namespace the test runner uses in fully-qualified test +/// names: tests in src/runtime/effects_tests.zig are named +/// "runtime.effects_tests.test.". +const DesktopTestFile = struct { + namespace: []const u8, + /// Names of the file's `test ""` declarations, in source order. + /// Unnamed `test { ... }` blocks are not listed: the test runner + /// exempts them from filtering, so they run in every shard. That is + /// deliberate — those blocks are the aggregators whose references + /// make the compiler discover the named tests in the first place, + /// and their bodies are empty at run time. + named_tests: []const []const u8, +}; + +/// Test binaries for the framework module, one per shard spec. Filters +/// are derived from the source tree at configure time: every framework +/// file with a top-level `test` declaration contributes its namespace as +/// a filter ("runtime.effects_tests.test") to exactly one shard, so a +/// new test file is picked up — and routed to a shard by prefix — the +/// moment it exists, and a file can never silently fall out of +/// `zig build test` because a hand-kept list went stale. +fn desktopTestShardArtifacts(b: *std.Build, mod: *std.Build.Module) [desktop_test_shard_specs.len]*std.Build.Step.Compile { + const files = desktopTestFiles(b); + var filters: [desktop_test_shard_specs.len]std.ArrayList([]const u8) = @splat(.empty); + for (files) |file| { + const shard = desktopTestShardIndex(file.namespace); + if (std.mem.eql(u8, file.namespace, "root")) { + // Filters match anywhere in a fully-qualified name, and the + // module root's namespace ("root") is a dotted suffix of + // every other root.zig namespace ("runtime.root", + // "platform.root", ...), so a bare "root.test" filter would + // pull those files' tests into this shard as well. Filter + // the module root by exact test name instead. + for (file.named_tests) |test_name| { + filters[shard].append(b.allocator, b.fmt("root.test.{s}", .{test_name})) catch @panic("OOM"); + } + continue; + } + filters[shard].append(b.allocator, b.fmt("{s}.test", .{file.namespace})) catch @panic("OOM"); + // The same suffix hazard across two shards would run the longer + // file's tests twice per `zig build test`. Refuse the layout up + // front so the collision is resolved when the file is added, not + // when a duplicated test starts flaking. + for (files) |other| { + if (desktopTestShardIndex(other.namespace) == shard) continue; + if (std.mem.endsWith(u8, other.namespace, b.fmt(".{s}", .{file.namespace}))) { + std.debug.panic( + "framework test shards: namespace {s} is a dotted suffix of {s} in another shard; their name filters would overlap. Adjust desktop_test_shard_specs so both land in one shard.", + .{ file.namespace, other.namespace }, + ); + } + } + } + var artifacts: [desktop_test_shard_specs.len]*std.Build.Step.Compile = undefined; + for (&artifacts, desktop_test_shard_specs, filters) |*artifact, spec, shard_filters| { + artifact.* = filteredTestArtifact(b, mod, b.fmt("desktop-{s}-tests", .{spec.name}), shard_filters.items); + } + return artifacts; +} + +fn desktopTestShardIndex(namespace: []const u8) usize { + for (desktop_test_shard_specs, 0..) |spec, index| { + for (spec.prefixes) |prefix| { + if (std.mem.startsWith(u8, namespace, prefix)) return index; + } + } + unreachable; // the final shard's empty prefix matches every namespace +} + +/// Scans the framework source tree for files declaring top-level tests, +/// sorted by namespace so the generated filters — and therefore the +/// shard binaries' cache manifests — do not churn with directory +/// iteration order. src/primitives and src/tooling are separate modules +/// with their own test binaries; everything else under src/ belongs to +/// the framework module rooted at src/root.zig. +fn desktopTestFiles(b: *std.Build) []const DesktopTestFile { + const gpa = b.allocator; + const io = b.graph.io; + var files: std.ArrayList(DesktopTestFile) = .empty; + var src_dir = b.build_root.handle.openDir(io, "src", .{ .iterate = true }) catch |err| + std.debug.panic("framework test shards: unable to open src/: {s}", .{@errorName(err)}); + defer src_dir.close(io); + var walker = src_dir.walk(gpa) catch @panic("OOM"); + defer walker.deinit(); + while (walker.next(io) catch |err| + std.debug.panic("framework test shards: unable to walk src/: {s}", .{@errorName(err)})) |entry| + { + if (entry.kind == .directory) { + if (entry.depth() == 1 and + (std.mem.eql(u8, entry.basename, "primitives") or std.mem.eql(u8, entry.basename, "tooling"))) + { + walker.leave(io); + } + continue; + } + if (entry.kind != .file or !std.mem.endsWith(u8, entry.basename, ".zig")) continue; + const source = entry.dir.readFileAlloc(io, entry.basename, gpa, .limited(16 * 1024 * 1024)) catch |err| + std.debug.panic("framework test shards: unable to read src/{s}: {s}", .{ entry.path, @errorName(err) }); + var named_tests: std.ArrayList([]const u8) = .empty; + var has_tests = false; + var lines = std.mem.splitScalar(u8, source, '\n'); + while (lines.next()) |line| { + // Only column-zero declarations: a test nested inside a + // container would carry that container's namespace, which + // these filters would not match. + if (!std.mem.startsWith(u8, line, "test ")) continue; + has_tests = true; + const rest = line["test ".len..]; + if (rest.len > 0 and rest[0] == '"') { + if (std.mem.indexOfScalar(u8, rest[1..], '"')) |end| { + named_tests.append(gpa, rest[1 .. 1 + end]) catch @panic("OOM"); + } + } + } + if (!has_tests) continue; + const namespace = gpa.dupe(u8, entry.path[0 .. entry.path.len - ".zig".len]) catch @panic("OOM"); + std.mem.replaceScalar(u8, namespace, std.fs.path.sep, '.'); + files.append(gpa, .{ .namespace = namespace, .named_tests = named_tests.items }) catch @panic("OOM"); + } + const sorted = files.items; + std.mem.sort(DesktopTestFile, sorted, {}, struct { + fn lessThan(_: void, lhs: DesktopTestFile, rhs: DesktopTestFile) bool { + return std.mem.lessThan(u8, lhs.namespace, rhs.namespace); + } + }.lessThan); + return sorted; } fn addTestStep(b: *std.Build, name: []const u8, description: []const u8, artifact: *std.Build.Step.Compile) void { @@ -836,14 +2459,55 @@ fn addTestStep(b: *std.Build, name: []const u8, description: []const u8, artifac step.dependOn(&b.addRunArtifact(artifact).step); } -fn addExampleTestStep(b: *std.Build, group: *std.Build.Step, name: []const u8, description: []const u8, example_path: []const u8) void { - const run = b.addSystemCommand(&.{ "zig", "build", "test", "-Dplatform=null" }); +/// How an example's build is driven. `managed` examples carry only +/// app.zon + src (+ assets): the `native` CLI synthesizes their build +/// graph, so their suites run through the CLI verbs — exactly what a user +/// gets from `native init`. `owned` examples keep a build.zig of their own +/// (extra steps or flags the generated graph does not provide) and are +/// driven through plain in-dir `zig build`. +const ExampleBuildShape = enum { managed, owned }; + +fn addExampleTestStep(b: *std.Build, cli_exe: *std.Build.Step.Compile, group: *std.Build.Step, name: []const u8, description: []const u8, example_path: []const u8, shape: ExampleBuildShape) void { + const run = switch (shape) { + .owned => b.addSystemCommand(&.{ "zig", "build", "test", "-Dplatform=null" }), + .managed => managedExampleRun(b, cli_exe, &.{ "test", "-Dplatform=null" }), + }; run.setCwd(b.path(example_path)); + // Every example suite must actually run every time: the child build owns + // its own caching, and this outer step's argv never changes when example + // or framework sources do, so letting the step cache would skip real + // tests. But a side-effect run that also inherits stdio holds the build + // runner's global stderr lock for the child's entire lifetime, which + // executes the example suites strictly one at a time. Capturing both + // streams keeps the always-run semantics while releasing that lock, so + // independent examples run concurrently (bounded by the runner's job + // pool, one worker per CPU — cold child builds peak well under 1 GB + // each, so a machine-wide pool of them fits in memory). On failure the + // captured stderr is reported under this step's name, so a failing + // example still names itself. + run.has_side_effects = true; + _ = run.captureStdOut(.{}); + _ = run.captureStdErr(.{}); + // Concurrent children all spawn the same binary, so carry the step name + // into the run: a failure line then reads "test-example- failure" + // instead of nineteen indistinguishable "run exe native" entries. + run.setName(name); const step = b.step(name, description); step.dependOn(&run.step); group.dependOn(&run.step); } +/// Run a `native` CLI verb (argv tail) against a managed example. The CLI +/// artifact runs from the build cache, where its executable location does +/// not reveal the framework checkout, so NATIVE_SDK_PATH pins the generated +/// graph's framework dependency to this repository. +fn managedExampleRun(b: *std.Build, cli_exe: *std.Build.Step.Compile, argv_tail: []const []const u8) *std.Build.Step.Run { + const run = b.addRunArtifact(cli_exe); + run.addArgs(argv_tail); + run.setEnvironmentVariable("NATIVE_SDK_PATH", b.pathFromRoot(".")); + return run; +} + fn addLayoutCheckStep(b: *std.Build, group: *std.Build.Step, name: []const u8, description: []const u8, paths: []const []const u8) void { const step = b.step(name, description); for (paths) |path| { @@ -862,6 +2526,12 @@ fn addFileContainsCheckStep(b: *std.Build, checker: *std.Build.Step.Compile, gro const step = b.step(name, description); for (checks) |check_value| { const check = b.addRunArtifact(checker); + // The checked paths are relative to this build script. Run steps + // otherwise inherit the invoking process's cwd, and `zig build test` + // is also invoked from zero-config app directories that resolve up + // to this build.zig (scaffolded workspaces, examples) — from there + // the relative paths would point into the app, not the repo. + check.setCwd(b.path(".")); check.addArg(check_value.path); check.addArg(check_value.pattern); step.dependOn(&check.step); @@ -898,6 +2568,16 @@ fn defaultCefDir(platform: PlatformOption, configured: []const u8) []const u8 { }; } +/// CEF dir for a sub-build whose cwd is an example directory (two levels +/// below the repo root). Relative paths are resolved by the example's +/// build against its own root, so the repo-root-relative default must be +/// rebased; absolute overrides pass through, but the example build.zigs +/// reject them (b.path panics), so callers should prefer relative paths. +fn exampleCefDir(b: *std.Build, cef_dir: []const u8) []const u8 { + if (std.fs.path.isAbsolute(cef_dir)) return cef_dir; + return b.fmt("../../{s}", .{cef_dir}); +} + fn webEngineFromBuildOption(option: WebEngineOption) web_engine_tool.Engine { return switch (option) { .system => .system, diff --git a/build.zig.zon b/build.zig.zon index 42e69966..2e190d44 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ - .name = .zero_native, - .fingerprint = 0x338d08a1e3dd81aa, + .name = .native_sdk, + .fingerprint = 0xc309966142f33087, .version = "0.1.0", .minimum_zig_version = "0.16.0", .dependencies = .{}, @@ -11,6 +11,7 @@ "SECURITY.md", "app.zon", "assets", + "build", "build.zig", "build.zig.zon", "docs", diff --git a/build/app.zig b/build/app.zig new file mode 100644 index 00000000..ca3c806c --- /dev/null +++ b/build/app.zig @@ -0,0 +1,716 @@ +//! Framework build helper: `addApp` gives a markup/builder app a complete +//! build (exe, run, test) from a ~5-line build.zig. The app supplies +//! src/main.zig, app.zon, and assets; the runner and all framework modules +//! come from the native-sdk dependency. + +const std = @import("std"); + +const PlatformOption = enum { + auto, + null, + macos, + linux, + windows, +}; + +const TraceOption = enum { + off, + events, + runtime, + all, +}; + +const WebEngineOption = enum { + system, + chromium, +}; + +pub const AppOptions = struct { + name: []const u8, + /// App entry point; defaults to src/main.zig (relative to `app_root`). + main: []const u8 = "src/main.zig", + /// Root of the app source tree, relative to the build root. "." for a + /// build.zig that lives in the app directory (every ejected app). The + /// CLI's generated build graph under `/.native/build/` passes + /// "../.." so `src/`, `app.zon`, and `assets/` keep resolving in the + /// app directory rather than the cache directory. + app_root: []const u8 = ".", +}; + +/// The `native_sdk_app_*` C ABI every embed static library exports. +pub const mobile_export_symbol_names = [_][]const u8{ + "native_sdk_app_create", + "native_sdk_app_destroy", + "native_sdk_app_start", + "native_sdk_app_activate", + "native_sdk_app_deactivate", + "native_sdk_app_stop", + "native_sdk_app_resize", + "native_sdk_app_viewport", + "native_sdk_app_viewport_state", + "native_sdk_app_gpu_frame_state", + "native_sdk_app_text_input_state", + "native_sdk_app_set_text_measure", + "native_sdk_app_set_audio_service", + "native_sdk_app_audio_event", + "native_sdk_app_set_image_service", + "native_sdk_app_set_automation_dir", + "native_sdk_app_touch", + "native_sdk_app_scroll", + "native_sdk_app_key", + "native_sdk_app_text", + "native_sdk_app_ime", + "native_sdk_app_command", + "native_sdk_app_frame", + "native_sdk_app_chrome_tab_count", + "native_sdk_app_chrome_tab_at", + "native_sdk_app_chrome_primary_action", + "native_sdk_app_chrome_selected_tab", + "native_sdk_app_chrome_navigation_depth", + "native_sdk_app_chrome_navigation_back_command", + "native_sdk_app_chrome_icon_pixels", + "native_sdk_app_set_form_factor", + "native_sdk_app_set_chrome_tabs_projected", + "native_sdk_app_set_asset_root", + "native_sdk_app_set_asset_entry", + "native_sdk_app_last_command_count", + "native_sdk_app_last_command_name", + "native_sdk_app_last_error_name", + "native_sdk_app_widget_semantics_count", + "native_sdk_app_widget_semantics_at", + "native_sdk_app_widget_semantics_by_id", + "native_sdk_app_widget_text_geometry", + "native_sdk_app_widget_action", + "native_sdk_app_render_pixel_size", + "native_sdk_app_render_pixels", + "native_sdk_app_render_pixels_damage", +}; + +pub const MobileSceneOption = enum { + /// The user app's UiApp on a gpu_surface view (window 1, + /// "mobile-surface"), pumped by the host's frame callback. + canvas, + /// The fixed WebView shell the ios/android/mobile-shell examples embed + /// today; the app module is not compiled in. + webview, +}; + +pub const MobileLibOptions = struct { + name: []const u8, + /// Mobile app entry (the `"app"` module the embed host drives); must + /// declare `Model`, `Msg`, `initModel`, and `mobileOptions` — see + /// `src/embed/ui_host.zig`. Ignored for `.scene = .webview`. + main: []const u8 = "src/main.zig", + scene: MobileSceneOption = .canvas, +}; + +/// Mobile counterpart of `addApp`: produce the embed static library +/// (`native_sdk_app_*` C ABI) compiled with the user's UiApp. Call it from +/// a standalone build.zig (it registers the standard `target`/`optimize` +/// options itself). +pub fn addMobileLib(b: *std.Build, dep: *std.Build.Dependency, options: MobileLibOptions) void { + const target = nativeSdkTarget(b); + const optimize_request = b.option(std.builtin.OptimizeMode, "optimize", "Prioritize performance, safety, or binary size"); + const optimize = exampleOptimizeMode(b, optimize_request, .Debug); + addMobileLibWithTarget(b, dep, target, optimize, options); +} + +/// The mobile-lib wiring behind `addMobileLib`, for builds that already +/// resolved `target`/`optimize` (`addAppArtifacts` registers the `lib` +/// step through this for iOS/Android targets, so every standard app — +/// generated graph or ejected `addApp` — can produce the embed library +/// with nothing but `-Dtarget`). +fn addMobileLibWithTarget(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, options: MobileLibOptions) void { + const native_sdk_mod = nativeSdkModule(b, dep, target, optimize); + // Android hosts load the embed lib inside a shared object + // (System.loadLibrary / NativeActivity), so every object must be PIC — + // without it Zig emits local-exec TLS relocations (R_AARCH64_TLSLE_*) + // that the NDK linker rejects when producing the shim .so. Imported + // modules leave `pic` null and inherit this from the root module. + const pic: ?bool = if (target.result.abi.isAndroid()) true else null; + const exports_mod = b.createModule(.{ + .root_source_file = dep.path(switch (options.scene) { + .canvas => "src/embed/app_exports.zig", + .webview => "src/embed/c_exports.zig", + }), + .target = target, + .optimize = optimize, + .pic = pic, + }); + exports_mod.addImport("native_sdk", native_sdk_mod); + if (options.scene == .canvas) { + const app_mod = localModule(b, target, optimize, options.main); + app_mod.addImport("native_sdk", native_sdk_mod); + exports_mod.addImport("app", app_mod); + } + exports_mod.export_symbol_names = &mobile_export_symbol_names; + + const lib = b.addLibrary(.{ + .linkage = .static, + .name = options.name, + .root_module = exports_mod, + // The embed C ABI (`native_sdk_app_viewport`) is exactly the + // f32-heavy SysV signature Zig 0.16.0's self-hosted x86_64 backend + // miscompiles (see useLlvmWorkaround in the framework build.zig): + // clang-compiled hosts calling a self-hosted Debug lib receive + // corrupted inset/keyboard floats on x86_64 (Android emulators, + // Intel simulators). Force LLVM there; Release already uses it. + .use_llvm = useLlvmWorkaround(target), + }); + b.installArtifact(lib); + + const lib_step = b.step("lib", "Build the mobile embed static library"); + lib_step.dependOn(&b.addInstallArtifact(lib, .{}).step); +} + +/// The pieces `addApp` wires, for callers that extend the standard app +/// build (extra native sources, frameworks, post-build steps such as +/// entitlement signing). `install` is the artifact-install step behind the +/// default `zig build`; append dependencies to it and to `run` to order +/// work between the emitted binary and its consumers. +pub const AppArtifacts = struct { + exe: *std.Build.Step.Compile, + tests: *std.Build.Step.Compile, + install: *std.Build.Step.InstallArtifact, + run: *std.Build.Step.Run, +}; + +pub fn addApp(b: *std.Build, dep: *std.Build.Dependency, app_options: AppOptions) void { + _ = addAppArtifacts(b, dep, app_options); +} + +pub fn addAppArtifacts(b: *std.Build, dep: *std.Build.Dependency, app_options: AppOptions) AppArtifacts { + const target = nativeSdkTarget(b); + const optimize_request = b.option(std.builtin.OptimizeMode, "optimize", "Prioritize performance, safety, or binary size"); + const optimize = exampleOptimizeMode(b, optimize_request, .Debug); + const app_optimize = exampleOptimizeMode(b, optimize_request, .ReleaseFast); + + // Mobile targets get the embed static library as a `lib` step: the + // artifact the toolkit-owned iOS host (and any hand-written shim) + // links, so `native dev|package --target ios` works against every + // standard app build — generated graph or ejected — with nothing but + // `-Dtarget`. Desktop targets keep the step absent. + if (target.result.os.tag == .ios or target.result.abi.isAndroid()) { + addMobileLibWithTarget(b, dep, target, optimize, .{ + .name = app_options.name, + .main = appPath(b, app_options.app_root, app_options.main), + }); + } + const platform_option = b.option(PlatformOption, "platform", "Desktop backend: auto, null, macos, linux, windows") orelse .auto; + const trace_option = b.option(TraceOption, "trace", "Trace output: off, events, runtime, all") orelse .events; + const debug_overlay = b.option(bool, "debug-overlay", "Enable debug overlay output") orelse false; + const automation_enabled = b.option(bool, "automation", "Enable Native SDK automation artifacts") orelse false; + const js_bridge_enabled = b.option(bool, "js-bridge", "Enable optional JavaScript bridge stubs") orelse false; + const web_engine_override = b.option(WebEngineOption, "web-engine", "Override app.zon web engine: system, chromium"); + const cef_dir_override = b.option([]const u8, "cef-dir", "Override CEF root directory for Chromium builds"); + const cef_auto_install_override = b.option(bool, "cef-auto-install", "Override app.zon CEF auto-install setting"); + const selected_platform: PlatformOption = switch (platform_option) { + .auto => if (target.result.os.tag == .macos) .macos else if (target.result.os.tag == .linux) .linux else if (target.result.os.tag == .windows) .windows else .null, + else => platform_option, + }; + if (selected_platform == .macos and target.result.os.tag != .macos) { + @panic("-Dplatform=macos requires a macOS target"); + } + if (selected_platform == .linux and target.result.os.tag != .linux) { + @panic("-Dplatform=linux requires a Linux target"); + } + if (selected_platform == .windows and target.result.os.tag != .windows) { + @panic("-Dplatform=windows requires a Windows target"); + } + const app_web_engine = appWebEngineConfig(b, app_options.app_root); + const web_engine = web_engine_override orelse app_web_engine.web_engine; + const cef_dir = cef_dir_override orelse defaultCefDir(selected_platform, app_web_engine.cef_dir); + const cef_auto_install = cef_auto_install_override orelse app_web_engine.cef_auto_install; + if (web_engine == .chromium and selected_platform != .macos) { + @panic("-Dweb-engine=chromium currently requires -Dplatform=macos"); + } + + const options = b.addOptions(); + options.addOption([]const u8, "platform", switch (selected_platform) { + .auto => unreachable, + .null => "null", + .macos => "macos", + .linux => "linux", + .windows => "windows", + }); + options.addOption([]const u8, "trace", @tagName(trace_option)); + options.addOption([]const u8, "web_engine", @tagName(web_engine)); + options.addOption(bool, "debug_overlay", debug_overlay); + options.addOption(bool, "automation", automation_enabled); + options.addOption(bool, "js_bridge", js_bridge_enabled); + const options_mod = options.createModule(); + + const app_mod = appModule(b, dep, target, app_optimize, app_options, options_mod); + const exe = b.addExecutable(.{ + .name = app_options.name, + .root_module = app_mod, + }); + linkPlatform(b, dep, target, app_mod, exe, selected_platform, web_engine, cef_dir, cef_auto_install); + const install = b.addInstallArtifact(exe, .{}); + b.getInstallStep().dependOn(&install.step); + + const run = b.addRunArtifact(exe); + addCefRuntimeRunFiles(b, target, run, exe, web_engine, cef_dir); + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run.step); + + const test_app_mod = if (app_optimize == optimize) app_mod else appModule(b, dep, target, optimize, app_options, options_mod); + const tests = b.addTest(.{ .root_module = test_app_mod, .use_llvm = useLlvmWorkaround(target) }); + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&b.addRunArtifact(tests).step); + + // `zig build model-contract`: reflect the app's Model/Msg into + // zig-out/model-contract.zon so `native check` can verify markup + // bindings against the app's real surface without compiling the app. + // The artifact carries a hash over the app's Zig sources; the checker + // degrades to structural checking when it goes stale. Apps without a + // pub Model/Msg pair make this a silent no-op. The test step refreshes + // the artifact too, so CI-checked apps always hold a fresh one. + const contract_root = b.addWriteFiles().add("model_contract_emit.zig", + \\//! Generated by the app build: emits the model contract artifact + \\//! (see the toolkit's ui_markup_contract.zig). + \\const std = @import("std"); + \\const native_sdk = @import("native_sdk"); + \\const app = @import("app"); + \\ + \\pub fn main(init: std.process.Init) !void { + \\ try native_sdk.canvas.emitModelContractMain(app, init); + \\} + \\ + ); + // The emit root must share the app module's native_sdk instance so + // the Msg payload types it classifies are the same types the app + // declares its variants with. + const contract_mod = b.createModule(.{ + .root_source_file = contract_root, + .target = target, + .optimize = optimize, + }); + contract_mod.addImport("app", test_app_mod); + if (test_app_mod.import_table.get("native_sdk")) |sdk_mod| { + contract_mod.addImport("native_sdk", sdk_mod); + } + const contract_exe = b.addExecutable(.{ + .name = b.fmt("{s}-model-contract", .{app_options.name}), + .root_module = contract_mod, + .use_llvm = useLlvmWorkaround(target), + }); + const contract_run = b.addRunArtifact(contract_exe); + contract_run.setCwd(b.path(app_options.app_root)); + contract_run.addArgs(&.{ "--src", "src", "--out", "zig-out/model-contract.zon" }); + contract_run.has_side_effects = true; + const contract_step = b.step("model-contract", "Emit zig-out/model-contract.zon for `native check`"); + contract_step.dependOn(&contract_run.step); + test_step.dependOn(&contract_run.step); + + // `zig build package`: bundle the built binary through the `native` + // CLI (built from the native_sdk dependency), so a scaffolded app can + // package itself without locating the CLI by hand. + const host_os = b.graph.host.result.os.tag; + const package_target: ?[]const u8 = switch (host_os) { + .macos => "macos", + .linux => "linux", + .windows => "windows", + else => null, + }; + if (package_target) |package_target_name| { + const package_run = b.addRunArtifact(dep.artifact("native")); + package_run.addArgs(&.{ "package", "--target", package_target_name, "--manifest", "app.zon", "--output" }); + package_run.addArg(if (host_os == .macos) + b.fmt("zig-out/package/{s}.app", .{app_options.name}) + else + b.fmt("zig-out/package/{s}", .{package_target_name})); + package_run.addArg("--binary"); + package_run.addFileArg(exe.getEmittedBin()); + // The archive and report names carry an optimize label; this + // build graph knows the packaged binary's REAL mode, so forward + // it instead of letting the CLI assume one. + package_run.addArgs(&.{ "--optimize", @tagName(app_optimize) }); + package_run.has_side_effects = true; + const package_step = b.step("package", "Create a distributable package via the native CLI"); + package_step.dependOn(&package_run.step); + } + + return .{ .exe = exe, .tests = tests, .install = install, .run = run }; +} + +/// Zig 0.16.0's self-hosted x86_64 backend miscompiles the SysV C calling +/// convention for f32-heavy signatures with interleaved pointer arguments +/// (`native_sdk_app_viewport`: 11 f32s + 2 pointers): both the caller and +/// the callee place/read the wrong registers and stack slots, so safe-area +/// insets arrive as garbage on x86_64 Debug builds while every LLVM-backed +/// build is correct. Minimal repro (fails under `zig test`, passes with +/// `-fllvm` on x86_64-linux): +/// +/// fn take(a: ?*anyopaque, w: f32, h: f32, s: f32, p: ?*anyopaque, +/// t: f32, r: f32, bo: f32, l: f32, kt: f32, kr: f32, kb: f32, +/// kl: f32) callconv(.c) void { ... } +/// +/// Force the LLVM backend on x86_64 until the upstream backend is fixed; +/// Release modes already default to LLVM, so this only changes Debug. +pub fn useLlvmWorkaround(target: std.Build.ResolvedTarget) ?bool { + return if (target.result.cpu.arch == .x86_64) true else null; +} + +fn exampleOptimizeMode(b: *std.Build, requested: ?std.builtin.OptimizeMode, default_mode: std.builtin.OptimizeMode) std.builtin.OptimizeMode { + if (requested) |mode| return mode; + return switch (b.release_mode) { + .off => default_mode, + .any, .fast => .ReleaseFast, + .safe => .ReleaseSafe, + .small => .ReleaseSmall, + }; +} + +fn appModule(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, app_options: AppOptions, options_mod: *std.Build.Module) *std.Build.Module { + const native_sdk_mod = nativeSdkModule(b, dep, target, optimize); + const runner_mod = b.createModule(.{ + .root_source_file = dep.path("src/app_runner/root.zig"), + .target = target, + .optimize = optimize, + }); + runner_mod.addImport("native_sdk", native_sdk_mod); + runner_mod.addImport("build_options", options_mod); + runner_mod.addImport("app_manifest_zon", b.createModule(.{ .root_source_file = b.path(appPath(b, app_options.app_root, "app.zon")) })); + + const app_mod = localModule(b, target, optimize, appPath(b, app_options.app_root, app_options.main)); + app_mod.addImport("native_sdk", native_sdk_mod); + app_mod.addImport("runner", runner_mod); + return app_mod; +} + +fn nativeSdkTarget(b: *std.Build) std.Build.ResolvedTarget { + const target = b.standardTargetOptions(.{}); + if (target.result.os.tag != .macos) return target; + + if (b.sysroot == null) { + b.sysroot = macosSdkPath(b) orelse b.sysroot; + } + + var query = target.query; + query.os_tag = .macos; + query.os_version_min = .{ .semver = .{ .major = 11, .minor = 0, .patch = 0 } }; + return b.resolveTargetQuery(query); +} + +fn macosSdkPath(b: *std.Build) ?[]const u8 { + if (b.graph.environ_map.get("SDKROOT")) |sdkroot| { + if (sdkroot.len > 0) return sdkroot; + } + + const result = std.process.run(b.allocator, b.graph.io, .{ + .argv = &.{ "xcrun", "--sdk", "macosx", "--show-sdk-path" }, + .stdout_limit = .limited(4096), + .stderr_limit = .limited(4096), + }) catch return null; + defer b.allocator.free(result.stderr); + if (result.term != .exited or result.term.exited != 0) { + b.allocator.free(result.stdout); + return null; + } + return std.mem.trimEnd(u8, result.stdout, "\r\n"); +} + +fn localModule(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, path: []const u8) *std.Build.Module { + return b.createModule(.{ + .root_source_file = b.path(path), + .target = target, + .optimize = optimize, + }); +} + +fn nativeSdkModule(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *std.Build.Module { + const geometry_mod = externalModule(b, dep, target, optimize, "src/primitives/geometry/root.zig"); + const assets_mod = externalModule(b, dep, target, optimize, "src/primitives/assets/root.zig"); + const app_dirs_mod = externalModule(b, dep, target, optimize, "src/primitives/app_dirs/root.zig"); + const trace_mod = externalModule(b, dep, target, optimize, "src/primitives/trace/root.zig"); + const app_manifest_mod = externalModule(b, dep, target, optimize, "src/primitives/app_manifest/root.zig"); + const diagnostics_mod = externalModule(b, dep, target, optimize, "src/primitives/diagnostics/root.zig"); + const platform_info_mod = externalModule(b, dep, target, optimize, "src/primitives/platform_info/root.zig"); + const json_mod = externalModule(b, dep, target, optimize, "src/primitives/json/root.zig"); + const canvas_mod = externalModule(b, dep, target, optimize, "src/primitives/canvas/root.zig"); + canvas_mod.addImport("geometry", geometry_mod); + canvas_mod.addImport("json", json_mod); + const debug_mod = externalModule(b, dep, target, optimize, "src/debug/root.zig"); + debug_mod.addImport("app_dirs", app_dirs_mod); + debug_mod.addImport("trace", trace_mod); + + const native_sdk_mod = externalModule(b, dep, target, optimize, "src/root.zig"); + native_sdk_mod.addImport("geometry", geometry_mod); + native_sdk_mod.addImport("assets", assets_mod); + native_sdk_mod.addImport("app_dirs", app_dirs_mod); + native_sdk_mod.addImport("trace", trace_mod); + native_sdk_mod.addImport("app_manifest", app_manifest_mod); + native_sdk_mod.addImport("diagnostics", diagnostics_mod); + native_sdk_mod.addImport("platform_info", platform_info_mod); + native_sdk_mod.addImport("json", json_mod); + native_sdk_mod.addImport("canvas", canvas_mod); + return native_sdk_mod; +} + +fn externalModule(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, path: []const u8) *std.Build.Module { + return b.createModule(.{ + .root_source_file = dep.path(path), + .target = target, + .optimize = optimize, + }); +} + +// -fno-sanitize=builtin on every ObjC compile: Zig 0.16.0's Debug UBSan +// aborts any process whose first dispatch_once runs — the macOS SDK's +// inline `_dispatch_once` ends in `__builtin_assume(*predicate == ~0l)` +// (dispatch/once.h), Zig's bundled clang instruments that builtin, and the +// check fires spuriously at startup; zig's ubsan_rt then cannot even decode +// the report ("invalid enum value" / "passing zero to clz()" panics). +// Reproduced with a 10-line `zig cc` program against both the 14.5 and +// 26.0 SDKs. Release builds never hit it (no UBSan), which is why only +// Debug-built examples (standardOptimizeOption default) crashed. +fn linkPlatform(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, app_mod: *std.Build.Module, exe: *std.Build.Step.Compile, platform: PlatformOption, web_engine: WebEngineOption, cef_dir: []const u8, cef_auto_install: bool) void { + if (platform == .macos) { + switch (web_engine) { + .system => { + const sdk_include = if (b.sysroot) |sysroot| b.fmt("-I{s}/usr/include", .{sysroot}) else ""; + const flags: []const []const u8 = if (b.sysroot) |sysroot| &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC", "-mmacosx-version-min=11.0", "-isysroot", sysroot, sdk_include } else &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC", "-mmacosx-version-min=11.0" }; + app_mod.addCSourceFile(.{ .file = dep.path("src/platform/macos/appkit_host.m"), .flags = flags }); + app_mod.linkFramework("WebKit", .{}); + }, + .chromium => { + const cef_check = addCefCheck(b, target, cef_dir); + if (cef_auto_install) { + const cef_auto = b.addSystemCommand(&.{ "native", "cef", "install", "--dir", cef_dir }); + cef_check.step.dependOn(&cef_auto.step); + } + exe.step.dependOn(&cef_check.step); + const include_arg = b.fmt("-I{s}", .{cef_dir}); + const define_arg = b.fmt("-DNATIVE_SDK_CEF_DIR=\"{s}\"", .{cef_dir}); + // The SDK's usr/include must stay a system include dir (searched after zig's + // bundled libc++/libc headers). A plain -I shadows libc++'s / + // wrappers in ObjC++ and surfaces SDK nullability gaps as a diagnostic flood. + const sdk_include = if (b.sysroot) |sysroot| b.fmt("-isystem{s}/usr/include", .{sysroot}) else ""; + const flags: []const []const u8 = if (b.sysroot) |sysroot| &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC++", "-std=c++17", "-stdlib=libc++", "-mmacosx-version-min=11.0", "-isysroot", sysroot, sdk_include, include_arg, define_arg } else &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC++", "-std=c++17", "-stdlib=libc++", "-mmacosx-version-min=11.0", include_arg, define_arg }; + app_mod.addCSourceFile(.{ .file = dep.path("src/platform/macos/cef_host.mm"), .flags = flags }); + app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.a", .{cef_dir}))); + app_mod.addFrameworkPath(b.path(b.fmt("{s}/Release", .{cef_dir}))); + app_mod.linkFramework("Chromium Embedded Framework", .{}); + app_mod.addRPath(.{ .cwd_relative = "@executable_path/Frameworks" }); + }, + } + if (b.sysroot) |sysroot| { + app_mod.addFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ sysroot, "System/Library/Frameworks" }) }); + } + app_mod.linkFramework("AppKit", .{}); + // The audio playback service (the AppKit host's single AVPlayer). + app_mod.linkFramework("AVFoundation", .{}); + // Spectrum analysis of the app's own playback: the MediaToolbox + // audio tap hands the player's PCM to the host, and Accelerate + // (vDSP) turns it into band magnitudes. + app_mod.linkFramework("MediaToolbox", .{}); + app_mod.linkFramework("Accelerate", .{}); + app_mod.linkFramework("Foundation", .{}); + app_mod.linkFramework("CoreText", .{}); + app_mod.linkFramework("UniformTypeIdentifiers", .{}); + app_mod.linkFramework("Security", .{}); + app_mod.linkFramework("Metal", .{}); + app_mod.linkFramework("QuartzCore", .{}); + app_mod.linkSystemLibrary("c", .{}); + if (web_engine == .chromium) app_mod.linkSystemLibrary("c++", .{}); + } else if (platform == .linux) { + switch (web_engine) { + .system => { + app_mod.addCSourceFile(.{ .file = dep.path("src/platform/linux/gtk_host.c"), .flags = &.{} }); + app_mod.linkSystemLibrary("gtk4", .{}); + app_mod.linkSystemLibrary("webkitgtk-6.0", .{}); + app_mod.linkSystemLibrary("dl", .{}); + }, + .chromium => { + const cef_check = addCefCheck(b, target, cef_dir); + if (cef_auto_install) { + const cef_auto = b.addSystemCommand(&.{ "native", "cef", "install", "--dir", cef_dir }); + cef_check.step.dependOn(&cef_auto.step); + } + exe.step.dependOn(&cef_check.step); + 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/linux/cef_host.cpp"), .flags = &.{ "-std=c++17", include_arg, define_arg } }); + app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.a", .{cef_dir}))); + app_mod.addLibraryPath(b.path(b.fmt("{s}/Release", .{cef_dir}))); + app_mod.linkSystemLibrary("cef", .{}); + app_mod.addRPath(.{ .cwd_relative = "$ORIGIN" }); + }, + } + app_mod.linkSystemLibrary("c", .{}); + if (web_engine == .chromium) app_mod.linkSystemLibrary("stdc++", .{}); + } else if (platform == .windows) { + // Common-controls v6 side-by-side dependency: without this + // manifest the loader binds the system-default v5 assembly, which + // renders classic-styled controls and lacks the v6-only exports. + exe.win32_manifest = dep.path("assets/native-sdk.manifest"); + switch (web_engine) { + .system => app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/webview2_host.cpp"), .flags = &.{"-std=c++17"} }), + .chromium => { + const cef_check = addCefCheck(b, target, cef_dir); + if (cef_auto_install) { + const cef_auto = b.addSystemCommand(&.{ "native", "cef", "install", "--dir", cef_dir }); + cef_check.step.dependOn(&cef_auto.step); + } + exe.step.dependOn(&cef_check.step); + 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.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}))); + }, + } + app_mod.linkSystemLibrary("c", .{}); + app_mod.linkSystemLibrary("c++", .{}); + app_mod.linkSystemLibrary("user32", .{}); + app_mod.linkSystemLibrary("gdi32", .{}); + app_mod.linkSystemLibrary("imm32", .{}); + app_mod.linkSystemLibrary("comctl32", .{}); + app_mod.linkSystemLibrary("ole32", .{}); + app_mod.linkSystemLibrary("oleacc", .{}); + app_mod.linkSystemLibrary("shell32", .{}); + // The audio backend: Media Foundation (session + source resolver + // + streaming audio renderer) and WinHTTP (the cache fill). + app_mod.linkSystemLibrary("mf", .{}); + app_mod.linkSystemLibrary("mfplat", .{}); + app_mod.linkSystemLibrary("winhttp", .{}); + if (web_engine == .chromium) app_mod.linkSystemLibrary("libcef", .{}); + } +} + +fn addCefRuntimeRunFiles(b: *std.Build, target: std.Build.ResolvedTarget, run: *std.Build.Step.Run, exe: *std.Build.Step.Compile, web_engine: WebEngineOption, cef_dir: []const u8) void { + if (web_engine != .chromium) return; + if (target.result.os.tag != .macos) return; + const copy = b.addSystemCommand(&.{ + "sh", "-c", + b.fmt( + \\set -e + \\exe="$0" + \\exe_dir="$(dirname "$exe")" + \\rm -rf "zig-out/Frameworks/Chromium Embedded Framework.framework" "zig-out/bin/Frameworks/Chromium Embedded Framework.framework" ".zig-cache/o/Frameworks/Chromium Embedded Framework.framework" && + \\mkdir -p "zig-out/Frameworks" "zig-out/bin/Frameworks" ".zig-cache/o/Frameworks" "$exe_dir" && + \\cp -R "{s}/Release/Chromium Embedded Framework.framework" "zig-out/Frameworks/" && + \\cp -R "{s}/Release/Chromium Embedded Framework.framework" "zig-out/bin/Frameworks/" && + \\cp -R "{s}/Release/Chromium Embedded Framework.framework" ".zig-cache/o/Frameworks/" && + \\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/libEGL.dylib" "$exe_dir/" && + \\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/libGLESv2.dylib" "$exe_dir/" && + \\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/libvk_swiftshader.dylib" "$exe_dir/" && + \\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/vk_swiftshader_icd.json" "$exe_dir/" + , .{ cef_dir, cef_dir, cef_dir, cef_dir, cef_dir, cef_dir, cef_dir }), + }); + copy.addFileArg(exe.getEmittedBin()); + run.step.dependOn(©.step); +} + +fn addCefCheck(b: *std.Build, target: std.Build.ResolvedTarget, cef_dir: []const u8) *std.Build.Step.Run { + const script = switch (target.result.os.tag) { + .macos => b.fmt( + \\test -f "{s}/include/cef_app.h" && + \\test -d "{s}/Release/Chromium Embedded Framework.framework" && + \\test -f "{s}/libcef_dll_wrapper/libcef_dll_wrapper.a" || {{ + \\ echo "missing CEF dependency for -Dweb-engine=chromium" >&2 + \\ echo "Fix with: native cef install --dir {s}" >&2 + \\ exit 1 + \\}} + , .{ cef_dir, cef_dir, cef_dir, cef_dir }), + .linux => b.fmt( + \\test -f "{s}/include/cef_app.h" && + \\test -f "{s}/Release/libcef.so" && + \\test -f "{s}/libcef_dll_wrapper/libcef_dll_wrapper.a" || {{ + \\ echo "missing CEF dependency for -Dweb-engine=chromium" >&2 + \\ echo "Fix with: native cef install --dir {s}" >&2 + \\ exit 1 + \\}} + , .{ cef_dir, cef_dir, cef_dir, cef_dir }), + .windows => b.fmt( + \\test -f "{s}/include/cef_app.h" && + \\test -f "{s}/Release/libcef.dll" && + \\test -f "{s}/libcef_dll_wrapper/libcef_dll_wrapper.lib" || {{ + \\ echo "missing CEF dependency for -Dweb-engine=chromium" >&2 + \\ echo "Fix with: native cef install --dir {s}" >&2 + \\ exit 1 + \\}} + , .{ cef_dir, cef_dir, cef_dir, cef_dir }), + else => "echo unsupported CEF target >&2; exit 1", + }; + return b.addSystemCommand(&.{ "sh", "-c", script }); +} + +const AppWebEngineConfig = struct { + web_engine: WebEngineOption = .system, + cef_dir: []const u8 = "third_party/cef/macos", + cef_auto_install: bool = false, +}; + +fn defaultCefDir(platform: PlatformOption, configured: []const u8) []const u8 { + if (!std.mem.eql(u8, configured, "third_party/cef/macos")) return configured; + return switch (platform) { + .linux => "third_party/cef/linux", + .windows => "third_party/cef/windows", + else => configured, + }; +} + +/// Resolve an app-relative path against `app_root` (see AppOptions). Kept +/// lexical: `b.path` rejects absolute paths and the generated build graph +/// hands us "../..", which openat/b.path both resolve fine. +fn appPath(b: *std.Build, app_root: []const u8, sub_path: []const u8) []const u8 { + if (app_root.len == 0 or std.mem.eql(u8, app_root, ".")) return sub_path; + return b.pathJoin(&.{ app_root, sub_path }); +} + +fn appWebEngineConfig(b: *std.Build, app_root: []const u8) AppWebEngineConfig { + const source = b.build_root.handle.readFileAlloc(b.graph.io, appPath(b, app_root, "app.zon"), b.allocator, .limited(1024 * 1024)) catch return .{}; + var config: AppWebEngineConfig = .{}; + if (stringField(source, ".web_engine")) |value| { + config.web_engine = parseWebEngine(value) orelse .system; + } + if (objectSection(source, ".cef")) |cef| { + if (stringField(cef, ".dir")) |value| config.cef_dir = value; + if (boolField(cef, ".auto_install")) |value| config.cef_auto_install = value; + } + return config; +} + +fn parseWebEngine(value: []const u8) ?WebEngineOption { + if (std.mem.eql(u8, value, "system")) return .system; + if (std.mem.eql(u8, value, "chromium")) return .chromium; + return null; +} + +fn stringField(source: []const u8, field: []const u8) ?[]const u8 { + const field_index = std.mem.indexOf(u8, source, field) orelse return null; + const equals = std.mem.indexOfScalarPos(u8, source, field_index, '=') orelse return null; + const start_quote = std.mem.indexOfScalarPos(u8, source, equals, '"') orelse return null; + const end_quote = std.mem.indexOfScalarPos(u8, source, start_quote + 1, '"') orelse return null; + return source[start_quote + 1 .. end_quote]; +} + +fn objectSection(source: []const u8, field: []const u8) ?[]const u8 { + const field_index = std.mem.indexOf(u8, source, field) orelse return null; + const open = std.mem.indexOfScalarPos(u8, source, field_index, '{') orelse return null; + var depth: usize = 0; + var index = open; + while (index < source.len) : (index += 1) { + switch (source[index]) { + '{' => depth += 1, + '}' => { + depth -= 1; + if (depth == 0) return source[open + 1 .. index]; + }, + else => {}, + } + } + return null; +} + +fn boolField(source: []const u8, field: []const u8) ?bool { + const field_index = std.mem.indexOf(u8, source, field) orelse return null; + const equals = std.mem.indexOfScalarPos(u8, source, field_index, '=') orelse return null; + var index = equals + 1; + while (index < source.len and std.ascii.isWhitespace(source[index])) : (index += 1) {} + if (std.mem.startsWith(u8, source[index..], "true")) return true; + if (std.mem.startsWith(u8, source[index..], "false")) return false; + return null; +} diff --git a/changelog.d/README.md b/changelog.d/README.md new file mode 100644 index 00000000..a48adafe --- /dev/null +++ b/changelog.d/README.md @@ -0,0 +1,29 @@ +# Changelog fragments + +Agents and feature branches do not edit `CHANGELOG.md` directly — concurrent work would conflict on every merge. Instead, each change lands with a small fragment in this directory, and `scripts/changelog-merge.sh` folds all fragments into the `## Unreleased` section of `CHANGELOG.md` (typically during release prep, see RELEASING.md). + +## Writing a fragment + +Add `changelog.d/.md`, where `` names your change (e.g. `gpu-dashboard-smoke-budget.md`). The file holds a bullet or two for one changelog section: + +- The first line starts with a section tag: `feature:`, `improvement:`, or `fix:`, followed by the first bullet's text. +- Any further lines are additional bullets (start them with `- `; bare lines get `- ` prefixed for you). +- One tag per fragment. A change that touches multiple sections ships multiple fragments. +- Match the CHANGELOG voice: bold lead-in, then the story. One line per bullet — never hard-wrap. + +Example (`changelog.d/faster-frobnication.md`): + +``` +improvement: **Faster frobnication**: the frobnicator now memoizes per-frame, cutting rebuild time ~40% on the kanban example. +- **Frobnication telemetry**: automation snapshots report `frob_cache_hits=`. +``` + +Tags map to sections: `feature:` → `### New Features`, `improvement:` → `### Improvements`, `fix:` → `### Bug Fixes`. + +## Merging + +```sh +scripts/changelog-merge.sh +``` + +appends every fragment's bullets to the end of its section under `## Unreleased` (creating the section — or the whole `## Unreleased` block — when missing), then deletes the merged fragments. This `README.md` is never merged or deleted. The script refuses unknown tags loudly instead of guessing. diff --git a/docs/.gitignore b/docs/.gitignore index beb7067f..f19c70d2 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,3 +1,5 @@ node_modules/ .next/ next-env.d.ts +.next-gate/ +.next-agent/ diff --git a/docs/next.config.mjs b/docs/next.config.mjs index b6a206cf..c5b51105 100644 --- a/docs/next.config.mjs +++ b/docs/next.config.mjs @@ -1,10 +1,40 @@ import createMDX from "@next/mdx"; +import { createRequire } from "node:module"; -const withMDX = createMDX(); +// Resolve the plugin to an absolute path (still a string, so the config +// stays serializable for Turbopack). A bare "remark-gfm" is require()d +// from the MDX loader's own package context, which under pnpm's strict +// 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 withMDX = createMDX({ + options: { + // GFM is what gives .mdx pages pipe tables (plus autolinks and + // strikethrough) — without it, table markdown renders as a plain + // paragraph of pipes. + remarkPlugins: [[require.resolve("remark-gfm")]], + }, +}); /** @type {import('next').NextConfig} */ const nextConfig = { pageExtensions: ["ts", "tsx", "md", "mdx"], + // CI-style builds set NEXT_DIST_DIR so `pnpm check` never shares .next + // with a running dev server (a shared dist dir corrupts the dev cache). + distDir: process.env.NEXT_DIST_DIR || ".next", + // The gate builds into .next-gate INSIDE this dir; without an ignore, + // the dev watcher sees every one of those build files land and + // recompiles continuously whenever a gate runs. + watchOptions: { + ignored: ["**/.next-gate/**", "**/.next-check/**"], + }, + async redirects() { + return [ + // The Philosophy page became the Introduction, the opening page of the docs. + { source: "/philosophy", destination: "/introduction", permanent: true }, + ]; + }, }; export default withMDX(nextConfig); diff --git a/docs/package.json b/docs/package.json index 82bd845b..a5714c1a 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,5 +1,5 @@ { - "name": "@zero-native/docs", + "name": "@native-sdk/docs", "version": "0.0.0", "private": true, "type": "module", @@ -22,6 +22,7 @@ "radix-ui": "^1.4.3", "react": "^19", "react-dom": "^19", + "remark-gfm": "^4.0.1", "shiki": "^4.0.2", "tailwind-merge": "^3.5.0", "tailwindcss-animate": "^1.0.7" @@ -40,4 +41,4 @@ "postcss@<8.5.10": ">=8.5.10" } } -} +} \ No newline at end of file diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index b02cc1d1..37fb0e5e 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: react-dom: specifier: ^19 version: 19.2.6(react@19.2.6) + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 shiki: specifier: ^4.0.2 version: 4.0.2 @@ -1302,6 +1305,10 @@ packages: esast-util-from-js@2.0.1: resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + estree-util-attach-comments@3.0.0: resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} @@ -1460,9 +1467,33 @@ packages: resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} engines: {node: '>=16'} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + mdast-util-from-markdown@2.0.3: resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + mdast-util-mdx-expression@2.0.1: resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} @@ -1490,6 +1521,27 @@ packages: micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + micromark-extension-mdx-expression@3.0.1: resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} @@ -1712,6 +1764,9 @@ packages: rehype-recma@1.0.0: resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + remark-mdx@3.1.1: resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==} @@ -1721,6 +1776,9 @@ packages: remark-rehype@11.1.2: resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -3050,6 +3108,8 @@ snapshots: esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 + escape-string-regexp@5.0.0: {} + estree-util-attach-comments@3.0.0: dependencies: '@types/estree': 1.0.9 @@ -3228,6 +3288,15 @@ snapshots: markdown-extensions@2.0.0: {} + markdown-table@3.0.4: {} + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + mdast-util-from-markdown@2.0.3: dependencies: '@types/mdast': 4.0.4 @@ -3245,6 +3314,63 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 @@ -3346,6 +3472,64 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + micromark-extension-mdx-expression@3.0.1: dependencies: '@types/estree': 1.0.9 @@ -3746,6 +3930,17 @@ snapshots: transitivePeerDependencies: - supports-color + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + remark-mdx@3.1.1: dependencies: mdast-util-mdx: 3.0.0 @@ -3770,6 +3965,12 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + scheduler@0.27.0: {} semver@7.7.4: diff --git a/docs/public/components/accordion-dark.webp b/docs/public/components/accordion-dark.webp new file mode 100644 index 00000000..f7f35122 Binary files /dev/null and b/docs/public/components/accordion-dark.webp differ diff --git a/docs/public/components/accordion-hero-dark.webp b/docs/public/components/accordion-hero-dark.webp new file mode 100644 index 00000000..ed41647d Binary files /dev/null and b/docs/public/components/accordion-hero-dark.webp differ diff --git a/docs/public/components/accordion-hero-light.webp b/docs/public/components/accordion-hero-light.webp new file mode 100644 index 00000000..ec43316b Binary files /dev/null and b/docs/public/components/accordion-hero-light.webp differ diff --git a/docs/public/components/accordion-light.webp b/docs/public/components/accordion-light.webp new file mode 100644 index 00000000..c7a43614 Binary files /dev/null and b/docs/public/components/accordion-light.webp differ diff --git a/docs/public/components/alert-dark.webp b/docs/public/components/alert-dark.webp new file mode 100644 index 00000000..ac60a7ac Binary files /dev/null and b/docs/public/components/alert-dark.webp differ diff --git a/docs/public/components/alert-hero-dark.webp b/docs/public/components/alert-hero-dark.webp new file mode 100644 index 00000000..a6920279 Binary files /dev/null and b/docs/public/components/alert-hero-dark.webp differ diff --git a/docs/public/components/alert-hero-light.webp b/docs/public/components/alert-hero-light.webp new file mode 100644 index 00000000..795c6726 Binary files /dev/null and b/docs/public/components/alert-hero-light.webp differ diff --git a/docs/public/components/alert-light.webp b/docs/public/components/alert-light.webp new file mode 100644 index 00000000..6354dd24 Binary files /dev/null and b/docs/public/components/alert-light.webp differ diff --git a/docs/public/components/avatar-dark.webp b/docs/public/components/avatar-dark.webp new file mode 100644 index 00000000..9e4b4f6e Binary files /dev/null and b/docs/public/components/avatar-dark.webp differ diff --git a/docs/public/components/avatar-hero-dark.webp b/docs/public/components/avatar-hero-dark.webp new file mode 100644 index 00000000..71262fa6 Binary files /dev/null and b/docs/public/components/avatar-hero-dark.webp differ diff --git a/docs/public/components/avatar-hero-light.webp b/docs/public/components/avatar-hero-light.webp new file mode 100644 index 00000000..6cbc7621 Binary files /dev/null and b/docs/public/components/avatar-hero-light.webp differ diff --git a/docs/public/components/avatar-light.webp b/docs/public/components/avatar-light.webp new file mode 100644 index 00000000..14fddaa0 Binary files /dev/null and b/docs/public/components/avatar-light.webp differ diff --git a/docs/public/components/badge-dark.webp b/docs/public/components/badge-dark.webp new file mode 100644 index 00000000..261803ed Binary files /dev/null and b/docs/public/components/badge-dark.webp differ diff --git a/docs/public/components/badge-hero-dark.webp b/docs/public/components/badge-hero-dark.webp new file mode 100644 index 00000000..fe7a2c6d Binary files /dev/null and b/docs/public/components/badge-hero-dark.webp differ diff --git a/docs/public/components/badge-hero-light.webp b/docs/public/components/badge-hero-light.webp new file mode 100644 index 00000000..5adc7dad Binary files /dev/null and b/docs/public/components/badge-hero-light.webp differ diff --git a/docs/public/components/badge-light.webp b/docs/public/components/badge-light.webp new file mode 100644 index 00000000..46f0c8b4 Binary files /dev/null and b/docs/public/components/badge-light.webp differ diff --git a/docs/public/components/breadcrumb-dark.webp b/docs/public/components/breadcrumb-dark.webp new file mode 100644 index 00000000..8e199a4c Binary files /dev/null and b/docs/public/components/breadcrumb-dark.webp differ diff --git a/docs/public/components/breadcrumb-hero-dark.webp b/docs/public/components/breadcrumb-hero-dark.webp new file mode 100644 index 00000000..aba57380 Binary files /dev/null and b/docs/public/components/breadcrumb-hero-dark.webp differ diff --git a/docs/public/components/breadcrumb-hero-light.webp b/docs/public/components/breadcrumb-hero-light.webp new file mode 100644 index 00000000..805b67cd Binary files /dev/null and b/docs/public/components/breadcrumb-hero-light.webp differ diff --git a/docs/public/components/breadcrumb-light.webp b/docs/public/components/breadcrumb-light.webp new file mode 100644 index 00000000..0b1d5827 Binary files /dev/null and b/docs/public/components/breadcrumb-light.webp differ diff --git a/docs/public/components/bubble-dark.webp b/docs/public/components/bubble-dark.webp new file mode 100644 index 00000000..5bc2eb15 Binary files /dev/null and b/docs/public/components/bubble-dark.webp differ diff --git a/docs/public/components/bubble-hero-dark.webp b/docs/public/components/bubble-hero-dark.webp new file mode 100644 index 00000000..1992867a Binary files /dev/null and b/docs/public/components/bubble-hero-dark.webp differ diff --git a/docs/public/components/bubble-hero-light.webp b/docs/public/components/bubble-hero-light.webp new file mode 100644 index 00000000..ffa92b86 Binary files /dev/null and b/docs/public/components/bubble-hero-light.webp differ diff --git a/docs/public/components/bubble-light.webp b/docs/public/components/bubble-light.webp new file mode 100644 index 00000000..74778068 Binary files /dev/null and b/docs/public/components/bubble-light.webp differ diff --git a/docs/public/components/button-dark.webp b/docs/public/components/button-dark.webp new file mode 100644 index 00000000..7050b2ce Binary files /dev/null and b/docs/public/components/button-dark.webp differ diff --git a/docs/public/components/button-group-dark.webp b/docs/public/components/button-group-dark.webp new file mode 100644 index 00000000..963892bd Binary files /dev/null and b/docs/public/components/button-group-dark.webp differ diff --git a/docs/public/components/button-group-hero-dark.webp b/docs/public/components/button-group-hero-dark.webp new file mode 100644 index 00000000..23253bb0 Binary files /dev/null and b/docs/public/components/button-group-hero-dark.webp differ diff --git a/docs/public/components/button-group-hero-light.webp b/docs/public/components/button-group-hero-light.webp new file mode 100644 index 00000000..87caf3e1 Binary files /dev/null and b/docs/public/components/button-group-hero-light.webp differ diff --git a/docs/public/components/button-group-light.webp b/docs/public/components/button-group-light.webp new file mode 100644 index 00000000..84702613 Binary files /dev/null and b/docs/public/components/button-group-light.webp differ diff --git a/docs/public/components/button-hero-dark.webp b/docs/public/components/button-hero-dark.webp new file mode 100644 index 00000000..aa142f93 Binary files /dev/null and b/docs/public/components/button-hero-dark.webp differ diff --git a/docs/public/components/button-hero-light.webp b/docs/public/components/button-hero-light.webp new file mode 100644 index 00000000..5d24bf79 Binary files /dev/null and b/docs/public/components/button-hero-light.webp differ diff --git a/docs/public/components/button-icons-dark.webp b/docs/public/components/button-icons-dark.webp new file mode 100644 index 00000000..8dd0acb5 Binary files /dev/null and b/docs/public/components/button-icons-dark.webp differ diff --git a/docs/public/components/button-icons-light.webp b/docs/public/components/button-icons-light.webp new file mode 100644 index 00000000..a54f73ef Binary files /dev/null and b/docs/public/components/button-icons-light.webp differ diff --git a/docs/public/components/button-light.webp b/docs/public/components/button-light.webp new file mode 100644 index 00000000..6122495b Binary files /dev/null and b/docs/public/components/button-light.webp differ diff --git a/docs/public/components/button-sizes-dark.webp b/docs/public/components/button-sizes-dark.webp new file mode 100644 index 00000000..5c65e43e Binary files /dev/null and b/docs/public/components/button-sizes-dark.webp differ diff --git a/docs/public/components/button-sizes-light.webp b/docs/public/components/button-sizes-light.webp new file mode 100644 index 00000000..67ac7b65 Binary files /dev/null and b/docs/public/components/button-sizes-light.webp differ diff --git a/docs/public/components/button-states-dark.webp b/docs/public/components/button-states-dark.webp new file mode 100644 index 00000000..fb968b3b Binary files /dev/null and b/docs/public/components/button-states-dark.webp differ diff --git a/docs/public/components/button-states-light.webp b/docs/public/components/button-states-light.webp new file mode 100644 index 00000000..7302c5f9 Binary files /dev/null and b/docs/public/components/button-states-light.webp differ diff --git a/docs/public/components/card-dark.webp b/docs/public/components/card-dark.webp new file mode 100644 index 00000000..21525f92 Binary files /dev/null and b/docs/public/components/card-dark.webp differ diff --git a/docs/public/components/card-hero-dark.webp b/docs/public/components/card-hero-dark.webp new file mode 100644 index 00000000..8eb4f398 Binary files /dev/null and b/docs/public/components/card-hero-dark.webp differ diff --git a/docs/public/components/card-hero-light.webp b/docs/public/components/card-hero-light.webp new file mode 100644 index 00000000..83d1df3a Binary files /dev/null and b/docs/public/components/card-hero-light.webp differ diff --git a/docs/public/components/card-light.webp b/docs/public/components/card-light.webp new file mode 100644 index 00000000..1c113114 Binary files /dev/null and b/docs/public/components/card-light.webp differ diff --git a/docs/public/components/chart-area-dark.webp b/docs/public/components/chart-area-dark.webp new file mode 100644 index 00000000..41fa934a Binary files /dev/null and b/docs/public/components/chart-area-dark.webp differ diff --git a/docs/public/components/chart-area-light.webp b/docs/public/components/chart-area-light.webp new file mode 100644 index 00000000..a0d6b686 Binary files /dev/null and b/docs/public/components/chart-area-light.webp differ diff --git a/docs/public/components/chart-bar-dark.webp b/docs/public/components/chart-bar-dark.webp new file mode 100644 index 00000000..0bc505f4 Binary files /dev/null and b/docs/public/components/chart-bar-dark.webp differ diff --git a/docs/public/components/chart-bar-light.webp b/docs/public/components/chart-bar-light.webp new file mode 100644 index 00000000..8cdb30c0 Binary files /dev/null and b/docs/public/components/chart-bar-light.webp differ diff --git a/docs/public/components/chart-dark.webp b/docs/public/components/chart-dark.webp new file mode 100644 index 00000000..4f414800 Binary files /dev/null and b/docs/public/components/chart-dark.webp differ diff --git a/docs/public/components/chart-hero-dark.webp b/docs/public/components/chart-hero-dark.webp new file mode 100644 index 00000000..9fb27c90 Binary files /dev/null and b/docs/public/components/chart-hero-dark.webp differ diff --git a/docs/public/components/chart-hero-light.webp b/docs/public/components/chart-hero-light.webp new file mode 100644 index 00000000..4efd641a Binary files /dev/null and b/docs/public/components/chart-hero-light.webp differ diff --git a/docs/public/components/chart-light.webp b/docs/public/components/chart-light.webp new file mode 100644 index 00000000..0be6b1aa Binary files /dev/null and b/docs/public/components/chart-light.webp differ diff --git a/docs/public/components/checkbox-dark.webp b/docs/public/components/checkbox-dark.webp new file mode 100644 index 00000000..7cd6e0e3 Binary files /dev/null and b/docs/public/components/checkbox-dark.webp differ diff --git a/docs/public/components/checkbox-hero-dark.webp b/docs/public/components/checkbox-hero-dark.webp new file mode 100644 index 00000000..11f686de Binary files /dev/null and b/docs/public/components/checkbox-hero-dark.webp differ diff --git a/docs/public/components/checkbox-hero-light.webp b/docs/public/components/checkbox-hero-light.webp new file mode 100644 index 00000000..6bc1260f Binary files /dev/null and b/docs/public/components/checkbox-hero-light.webp differ diff --git a/docs/public/components/checkbox-light.webp b/docs/public/components/checkbox-light.webp new file mode 100644 index 00000000..b3eb9b1c Binary files /dev/null and b/docs/public/components/checkbox-light.webp differ diff --git a/docs/public/components/combobox-dark.webp b/docs/public/components/combobox-dark.webp new file mode 100644 index 00000000..d1a5dd5b Binary files /dev/null and b/docs/public/components/combobox-dark.webp differ diff --git a/docs/public/components/combobox-hero-dark.webp b/docs/public/components/combobox-hero-dark.webp new file mode 100644 index 00000000..70455ce7 Binary files /dev/null and b/docs/public/components/combobox-hero-dark.webp differ diff --git a/docs/public/components/combobox-hero-light.webp b/docs/public/components/combobox-hero-light.webp new file mode 100644 index 00000000..8deda9be Binary files /dev/null and b/docs/public/components/combobox-hero-light.webp differ diff --git a/docs/public/components/combobox-light.webp b/docs/public/components/combobox-light.webp new file mode 100644 index 00000000..26bf3289 Binary files /dev/null and b/docs/public/components/combobox-light.webp differ diff --git a/docs/public/components/dialog-dark.webp b/docs/public/components/dialog-dark.webp new file mode 100644 index 00000000..1ef0ac74 Binary files /dev/null and b/docs/public/components/dialog-dark.webp differ diff --git a/docs/public/components/dialog-hero-dark.webp b/docs/public/components/dialog-hero-dark.webp new file mode 100644 index 00000000..964ffba2 Binary files /dev/null and b/docs/public/components/dialog-hero-dark.webp differ diff --git a/docs/public/components/dialog-hero-light.webp b/docs/public/components/dialog-hero-light.webp new file mode 100644 index 00000000..6ded0882 Binary files /dev/null and b/docs/public/components/dialog-hero-light.webp differ diff --git a/docs/public/components/dialog-light.webp b/docs/public/components/dialog-light.webp new file mode 100644 index 00000000..2e496a5d Binary files /dev/null and b/docs/public/components/dialog-light.webp differ diff --git a/docs/public/components/drawer-dark.webp b/docs/public/components/drawer-dark.webp new file mode 100644 index 00000000..1933c2e7 Binary files /dev/null and b/docs/public/components/drawer-dark.webp differ diff --git a/docs/public/components/drawer-hero-dark.webp b/docs/public/components/drawer-hero-dark.webp new file mode 100644 index 00000000..62dd5b31 Binary files /dev/null and b/docs/public/components/drawer-hero-dark.webp differ diff --git a/docs/public/components/drawer-hero-light.webp b/docs/public/components/drawer-hero-light.webp new file mode 100644 index 00000000..8a0ae2e6 Binary files /dev/null and b/docs/public/components/drawer-hero-light.webp differ diff --git a/docs/public/components/drawer-light.webp b/docs/public/components/drawer-light.webp new file mode 100644 index 00000000..ff044a54 Binary files /dev/null and b/docs/public/components/drawer-light.webp differ diff --git a/docs/public/components/dropdown-menu-dark.webp b/docs/public/components/dropdown-menu-dark.webp new file mode 100644 index 00000000..389b8205 Binary files /dev/null and b/docs/public/components/dropdown-menu-dark.webp differ diff --git a/docs/public/components/dropdown-menu-hero-dark.webp b/docs/public/components/dropdown-menu-hero-dark.webp new file mode 100644 index 00000000..9d701b78 Binary files /dev/null and b/docs/public/components/dropdown-menu-hero-dark.webp differ diff --git a/docs/public/components/dropdown-menu-hero-light.webp b/docs/public/components/dropdown-menu-hero-light.webp new file mode 100644 index 00000000..83f7c2b2 Binary files /dev/null and b/docs/public/components/dropdown-menu-hero-light.webp differ diff --git a/docs/public/components/dropdown-menu-light.webp b/docs/public/components/dropdown-menu-light.webp new file mode 100644 index 00000000..929c2cab Binary files /dev/null and b/docs/public/components/dropdown-menu-light.webp differ diff --git a/docs/public/components/icon-dark.webp b/docs/public/components/icon-dark.webp new file mode 100644 index 00000000..d856b1c4 Binary files /dev/null and b/docs/public/components/icon-dark.webp differ diff --git a/docs/public/components/icon-hero-dark.webp b/docs/public/components/icon-hero-dark.webp new file mode 100644 index 00000000..a5a2214a Binary files /dev/null and b/docs/public/components/icon-hero-dark.webp differ diff --git a/docs/public/components/icon-hero-light.webp b/docs/public/components/icon-hero-light.webp new file mode 100644 index 00000000..a5a9bf72 Binary files /dev/null and b/docs/public/components/icon-hero-light.webp differ diff --git a/docs/public/components/icon-light.webp b/docs/public/components/icon-light.webp new file mode 100644 index 00000000..746f1f03 Binary files /dev/null and b/docs/public/components/icon-light.webp differ diff --git a/docs/public/components/icons/alert-dark.webp b/docs/public/components/icons/alert-dark.webp new file mode 100644 index 00000000..52402849 Binary files /dev/null and b/docs/public/components/icons/alert-dark.webp differ diff --git a/docs/public/components/icons/alert-light.webp b/docs/public/components/icons/alert-light.webp new file mode 100644 index 00000000..49d66572 Binary files /dev/null and b/docs/public/components/icons/alert-light.webp differ diff --git a/docs/public/components/icons/archive-dark.webp b/docs/public/components/icons/archive-dark.webp new file mode 100644 index 00000000..1e36d1da Binary files /dev/null and b/docs/public/components/icons/archive-dark.webp differ diff --git a/docs/public/components/icons/archive-light.webp b/docs/public/components/icons/archive-light.webp new file mode 100644 index 00000000..1cecaeb3 Binary files /dev/null and b/docs/public/components/icons/archive-light.webp differ diff --git a/docs/public/components/icons/arrow-down-dark.webp b/docs/public/components/icons/arrow-down-dark.webp new file mode 100644 index 00000000..e67ce52b Binary files /dev/null and b/docs/public/components/icons/arrow-down-dark.webp differ diff --git a/docs/public/components/icons/arrow-down-light.webp b/docs/public/components/icons/arrow-down-light.webp new file mode 100644 index 00000000..547be2b4 Binary files /dev/null and b/docs/public/components/icons/arrow-down-light.webp differ diff --git a/docs/public/components/icons/arrow-right-dark.webp b/docs/public/components/icons/arrow-right-dark.webp new file mode 100644 index 00000000..0e0a672d Binary files /dev/null and b/docs/public/components/icons/arrow-right-dark.webp differ diff --git a/docs/public/components/icons/arrow-right-light.webp b/docs/public/components/icons/arrow-right-light.webp new file mode 100644 index 00000000..58611078 Binary files /dev/null and b/docs/public/components/icons/arrow-right-light.webp differ diff --git a/docs/public/components/icons/arrow-up-dark.webp b/docs/public/components/icons/arrow-up-dark.webp new file mode 100644 index 00000000..cf515dc1 Binary files /dev/null and b/docs/public/components/icons/arrow-up-dark.webp differ diff --git a/docs/public/components/icons/arrow-up-light.webp b/docs/public/components/icons/arrow-up-light.webp new file mode 100644 index 00000000..832e6c87 Binary files /dev/null and b/docs/public/components/icons/arrow-up-light.webp differ diff --git a/docs/public/components/icons/check-circle-dark.webp b/docs/public/components/icons/check-circle-dark.webp new file mode 100644 index 00000000..4a50d86c Binary files /dev/null and b/docs/public/components/icons/check-circle-dark.webp differ diff --git a/docs/public/components/icons/check-circle-light.webp b/docs/public/components/icons/check-circle-light.webp new file mode 100644 index 00000000..c23bd5ed Binary files /dev/null and b/docs/public/components/icons/check-circle-light.webp differ diff --git a/docs/public/components/icons/check-dark.webp b/docs/public/components/icons/check-dark.webp new file mode 100644 index 00000000..20f5f228 Binary files /dev/null and b/docs/public/components/icons/check-dark.webp differ diff --git a/docs/public/components/icons/check-light.webp b/docs/public/components/icons/check-light.webp new file mode 100644 index 00000000..9591b9bd Binary files /dev/null and b/docs/public/components/icons/check-light.webp differ diff --git a/docs/public/components/icons/chevron-down-dark.webp b/docs/public/components/icons/chevron-down-dark.webp new file mode 100644 index 00000000..3122e167 Binary files /dev/null and b/docs/public/components/icons/chevron-down-dark.webp differ diff --git a/docs/public/components/icons/chevron-down-light.webp b/docs/public/components/icons/chevron-down-light.webp new file mode 100644 index 00000000..d7c1712a Binary files /dev/null and b/docs/public/components/icons/chevron-down-light.webp differ diff --git a/docs/public/components/icons/chevron-left-dark.webp b/docs/public/components/icons/chevron-left-dark.webp new file mode 100644 index 00000000..0e90dd87 Binary files /dev/null and b/docs/public/components/icons/chevron-left-dark.webp differ diff --git a/docs/public/components/icons/chevron-left-light.webp b/docs/public/components/icons/chevron-left-light.webp new file mode 100644 index 00000000..6c58ece6 Binary files /dev/null and b/docs/public/components/icons/chevron-left-light.webp differ diff --git a/docs/public/components/icons/chevron-right-dark.webp b/docs/public/components/icons/chevron-right-dark.webp new file mode 100644 index 00000000..69cdcda5 Binary files /dev/null and b/docs/public/components/icons/chevron-right-dark.webp differ diff --git a/docs/public/components/icons/chevron-right-light.webp b/docs/public/components/icons/chevron-right-light.webp new file mode 100644 index 00000000..850ada32 Binary files /dev/null and b/docs/public/components/icons/chevron-right-light.webp differ diff --git a/docs/public/components/icons/chevron-up-dark.webp b/docs/public/components/icons/chevron-up-dark.webp new file mode 100644 index 00000000..d8bca06e Binary files /dev/null and b/docs/public/components/icons/chevron-up-dark.webp differ diff --git a/docs/public/components/icons/chevron-up-light.webp b/docs/public/components/icons/chevron-up-light.webp new file mode 100644 index 00000000..f22b9a13 Binary files /dev/null and b/docs/public/components/icons/chevron-up-light.webp differ diff --git a/docs/public/components/icons/circle-dot-dark.webp b/docs/public/components/icons/circle-dot-dark.webp new file mode 100644 index 00000000..1601005b Binary files /dev/null and b/docs/public/components/icons/circle-dot-dark.webp differ diff --git a/docs/public/components/icons/circle-dot-light.webp b/docs/public/components/icons/circle-dot-light.webp new file mode 100644 index 00000000..78f9a669 Binary files /dev/null and b/docs/public/components/icons/circle-dot-light.webp differ diff --git a/docs/public/components/icons/clock-dark.webp b/docs/public/components/icons/clock-dark.webp new file mode 100644 index 00000000..00fa37af Binary files /dev/null and b/docs/public/components/icons/clock-dark.webp differ diff --git a/docs/public/components/icons/clock-light.webp b/docs/public/components/icons/clock-light.webp new file mode 100644 index 00000000..ac40d37a Binary files /dev/null and b/docs/public/components/icons/clock-light.webp differ diff --git a/docs/public/components/icons/copy-dark.webp b/docs/public/components/icons/copy-dark.webp new file mode 100644 index 00000000..1f4f6f2b Binary files /dev/null and b/docs/public/components/icons/copy-dark.webp differ diff --git a/docs/public/components/icons/copy-light.webp b/docs/public/components/icons/copy-light.webp new file mode 100644 index 00000000..51bb2193 Binary files /dev/null and b/docs/public/components/icons/copy-light.webp differ diff --git a/docs/public/components/icons/download-dark.webp b/docs/public/components/icons/download-dark.webp new file mode 100644 index 00000000..0998bf6e Binary files /dev/null and b/docs/public/components/icons/download-dark.webp differ diff --git a/docs/public/components/icons/download-light.webp b/docs/public/components/icons/download-light.webp new file mode 100644 index 00000000..11f6e469 Binary files /dev/null and b/docs/public/components/icons/download-light.webp differ diff --git a/docs/public/components/icons/edit-dark.webp b/docs/public/components/icons/edit-dark.webp new file mode 100644 index 00000000..83dfe121 Binary files /dev/null and b/docs/public/components/icons/edit-dark.webp differ diff --git a/docs/public/components/icons/edit-light.webp b/docs/public/components/icons/edit-light.webp new file mode 100644 index 00000000..9ca9f6a7 Binary files /dev/null and b/docs/public/components/icons/edit-light.webp differ diff --git a/docs/public/components/icons/ellipsis-dark.webp b/docs/public/components/icons/ellipsis-dark.webp new file mode 100644 index 00000000..cf88dafb Binary files /dev/null and b/docs/public/components/icons/ellipsis-dark.webp differ diff --git a/docs/public/components/icons/ellipsis-light.webp b/docs/public/components/icons/ellipsis-light.webp new file mode 100644 index 00000000..fc8cff6e Binary files /dev/null and b/docs/public/components/icons/ellipsis-light.webp differ diff --git a/docs/public/components/icons/external-link-dark.webp b/docs/public/components/icons/external-link-dark.webp new file mode 100644 index 00000000..d0f274ba Binary files /dev/null and b/docs/public/components/icons/external-link-dark.webp differ diff --git a/docs/public/components/icons/external-link-light.webp b/docs/public/components/icons/external-link-light.webp new file mode 100644 index 00000000..78e99e63 Binary files /dev/null and b/docs/public/components/icons/external-link-light.webp differ diff --git a/docs/public/components/icons/eye-dark.webp b/docs/public/components/icons/eye-dark.webp new file mode 100644 index 00000000..75e4ab88 Binary files /dev/null and b/docs/public/components/icons/eye-dark.webp differ diff --git a/docs/public/components/icons/eye-light.webp b/docs/public/components/icons/eye-light.webp new file mode 100644 index 00000000..e1a0cea1 Binary files /dev/null and b/docs/public/components/icons/eye-light.webp differ diff --git a/docs/public/components/icons/file-text-dark.webp b/docs/public/components/icons/file-text-dark.webp new file mode 100644 index 00000000..17831c73 Binary files /dev/null and b/docs/public/components/icons/file-text-dark.webp differ diff --git a/docs/public/components/icons/file-text-light.webp b/docs/public/components/icons/file-text-light.webp new file mode 100644 index 00000000..3061f121 Binary files /dev/null and b/docs/public/components/icons/file-text-light.webp differ diff --git a/docs/public/components/icons/folder-dark.webp b/docs/public/components/icons/folder-dark.webp new file mode 100644 index 00000000..c65ee857 Binary files /dev/null and b/docs/public/components/icons/folder-dark.webp differ diff --git a/docs/public/components/icons/folder-light.webp b/docs/public/components/icons/folder-light.webp new file mode 100644 index 00000000..3206a7ef Binary files /dev/null and b/docs/public/components/icons/folder-light.webp differ diff --git a/docs/public/components/icons/folder-open-dark.webp b/docs/public/components/icons/folder-open-dark.webp new file mode 100644 index 00000000..f56ba07a Binary files /dev/null and b/docs/public/components/icons/folder-open-dark.webp differ diff --git a/docs/public/components/icons/folder-open-light.webp b/docs/public/components/icons/folder-open-light.webp new file mode 100644 index 00000000..bd2ea1a7 Binary files /dev/null and b/docs/public/components/icons/folder-open-light.webp differ diff --git a/docs/public/components/icons/git-branch-dark.webp b/docs/public/components/icons/git-branch-dark.webp new file mode 100644 index 00000000..01d9a4d0 Binary files /dev/null and b/docs/public/components/icons/git-branch-dark.webp differ diff --git a/docs/public/components/icons/git-branch-light.webp b/docs/public/components/icons/git-branch-light.webp new file mode 100644 index 00000000..e2b404a8 Binary files /dev/null and b/docs/public/components/icons/git-branch-light.webp differ diff --git a/docs/public/components/icons/git-merge-dark.webp b/docs/public/components/icons/git-merge-dark.webp new file mode 100644 index 00000000..d11a799e Binary files /dev/null and b/docs/public/components/icons/git-merge-dark.webp differ diff --git a/docs/public/components/icons/git-merge-light.webp b/docs/public/components/icons/git-merge-light.webp new file mode 100644 index 00000000..504bff72 Binary files /dev/null and b/docs/public/components/icons/git-merge-light.webp differ diff --git a/docs/public/components/icons/git-pull-request-dark.webp b/docs/public/components/icons/git-pull-request-dark.webp new file mode 100644 index 00000000..8b9072f3 Binary files /dev/null and b/docs/public/components/icons/git-pull-request-dark.webp differ diff --git a/docs/public/components/icons/git-pull-request-light.webp b/docs/public/components/icons/git-pull-request-light.webp new file mode 100644 index 00000000..6f939ebf Binary files /dev/null and b/docs/public/components/icons/git-pull-request-light.webp differ diff --git a/docs/public/components/icons/info-dark.webp b/docs/public/components/icons/info-dark.webp new file mode 100644 index 00000000..97afc780 Binary files /dev/null and b/docs/public/components/icons/info-dark.webp differ diff --git a/docs/public/components/icons/info-light.webp b/docs/public/components/icons/info-light.webp new file mode 100644 index 00000000..ff54baa4 Binary files /dev/null and b/docs/public/components/icons/info-light.webp differ diff --git a/docs/public/components/icons/menu-dark.webp b/docs/public/components/icons/menu-dark.webp new file mode 100644 index 00000000..e3617cd9 Binary files /dev/null and b/docs/public/components/icons/menu-dark.webp differ diff --git a/docs/public/components/icons/menu-light.webp b/docs/public/components/icons/menu-light.webp new file mode 100644 index 00000000..eeb2b83b Binary files /dev/null and b/docs/public/components/icons/menu-light.webp differ diff --git a/docs/public/components/icons/moon-dark.webp b/docs/public/components/icons/moon-dark.webp new file mode 100644 index 00000000..d7ad0ca3 Binary files /dev/null and b/docs/public/components/icons/moon-dark.webp differ diff --git a/docs/public/components/icons/moon-light.webp b/docs/public/components/icons/moon-light.webp new file mode 100644 index 00000000..461f11d0 Binary files /dev/null and b/docs/public/components/icons/moon-light.webp differ diff --git a/docs/public/components/icons/music-dark.webp b/docs/public/components/icons/music-dark.webp new file mode 100644 index 00000000..8a8e57fb Binary files /dev/null and b/docs/public/components/icons/music-dark.webp differ diff --git a/docs/public/components/icons/music-light.webp b/docs/public/components/icons/music-light.webp new file mode 100644 index 00000000..0f25436f Binary files /dev/null and b/docs/public/components/icons/music-light.webp differ diff --git a/docs/public/components/icons/panel-left-dark.webp b/docs/public/components/icons/panel-left-dark.webp new file mode 100644 index 00000000..99a4c225 Binary files /dev/null and b/docs/public/components/icons/panel-left-dark.webp differ diff --git a/docs/public/components/icons/panel-left-light.webp b/docs/public/components/icons/panel-left-light.webp new file mode 100644 index 00000000..4c5b2d5f Binary files /dev/null and b/docs/public/components/icons/panel-left-light.webp differ diff --git a/docs/public/components/icons/panel-right-dark.webp b/docs/public/components/icons/panel-right-dark.webp new file mode 100644 index 00000000..06342308 Binary files /dev/null and b/docs/public/components/icons/panel-right-dark.webp differ diff --git a/docs/public/components/icons/panel-right-light.webp b/docs/public/components/icons/panel-right-light.webp new file mode 100644 index 00000000..804b5625 Binary files /dev/null and b/docs/public/components/icons/panel-right-light.webp differ diff --git a/docs/public/components/icons/pause-dark.webp b/docs/public/components/icons/pause-dark.webp new file mode 100644 index 00000000..c7981a1f Binary files /dev/null and b/docs/public/components/icons/pause-dark.webp differ diff --git a/docs/public/components/icons/pause-light.webp b/docs/public/components/icons/pause-light.webp new file mode 100644 index 00000000..00468b9f Binary files /dev/null and b/docs/public/components/icons/pause-light.webp differ diff --git a/docs/public/components/icons/play-dark.webp b/docs/public/components/icons/play-dark.webp new file mode 100644 index 00000000..70888ae7 Binary files /dev/null and b/docs/public/components/icons/play-dark.webp differ diff --git a/docs/public/components/icons/play-light.webp b/docs/public/components/icons/play-light.webp new file mode 100644 index 00000000..52723e9e Binary files /dev/null and b/docs/public/components/icons/play-light.webp differ diff --git a/docs/public/components/icons/plus-dark.webp b/docs/public/components/icons/plus-dark.webp new file mode 100644 index 00000000..dcb6c3fe Binary files /dev/null and b/docs/public/components/icons/plus-dark.webp differ diff --git a/docs/public/components/icons/plus-light.webp b/docs/public/components/icons/plus-light.webp new file mode 100644 index 00000000..0d045ed1 Binary files /dev/null and b/docs/public/components/icons/plus-light.webp differ diff --git a/docs/public/components/icons/refresh-cw-dark.webp b/docs/public/components/icons/refresh-cw-dark.webp new file mode 100644 index 00000000..edb0ffab Binary files /dev/null and b/docs/public/components/icons/refresh-cw-dark.webp differ diff --git a/docs/public/components/icons/refresh-cw-light.webp b/docs/public/components/icons/refresh-cw-light.webp new file mode 100644 index 00000000..2dc389a2 Binary files /dev/null and b/docs/public/components/icons/refresh-cw-light.webp differ diff --git a/docs/public/components/icons/repeat-dark.webp b/docs/public/components/icons/repeat-dark.webp new file mode 100644 index 00000000..c42d5683 Binary files /dev/null and b/docs/public/components/icons/repeat-dark.webp differ diff --git a/docs/public/components/icons/repeat-light.webp b/docs/public/components/icons/repeat-light.webp new file mode 100644 index 00000000..cbdd1f5a Binary files /dev/null and b/docs/public/components/icons/repeat-light.webp differ diff --git a/docs/public/components/icons/save-dark.webp b/docs/public/components/icons/save-dark.webp new file mode 100644 index 00000000..bd44a93c Binary files /dev/null and b/docs/public/components/icons/save-dark.webp differ diff --git a/docs/public/components/icons/save-light.webp b/docs/public/components/icons/save-light.webp new file mode 100644 index 00000000..d159e966 Binary files /dev/null and b/docs/public/components/icons/save-light.webp differ diff --git a/docs/public/components/icons/search-dark.webp b/docs/public/components/icons/search-dark.webp new file mode 100644 index 00000000..115827dc Binary files /dev/null and b/docs/public/components/icons/search-dark.webp differ diff --git a/docs/public/components/icons/search-light.webp b/docs/public/components/icons/search-light.webp new file mode 100644 index 00000000..0797e370 Binary files /dev/null and b/docs/public/components/icons/search-light.webp differ diff --git a/docs/public/components/icons/send-dark.webp b/docs/public/components/icons/send-dark.webp new file mode 100644 index 00000000..0615ce8e Binary files /dev/null and b/docs/public/components/icons/send-dark.webp differ diff --git a/docs/public/components/icons/send-light.webp b/docs/public/components/icons/send-light.webp new file mode 100644 index 00000000..9c0945a3 Binary files /dev/null and b/docs/public/components/icons/send-light.webp differ diff --git a/docs/public/components/icons/settings-dark.webp b/docs/public/components/icons/settings-dark.webp new file mode 100644 index 00000000..f9750330 Binary files /dev/null and b/docs/public/components/icons/settings-dark.webp differ diff --git a/docs/public/components/icons/settings-light.webp b/docs/public/components/icons/settings-light.webp new file mode 100644 index 00000000..df3a0b73 Binary files /dev/null and b/docs/public/components/icons/settings-light.webp differ diff --git a/docs/public/components/icons/shuffle-dark.webp b/docs/public/components/icons/shuffle-dark.webp new file mode 100644 index 00000000..ef177f70 Binary files /dev/null and b/docs/public/components/icons/shuffle-dark.webp differ diff --git a/docs/public/components/icons/shuffle-light.webp b/docs/public/components/icons/shuffle-light.webp new file mode 100644 index 00000000..1f33dbed Binary files /dev/null and b/docs/public/components/icons/shuffle-light.webp differ diff --git a/docs/public/components/icons/skip-back-dark.webp b/docs/public/components/icons/skip-back-dark.webp new file mode 100644 index 00000000..fac00eff Binary files /dev/null and b/docs/public/components/icons/skip-back-dark.webp differ diff --git a/docs/public/components/icons/skip-back-light.webp b/docs/public/components/icons/skip-back-light.webp new file mode 100644 index 00000000..f5a814f7 Binary files /dev/null and b/docs/public/components/icons/skip-back-light.webp differ diff --git a/docs/public/components/icons/skip-forward-dark.webp b/docs/public/components/icons/skip-forward-dark.webp new file mode 100644 index 00000000..39c1d544 Binary files /dev/null and b/docs/public/components/icons/skip-forward-dark.webp differ diff --git a/docs/public/components/icons/skip-forward-light.webp b/docs/public/components/icons/skip-forward-light.webp new file mode 100644 index 00000000..099ad0d1 Binary files /dev/null and b/docs/public/components/icons/skip-forward-light.webp differ diff --git a/docs/public/components/icons/sun-dark.webp b/docs/public/components/icons/sun-dark.webp new file mode 100644 index 00000000..45448c6b Binary files /dev/null and b/docs/public/components/icons/sun-dark.webp differ diff --git a/docs/public/components/icons/sun-light.webp b/docs/public/components/icons/sun-light.webp new file mode 100644 index 00000000..468d5b2d Binary files /dev/null and b/docs/public/components/icons/sun-light.webp differ diff --git a/docs/public/components/icons/terminal-dark.webp b/docs/public/components/icons/terminal-dark.webp new file mode 100644 index 00000000..687cee32 Binary files /dev/null and b/docs/public/components/icons/terminal-dark.webp differ diff --git a/docs/public/components/icons/terminal-light.webp b/docs/public/components/icons/terminal-light.webp new file mode 100644 index 00000000..89d3ef5e Binary files /dev/null and b/docs/public/components/icons/terminal-light.webp differ diff --git a/docs/public/components/icons/trash-dark.webp b/docs/public/components/icons/trash-dark.webp new file mode 100644 index 00000000..b19f0e94 Binary files /dev/null and b/docs/public/components/icons/trash-dark.webp differ diff --git a/docs/public/components/icons/trash-light.webp b/docs/public/components/icons/trash-light.webp new file mode 100644 index 00000000..a6fd4ee2 Binary files /dev/null and b/docs/public/components/icons/trash-light.webp differ diff --git a/docs/public/components/icons/volume-dark.webp b/docs/public/components/icons/volume-dark.webp new file mode 100644 index 00000000..ee2ff00c Binary files /dev/null and b/docs/public/components/icons/volume-dark.webp differ diff --git a/docs/public/components/icons/volume-light.webp b/docs/public/components/icons/volume-light.webp new file mode 100644 index 00000000..311141f8 Binary files /dev/null and b/docs/public/components/icons/volume-light.webp differ diff --git a/docs/public/components/icons/wrench-dark.webp b/docs/public/components/icons/wrench-dark.webp new file mode 100644 index 00000000..299cd1f3 Binary files /dev/null and b/docs/public/components/icons/wrench-dark.webp differ diff --git a/docs/public/components/icons/wrench-light.webp b/docs/public/components/icons/wrench-light.webp new file mode 100644 index 00000000..819e69a7 Binary files /dev/null and b/docs/public/components/icons/wrench-light.webp differ diff --git a/docs/public/components/icons/x-circle-dark.webp b/docs/public/components/icons/x-circle-dark.webp new file mode 100644 index 00000000..18dc5ff9 Binary files /dev/null and b/docs/public/components/icons/x-circle-dark.webp differ diff --git a/docs/public/components/icons/x-circle-light.webp b/docs/public/components/icons/x-circle-light.webp new file mode 100644 index 00000000..46eb626f Binary files /dev/null and b/docs/public/components/icons/x-circle-light.webp differ diff --git a/docs/public/components/icons/x-dark.webp b/docs/public/components/icons/x-dark.webp new file mode 100644 index 00000000..6727cf9c Binary files /dev/null and b/docs/public/components/icons/x-dark.webp differ diff --git a/docs/public/components/icons/x-light.webp b/docs/public/components/icons/x-light.webp new file mode 100644 index 00000000..964ed615 Binary files /dev/null and b/docs/public/components/icons/x-light.webp differ diff --git a/docs/public/components/input-dark.webp b/docs/public/components/input-dark.webp new file mode 100644 index 00000000..d9f93604 Binary files /dev/null and b/docs/public/components/input-dark.webp differ diff --git a/docs/public/components/input-group-dark.webp b/docs/public/components/input-group-dark.webp new file mode 100644 index 00000000..21006314 Binary files /dev/null and b/docs/public/components/input-group-dark.webp differ diff --git a/docs/public/components/input-group-hero-dark.webp b/docs/public/components/input-group-hero-dark.webp new file mode 100644 index 00000000..3b299591 Binary files /dev/null and b/docs/public/components/input-group-hero-dark.webp differ diff --git a/docs/public/components/input-group-hero-light.webp b/docs/public/components/input-group-hero-light.webp new file mode 100644 index 00000000..f2866803 Binary files /dev/null and b/docs/public/components/input-group-hero-light.webp differ diff --git a/docs/public/components/input-group-light.webp b/docs/public/components/input-group-light.webp new file mode 100644 index 00000000..455fced8 Binary files /dev/null and b/docs/public/components/input-group-light.webp differ diff --git a/docs/public/components/input-hero-dark.webp b/docs/public/components/input-hero-dark.webp new file mode 100644 index 00000000..fa5729e7 Binary files /dev/null and b/docs/public/components/input-hero-dark.webp differ diff --git a/docs/public/components/input-hero-light.webp b/docs/public/components/input-hero-light.webp new file mode 100644 index 00000000..16444b0d Binary files /dev/null and b/docs/public/components/input-hero-light.webp differ diff --git a/docs/public/components/input-light.webp b/docs/public/components/input-light.webp new file mode 100644 index 00000000..7979eb39 Binary files /dev/null and b/docs/public/components/input-light.webp differ diff --git a/docs/public/components/list-dark.webp b/docs/public/components/list-dark.webp new file mode 100644 index 00000000..072d87ed Binary files /dev/null and b/docs/public/components/list-dark.webp differ diff --git a/docs/public/components/list-hero-dark.webp b/docs/public/components/list-hero-dark.webp new file mode 100644 index 00000000..ae8b226c Binary files /dev/null and b/docs/public/components/list-hero-dark.webp differ diff --git a/docs/public/components/list-hero-light.webp b/docs/public/components/list-hero-light.webp new file mode 100644 index 00000000..6a5b2014 Binary files /dev/null and b/docs/public/components/list-hero-light.webp differ diff --git a/docs/public/components/list-light.webp b/docs/public/components/list-light.webp new file mode 100644 index 00000000..450ec9b3 Binary files /dev/null and b/docs/public/components/list-light.webp differ diff --git a/docs/public/components/markdown-dark.webp b/docs/public/components/markdown-dark.webp new file mode 100644 index 00000000..709c6815 Binary files /dev/null and b/docs/public/components/markdown-dark.webp differ diff --git a/docs/public/components/markdown-hero-dark.webp b/docs/public/components/markdown-hero-dark.webp new file mode 100644 index 00000000..781ba7d5 Binary files /dev/null and b/docs/public/components/markdown-hero-dark.webp differ diff --git a/docs/public/components/markdown-hero-light.webp b/docs/public/components/markdown-hero-light.webp new file mode 100644 index 00000000..9e00555d Binary files /dev/null and b/docs/public/components/markdown-hero-light.webp differ diff --git a/docs/public/components/markdown-light.webp b/docs/public/components/markdown-light.webp new file mode 100644 index 00000000..7ebfcf2b Binary files /dev/null and b/docs/public/components/markdown-light.webp differ diff --git a/docs/public/components/menu-dark.webp b/docs/public/components/menu-dark.webp new file mode 100644 index 00000000..091e5604 Binary files /dev/null and b/docs/public/components/menu-dark.webp differ diff --git a/docs/public/components/menu-light.webp b/docs/public/components/menu-light.webp new file mode 100644 index 00000000..397e9ad4 Binary files /dev/null and b/docs/public/components/menu-light.webp differ diff --git a/docs/public/components/pagination-dark.webp b/docs/public/components/pagination-dark.webp new file mode 100644 index 00000000..b9f08b6f Binary files /dev/null and b/docs/public/components/pagination-dark.webp differ diff --git a/docs/public/components/pagination-hero-dark.webp b/docs/public/components/pagination-hero-dark.webp new file mode 100644 index 00000000..a3b295be Binary files /dev/null and b/docs/public/components/pagination-hero-dark.webp differ diff --git a/docs/public/components/pagination-hero-light.webp b/docs/public/components/pagination-hero-light.webp new file mode 100644 index 00000000..07ce172a Binary files /dev/null and b/docs/public/components/pagination-hero-light.webp differ diff --git a/docs/public/components/pagination-light.webp b/docs/public/components/pagination-light.webp new file mode 100644 index 00000000..f3f0be46 Binary files /dev/null and b/docs/public/components/pagination-light.webp differ diff --git a/docs/public/components/panel-dark.webp b/docs/public/components/panel-dark.webp new file mode 100644 index 00000000..fd77097e Binary files /dev/null and b/docs/public/components/panel-dark.webp differ diff --git a/docs/public/components/panel-hero-dark.webp b/docs/public/components/panel-hero-dark.webp new file mode 100644 index 00000000..b51d6b88 Binary files /dev/null and b/docs/public/components/panel-hero-dark.webp differ diff --git a/docs/public/components/panel-hero-light.webp b/docs/public/components/panel-hero-light.webp new file mode 100644 index 00000000..83ba4459 Binary files /dev/null and b/docs/public/components/panel-hero-light.webp differ diff --git a/docs/public/components/panel-light.webp b/docs/public/components/panel-light.webp new file mode 100644 index 00000000..1584ac17 Binary files /dev/null and b/docs/public/components/panel-light.webp differ diff --git a/docs/public/components/progress-dark.webp b/docs/public/components/progress-dark.webp new file mode 100644 index 00000000..4047d1f5 Binary files /dev/null and b/docs/public/components/progress-dark.webp differ diff --git a/docs/public/components/progress-hero-dark.webp b/docs/public/components/progress-hero-dark.webp new file mode 100644 index 00000000..58c8b6cc Binary files /dev/null and b/docs/public/components/progress-hero-dark.webp differ diff --git a/docs/public/components/progress-hero-light.webp b/docs/public/components/progress-hero-light.webp new file mode 100644 index 00000000..57a91e35 Binary files /dev/null and b/docs/public/components/progress-hero-light.webp differ diff --git a/docs/public/components/progress-light.webp b/docs/public/components/progress-light.webp new file mode 100644 index 00000000..2d92d10b Binary files /dev/null and b/docs/public/components/progress-light.webp differ diff --git a/docs/public/components/radio-group-dark.webp b/docs/public/components/radio-group-dark.webp new file mode 100644 index 00000000..5485d218 Binary files /dev/null and b/docs/public/components/radio-group-dark.webp differ diff --git a/docs/public/components/radio-group-light.webp b/docs/public/components/radio-group-light.webp new file mode 100644 index 00000000..05c21595 Binary files /dev/null and b/docs/public/components/radio-group-light.webp differ diff --git a/docs/public/components/radio-hero-dark.webp b/docs/public/components/radio-hero-dark.webp new file mode 100644 index 00000000..78e23963 Binary files /dev/null and b/docs/public/components/radio-hero-dark.webp differ diff --git a/docs/public/components/radio-hero-light.webp b/docs/public/components/radio-hero-light.webp new file mode 100644 index 00000000..759b575f Binary files /dev/null and b/docs/public/components/radio-hero-light.webp differ diff --git a/docs/public/components/resizable-dark.webp b/docs/public/components/resizable-dark.webp new file mode 100644 index 00000000..7565a70b Binary files /dev/null and b/docs/public/components/resizable-dark.webp differ diff --git a/docs/public/components/resizable-hero-dark.webp b/docs/public/components/resizable-hero-dark.webp new file mode 100644 index 00000000..15cc4035 Binary files /dev/null and b/docs/public/components/resizable-hero-dark.webp differ diff --git a/docs/public/components/resizable-hero-light.webp b/docs/public/components/resizable-hero-light.webp new file mode 100644 index 00000000..5441af11 Binary files /dev/null and b/docs/public/components/resizable-hero-light.webp differ diff --git a/docs/public/components/resizable-light.webp b/docs/public/components/resizable-light.webp new file mode 100644 index 00000000..930ee48c Binary files /dev/null and b/docs/public/components/resizable-light.webp differ diff --git a/docs/public/components/scroll-dark.webp b/docs/public/components/scroll-dark.webp new file mode 100644 index 00000000..653aaccf Binary files /dev/null and b/docs/public/components/scroll-dark.webp differ diff --git a/docs/public/components/scroll-hero-dark.webp b/docs/public/components/scroll-hero-dark.webp new file mode 100644 index 00000000..329be058 Binary files /dev/null and b/docs/public/components/scroll-hero-dark.webp differ diff --git a/docs/public/components/scroll-hero-light.webp b/docs/public/components/scroll-hero-light.webp new file mode 100644 index 00000000..ace59175 Binary files /dev/null and b/docs/public/components/scroll-hero-light.webp differ diff --git a/docs/public/components/scroll-light.webp b/docs/public/components/scroll-light.webp new file mode 100644 index 00000000..f716b4e4 Binary files /dev/null and b/docs/public/components/scroll-light.webp differ diff --git a/docs/public/components/search-field-dark.webp b/docs/public/components/search-field-dark.webp new file mode 100644 index 00000000..a217e6a7 Binary files /dev/null and b/docs/public/components/search-field-dark.webp differ diff --git a/docs/public/components/search-field-light.webp b/docs/public/components/search-field-light.webp new file mode 100644 index 00000000..f233b0e3 Binary files /dev/null and b/docs/public/components/search-field-light.webp differ diff --git a/docs/public/components/select-dark.webp b/docs/public/components/select-dark.webp new file mode 100644 index 00000000..6f5ffd22 Binary files /dev/null and b/docs/public/components/select-dark.webp differ diff --git a/docs/public/components/select-hero-dark.webp b/docs/public/components/select-hero-dark.webp new file mode 100644 index 00000000..588db456 Binary files /dev/null and b/docs/public/components/select-hero-dark.webp differ diff --git a/docs/public/components/select-hero-light.webp b/docs/public/components/select-hero-light.webp new file mode 100644 index 00000000..9a426c25 Binary files /dev/null and b/docs/public/components/select-hero-light.webp differ diff --git a/docs/public/components/select-light.webp b/docs/public/components/select-light.webp new file mode 100644 index 00000000..8f031a2c Binary files /dev/null and b/docs/public/components/select-light.webp differ diff --git a/docs/public/components/separator-dark.webp b/docs/public/components/separator-dark.webp new file mode 100644 index 00000000..2d575ea5 Binary files /dev/null and b/docs/public/components/separator-dark.webp differ diff --git a/docs/public/components/separator-hero-dark.webp b/docs/public/components/separator-hero-dark.webp new file mode 100644 index 00000000..c06a56c3 Binary files /dev/null and b/docs/public/components/separator-hero-dark.webp differ diff --git a/docs/public/components/separator-hero-light.webp b/docs/public/components/separator-hero-light.webp new file mode 100644 index 00000000..f6794cbf Binary files /dev/null and b/docs/public/components/separator-hero-light.webp differ diff --git a/docs/public/components/separator-light.webp b/docs/public/components/separator-light.webp new file mode 100644 index 00000000..2a6468f9 Binary files /dev/null and b/docs/public/components/separator-light.webp differ diff --git a/docs/public/components/sheet-dark.webp b/docs/public/components/sheet-dark.webp new file mode 100644 index 00000000..d9d10e6f Binary files /dev/null and b/docs/public/components/sheet-dark.webp differ diff --git a/docs/public/components/sheet-hero-dark.webp b/docs/public/components/sheet-hero-dark.webp new file mode 100644 index 00000000..2160c378 Binary files /dev/null and b/docs/public/components/sheet-hero-dark.webp differ diff --git a/docs/public/components/sheet-hero-light.webp b/docs/public/components/sheet-hero-light.webp new file mode 100644 index 00000000..95554fb1 Binary files /dev/null and b/docs/public/components/sheet-hero-light.webp differ diff --git a/docs/public/components/sheet-light.webp b/docs/public/components/sheet-light.webp new file mode 100644 index 00000000..415b130f Binary files /dev/null and b/docs/public/components/sheet-light.webp differ diff --git a/docs/public/components/skeleton-dark.webp b/docs/public/components/skeleton-dark.webp new file mode 100644 index 00000000..17de7211 Binary files /dev/null and b/docs/public/components/skeleton-dark.webp differ diff --git a/docs/public/components/skeleton-hero-dark.webp b/docs/public/components/skeleton-hero-dark.webp new file mode 100644 index 00000000..927db2bf Binary files /dev/null and b/docs/public/components/skeleton-hero-dark.webp differ diff --git a/docs/public/components/skeleton-hero-light.webp b/docs/public/components/skeleton-hero-light.webp new file mode 100644 index 00000000..fc3edc19 Binary files /dev/null and b/docs/public/components/skeleton-hero-light.webp differ diff --git a/docs/public/components/skeleton-light.webp b/docs/public/components/skeleton-light.webp new file mode 100644 index 00000000..8105f2fe Binary files /dev/null and b/docs/public/components/skeleton-light.webp differ diff --git a/docs/public/components/slider-dark.webp b/docs/public/components/slider-dark.webp new file mode 100644 index 00000000..39b84e04 Binary files /dev/null and b/docs/public/components/slider-dark.webp differ diff --git a/docs/public/components/slider-hero-dark.webp b/docs/public/components/slider-hero-dark.webp new file mode 100644 index 00000000..6e04a40c Binary files /dev/null and b/docs/public/components/slider-hero-dark.webp differ diff --git a/docs/public/components/slider-hero-light.webp b/docs/public/components/slider-hero-light.webp new file mode 100644 index 00000000..27b0d8f5 Binary files /dev/null and b/docs/public/components/slider-hero-light.webp differ diff --git a/docs/public/components/slider-light.webp b/docs/public/components/slider-light.webp new file mode 100644 index 00000000..1573083d Binary files /dev/null and b/docs/public/components/slider-light.webp differ diff --git a/docs/public/components/spacer-dark.webp b/docs/public/components/spacer-dark.webp new file mode 100644 index 00000000..3eb0afac Binary files /dev/null and b/docs/public/components/spacer-dark.webp differ diff --git a/docs/public/components/spacer-hero-dark.webp b/docs/public/components/spacer-hero-dark.webp new file mode 100644 index 00000000..e99680e5 Binary files /dev/null and b/docs/public/components/spacer-hero-dark.webp differ diff --git a/docs/public/components/spacer-hero-light.webp b/docs/public/components/spacer-hero-light.webp new file mode 100644 index 00000000..a68aa935 Binary files /dev/null and b/docs/public/components/spacer-hero-light.webp differ diff --git a/docs/public/components/spacer-light.webp b/docs/public/components/spacer-light.webp new file mode 100644 index 00000000..507f346c Binary files /dev/null and b/docs/public/components/spacer-light.webp differ diff --git a/docs/public/components/spinner-dark.webp b/docs/public/components/spinner-dark.webp new file mode 100644 index 00000000..6452de21 Binary files /dev/null and b/docs/public/components/spinner-dark.webp differ diff --git a/docs/public/components/spinner-hero-dark.webp b/docs/public/components/spinner-hero-dark.webp new file mode 100644 index 00000000..d50b4d68 Binary files /dev/null and b/docs/public/components/spinner-hero-dark.webp differ diff --git a/docs/public/components/spinner-hero-light.webp b/docs/public/components/spinner-hero-light.webp new file mode 100644 index 00000000..d3173000 Binary files /dev/null and b/docs/public/components/spinner-hero-light.webp differ diff --git a/docs/public/components/spinner-light.webp b/docs/public/components/spinner-light.webp new file mode 100644 index 00000000..cd8bae39 Binary files /dev/null and b/docs/public/components/spinner-light.webp differ diff --git a/docs/public/components/split-dark.webp b/docs/public/components/split-dark.webp new file mode 100644 index 00000000..2a9ee14d Binary files /dev/null and b/docs/public/components/split-dark.webp differ diff --git a/docs/public/components/split-hero-dark.webp b/docs/public/components/split-hero-dark.webp new file mode 100644 index 00000000..0d34c600 Binary files /dev/null and b/docs/public/components/split-hero-dark.webp differ diff --git a/docs/public/components/split-hero-light.webp b/docs/public/components/split-hero-light.webp new file mode 100644 index 00000000..c6c4b7c5 Binary files /dev/null and b/docs/public/components/split-hero-light.webp differ diff --git a/docs/public/components/split-light.webp b/docs/public/components/split-light.webp new file mode 100644 index 00000000..fba4d39e Binary files /dev/null and b/docs/public/components/split-light.webp differ diff --git a/docs/public/components/status-bar-dark.webp b/docs/public/components/status-bar-dark.webp new file mode 100644 index 00000000..2e798665 Binary files /dev/null and b/docs/public/components/status-bar-dark.webp differ diff --git a/docs/public/components/status-bar-hero-dark.webp b/docs/public/components/status-bar-hero-dark.webp new file mode 100644 index 00000000..039dd555 Binary files /dev/null and b/docs/public/components/status-bar-hero-dark.webp differ diff --git a/docs/public/components/status-bar-hero-light.webp b/docs/public/components/status-bar-hero-light.webp new file mode 100644 index 00000000..a01c9a05 Binary files /dev/null and b/docs/public/components/status-bar-hero-light.webp differ diff --git a/docs/public/components/status-bar-light.webp b/docs/public/components/status-bar-light.webp new file mode 100644 index 00000000..488a877b Binary files /dev/null and b/docs/public/components/status-bar-light.webp differ diff --git a/docs/public/components/stepper-dark.webp b/docs/public/components/stepper-dark.webp new file mode 100644 index 00000000..9e58f67a Binary files /dev/null and b/docs/public/components/stepper-dark.webp differ diff --git a/docs/public/components/stepper-hero-dark.webp b/docs/public/components/stepper-hero-dark.webp new file mode 100644 index 00000000..1701a2ba Binary files /dev/null and b/docs/public/components/stepper-hero-dark.webp differ diff --git a/docs/public/components/stepper-hero-light.webp b/docs/public/components/stepper-hero-light.webp new file mode 100644 index 00000000..ce3a90f0 Binary files /dev/null and b/docs/public/components/stepper-hero-light.webp differ diff --git a/docs/public/components/stepper-light.webp b/docs/public/components/stepper-light.webp new file mode 100644 index 00000000..009b56b1 Binary files /dev/null and b/docs/public/components/stepper-light.webp differ diff --git a/docs/public/components/switch-dark.webp b/docs/public/components/switch-dark.webp new file mode 100644 index 00000000..0b7f540a Binary files /dev/null and b/docs/public/components/switch-dark.webp differ diff --git a/docs/public/components/switch-hero-dark.webp b/docs/public/components/switch-hero-dark.webp new file mode 100644 index 00000000..d5daa8ab Binary files /dev/null and b/docs/public/components/switch-hero-dark.webp differ diff --git a/docs/public/components/switch-hero-light.webp b/docs/public/components/switch-hero-light.webp new file mode 100644 index 00000000..84059fd3 Binary files /dev/null and b/docs/public/components/switch-hero-light.webp differ diff --git a/docs/public/components/switch-light.webp b/docs/public/components/switch-light.webp new file mode 100644 index 00000000..ca1808e0 Binary files /dev/null and b/docs/public/components/switch-light.webp differ diff --git a/docs/public/components/table-dark.webp b/docs/public/components/table-dark.webp new file mode 100644 index 00000000..493bc9ed Binary files /dev/null and b/docs/public/components/table-dark.webp differ diff --git a/docs/public/components/table-hero-dark.webp b/docs/public/components/table-hero-dark.webp new file mode 100644 index 00000000..182f2542 Binary files /dev/null and b/docs/public/components/table-hero-dark.webp differ diff --git a/docs/public/components/table-hero-light.webp b/docs/public/components/table-hero-light.webp new file mode 100644 index 00000000..0700b7af Binary files /dev/null and b/docs/public/components/table-hero-light.webp differ diff --git a/docs/public/components/table-light.webp b/docs/public/components/table-light.webp new file mode 100644 index 00000000..a59d4b51 Binary files /dev/null and b/docs/public/components/table-light.webp differ diff --git a/docs/public/components/tabs-dark.webp b/docs/public/components/tabs-dark.webp new file mode 100644 index 00000000..4edf8938 Binary files /dev/null and b/docs/public/components/tabs-dark.webp differ diff --git a/docs/public/components/tabs-hero-dark.webp b/docs/public/components/tabs-hero-dark.webp new file mode 100644 index 00000000..47887321 Binary files /dev/null and b/docs/public/components/tabs-hero-dark.webp differ diff --git a/docs/public/components/tabs-hero-light.webp b/docs/public/components/tabs-hero-light.webp new file mode 100644 index 00000000..4df4c32e Binary files /dev/null and b/docs/public/components/tabs-hero-light.webp differ diff --git a/docs/public/components/tabs-light.webp b/docs/public/components/tabs-light.webp new file mode 100644 index 00000000..7e7e8296 Binary files /dev/null and b/docs/public/components/tabs-light.webp differ diff --git a/docs/public/components/textarea-dark.webp b/docs/public/components/textarea-dark.webp new file mode 100644 index 00000000..c4678504 Binary files /dev/null and b/docs/public/components/textarea-dark.webp differ diff --git a/docs/public/components/textarea-hero-dark.webp b/docs/public/components/textarea-hero-dark.webp new file mode 100644 index 00000000..7c1a3bcb Binary files /dev/null and b/docs/public/components/textarea-hero-dark.webp differ diff --git a/docs/public/components/textarea-hero-light.webp b/docs/public/components/textarea-hero-light.webp new file mode 100644 index 00000000..bf73ec48 Binary files /dev/null and b/docs/public/components/textarea-hero-light.webp differ diff --git a/docs/public/components/textarea-light.webp b/docs/public/components/textarea-light.webp new file mode 100644 index 00000000..aa6499af Binary files /dev/null and b/docs/public/components/textarea-light.webp differ diff --git a/docs/public/components/timeline-dark.webp b/docs/public/components/timeline-dark.webp new file mode 100644 index 00000000..b99f3e65 Binary files /dev/null and b/docs/public/components/timeline-dark.webp differ diff --git a/docs/public/components/timeline-hero-dark.webp b/docs/public/components/timeline-hero-dark.webp new file mode 100644 index 00000000..9ab609b7 Binary files /dev/null and b/docs/public/components/timeline-hero-dark.webp differ diff --git a/docs/public/components/timeline-hero-light.webp b/docs/public/components/timeline-hero-light.webp new file mode 100644 index 00000000..5b82042d Binary files /dev/null and b/docs/public/components/timeline-hero-light.webp differ diff --git a/docs/public/components/timeline-light.webp b/docs/public/components/timeline-light.webp new file mode 100644 index 00000000..6592ed9d Binary files /dev/null and b/docs/public/components/timeline-light.webp differ diff --git a/docs/public/components/toggle-dark.webp b/docs/public/components/toggle-dark.webp new file mode 100644 index 00000000..7665d302 Binary files /dev/null and b/docs/public/components/toggle-dark.webp differ diff --git a/docs/public/components/toggle-group-dark.webp b/docs/public/components/toggle-group-dark.webp new file mode 100644 index 00000000..855d2985 Binary files /dev/null and b/docs/public/components/toggle-group-dark.webp differ diff --git a/docs/public/components/toggle-group-light.webp b/docs/public/components/toggle-group-light.webp new file mode 100644 index 00000000..e6d223d2 Binary files /dev/null and b/docs/public/components/toggle-group-light.webp differ diff --git a/docs/public/components/toggle-hero-dark.webp b/docs/public/components/toggle-hero-dark.webp new file mode 100644 index 00000000..fa7923cd Binary files /dev/null and b/docs/public/components/toggle-hero-dark.webp differ diff --git a/docs/public/components/toggle-hero-light.webp b/docs/public/components/toggle-hero-light.webp new file mode 100644 index 00000000..86ae2d56 Binary files /dev/null and b/docs/public/components/toggle-hero-light.webp differ diff --git a/docs/public/components/toggle-light.webp b/docs/public/components/toggle-light.webp new file mode 100644 index 00000000..218deea4 Binary files /dev/null and b/docs/public/components/toggle-light.webp differ diff --git a/docs/public/components/tooltip-dark.webp b/docs/public/components/tooltip-dark.webp new file mode 100644 index 00000000..fef38f92 Binary files /dev/null and b/docs/public/components/tooltip-dark.webp differ diff --git a/docs/public/components/tooltip-hero-dark.webp b/docs/public/components/tooltip-hero-dark.webp new file mode 100644 index 00000000..068b691e Binary files /dev/null and b/docs/public/components/tooltip-hero-dark.webp differ diff --git a/docs/public/components/tooltip-hero-light.webp b/docs/public/components/tooltip-hero-light.webp new file mode 100644 index 00000000..03f5a192 Binary files /dev/null and b/docs/public/components/tooltip-hero-light.webp differ diff --git a/docs/public/components/tooltip-light.webp b/docs/public/components/tooltip-light.webp new file mode 100644 index 00000000..54d74ffb Binary files /dev/null and b/docs/public/components/tooltip-light.webp differ diff --git a/docs/public/components/tree-dark.webp b/docs/public/components/tree-dark.webp new file mode 100644 index 00000000..a6ee7997 Binary files /dev/null and b/docs/public/components/tree-dark.webp differ diff --git a/docs/public/components/tree-hero-dark.webp b/docs/public/components/tree-hero-dark.webp new file mode 100644 index 00000000..1f55b5a2 Binary files /dev/null and b/docs/public/components/tree-hero-dark.webp differ diff --git a/docs/public/components/tree-hero-light.webp b/docs/public/components/tree-hero-light.webp new file mode 100644 index 00000000..2ac77353 Binary files /dev/null and b/docs/public/components/tree-hero-light.webp differ diff --git a/docs/public/components/tree-light.webp b/docs/public/components/tree-light.webp new file mode 100644 index 00000000..ea195712 Binary files /dev/null and b/docs/public/components/tree-light.webp differ diff --git a/docs/public/components/virtual-list-dark.webp b/docs/public/components/virtual-list-dark.webp new file mode 100644 index 00000000..e547a3b3 Binary files /dev/null and b/docs/public/components/virtual-list-dark.webp differ diff --git a/docs/public/components/virtual-list-hero-dark.webp b/docs/public/components/virtual-list-hero-dark.webp new file mode 100644 index 00000000..0c23eb9a Binary files /dev/null and b/docs/public/components/virtual-list-hero-dark.webp differ diff --git a/docs/public/components/virtual-list-hero-light.webp b/docs/public/components/virtual-list-hero-light.webp new file mode 100644 index 00000000..6dfffb91 Binary files /dev/null and b/docs/public/components/virtual-list-hero-light.webp differ diff --git a/docs/public/components/virtual-list-light.webp b/docs/public/components/virtual-list-light.webp new file mode 100644 index 00000000..6146a722 Binary files /dev/null and b/docs/public/components/virtual-list-light.webp differ diff --git a/docs/public/home/calculator-dark.webp b/docs/public/home/calculator-dark.webp new file mode 100644 index 00000000..db67eb88 Binary files /dev/null and b/docs/public/home/calculator-dark.webp differ diff --git a/docs/public/home/calculator-light.webp b/docs/public/home/calculator-light.webp new file mode 100644 index 00000000..1d6d5198 Binary files /dev/null and b/docs/public/home/calculator-light.webp differ diff --git a/docs/public/home/deck-dark.webp b/docs/public/home/deck-dark.webp new file mode 100644 index 00000000..e6474fe2 Binary files /dev/null and b/docs/public/home/deck-dark.webp differ diff --git a/docs/public/home/deck-playlist-dark.webp b/docs/public/home/deck-playlist-dark.webp new file mode 100644 index 00000000..532617e3 Binary files /dev/null and b/docs/public/home/deck-playlist-dark.webp differ diff --git a/docs/public/home/feed-dark.webp b/docs/public/home/feed-dark.webp new file mode 100644 index 00000000..0b85761f Binary files /dev/null and b/docs/public/home/feed-dark.webp differ diff --git a/docs/public/home/feed-light.webp b/docs/public/home/feed-light.webp new file mode 100644 index 00000000..a63f22a5 Binary files /dev/null and b/docs/public/home/feed-light.webp differ diff --git a/docs/public/home/markdown-viewer-dark.webp b/docs/public/home/markdown-viewer-dark.webp new file mode 100644 index 00000000..928ec6a4 Binary files /dev/null and b/docs/public/home/markdown-viewer-dark.webp differ diff --git a/docs/public/home/markdown-viewer-light.webp b/docs/public/home/markdown-viewer-light.webp new file mode 100644 index 00000000..9d33ec32 Binary files /dev/null and b/docs/public/home/markdown-viewer-light.webp differ diff --git a/docs/public/home/notes-dark.webp b/docs/public/home/notes-dark.webp new file mode 100644 index 00000000..1767a973 Binary files /dev/null and b/docs/public/home/notes-dark.webp differ diff --git a/docs/public/home/notes-light.webp b/docs/public/home/notes-light.webp new file mode 100644 index 00000000..47caa566 Binary files /dev/null and b/docs/public/home/notes-light.webp differ diff --git a/docs/public/home/soundboard-dark.webp b/docs/public/home/soundboard-dark.webp new file mode 100644 index 00000000..05745fcb Binary files /dev/null and b/docs/public/home/soundboard-dark.webp differ diff --git a/docs/public/home/soundboard-light.webp b/docs/public/home/soundboard-light.webp new file mode 100644 index 00000000..c51cc0fa Binary files /dev/null and b/docs/public/home/soundboard-light.webp differ diff --git a/docs/public/home/system-monitor-dark.webp b/docs/public/home/system-monitor-dark.webp new file mode 100644 index 00000000..e483e7c4 Binary files /dev/null and b/docs/public/home/system-monitor-dark.webp differ diff --git a/docs/public/home/system-monitor-light.webp b/docs/public/home/system-monitor-light.webp new file mode 100644 index 00000000..853c55fe Binary files /dev/null and b/docs/public/home/system-monitor-light.webp differ diff --git a/docs/public/home/ui-inbox-macos.png b/docs/public/home/ui-inbox-macos.png new file mode 100644 index 00000000..23f86bf5 Binary files /dev/null and b/docs/public/home/ui-inbox-macos.png differ diff --git a/docs/public/wasm/component-preview.wasm b/docs/public/wasm/component-preview.wasm new file mode 100755 index 00000000..05c0bed6 Binary files /dev/null and b/docs/public/wasm/component-preview.wasm differ diff --git a/docs/src/app/app-model/page.mdx b/docs/src/app/app-model/page.mdx index afc09362..5c301847 100644 --- a/docs/src/app/app-model/page.mdx +++ b/docs/src/app/app-model/page.mdx @@ -1,339 +1,129 @@ # App Model -A zero-native app provides a name, optional WebView content, and optional lifecycle callbacks. The runtime owns the event loop, windows, native views, and native services; the platform owns the web engine and OS host. +A Native SDK app is one loop with four parts: -## The App struct +- **Model** — a plain Zig struct holding all app state. +- **Msg** — a tagged union of everything that can happen. +- **update(model, msg)** — the only place state changes. +- **View** — a Native markup file (`.native`, or a Zig view function) that derives the UI from the model. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
context*anyopaquePointer to your app state (required)
name[]const u8App name used in traces and automation snapshots (required)
sourceWebViewSourceInitial WebView content for compatibility startup windows; defaults to empty HTML
source_fn?fn(*anyopaque) !WebViewSourceDynamic source resolver (overrides source when set)
scene_fn?fn(*anyopaque) !ShellConfigDeclarative native window and view tree; when set, startup uses the scene instead of the compatibility window list
start_fn?fn(*anyopaque, *Runtime) !voidCalled after the runtime starts, before startup scene or source loading
event_fn?fn(*anyopaque, *Runtime, Event) !voidCalled on every runtime event (lifecycle + commands)
stop_fn?fn(*anyopaque, *Runtime) !voidCalled before the runtime shuts down
+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. -All callback fields are optional. A minimal app only needs `context` and `name`; provide `source` or `source_fn` when the app has WebView content, and `scene_fn` when startup should use a declarative native shell. +## The loop in full -## Startup shape - -Without `scene_fn`, zero-native uses the compatibility path: it loads `source` or `source_fn` into the configured startup window list. - -With `scene_fn`, zero-native materializes the returned `ShellConfig` as native shell windows and views. The first scene window adopts the startup native window; additional scene windows are created through the window service. `App.source` or `source_fn` still provides the main WebView content for those windows. - -Scene windows and views are kept as resize layout bindings, so return slices backed by static or app-owned storage that lives as long as the window. +The generated counter app (`native init`) is the complete shape. State and transitions in `src/main.zig`: ```zig -const shell_views = [_]zero_native.ShellView{ - .{ .label = "toolbar", .kind = .toolbar, .edge = .top, .height = 52 }, - .{ .label = "refresh", .kind = .button, .parent = "toolbar", .text = "Refresh", .command = "app.refresh" }, - .{ .label = "main", .kind = .webview, .url = "zero://inline", .fill = true }, - .{ .label = "status", .kind = .statusbar, .edge = .bottom, .height = 28, .text = "Ready" }, +pub const Msg = union(enum) { + increment, + decrement, + reset, }; -const shell_windows = [_]zero_native.ShellWindow{.{ - .label = "main", - .title = "Acme", - .width = 1100, - .height = 760, - .views = &shell_views, -}}; -const shell_scene: zero_native.ShellConfig = .{ .windows = &shell_windows }; -fn scene(context: *anyopaque) anyerror!zero_native.ShellConfig { - _ = context; - return shell_scene; -} +pub const Model = struct { + count: i64 = 0, +}; -fn app(self: *AppState) zero_native.App { - return .{ - .context = self, - .name = "acme", - .source = zero_native.WebViewSource.html("
Content
"), - .scene_fn = scene, - .event_fn = event, - }; +pub fn update(model: *Model, msg: Msg) void { + switch (msg) { + .increment => model.count += 1, + .decrement => model.count -= 1, + .reset => model.count = 0, + } } ``` -## WebViewSource +The view in `src/app.native` binds the model and names the messages: -Three constructors for specifying what the WebView loads: +```html + + + {count} + + +``` -- **`.html(content)`** -- inline HTML string, served as `zero://inline` -- **`.url(address)`** -- load a remote or local URL -- **`.assets(options)`** -- serve a local file tree through a custom origin +Markup can never mutate state. `{count}` is a read; `on-press="increment"` names a `Msg` variant. Every state change flows through `update`, which makes the app's behavior testable as a plain function — the generated `src/tests.zig` drives it with no GUI at all. -The assets constructor takes a `WebViewAssetSource`: +## Wiring + +`native_sdk.UiApp(Model, Msg)` ties the loop to the runtime. From the generated `main`: ```zig -.source = zero_native.WebViewSource.assets(.{ - .root_path = "dist", - .entry = "index.html", // default - .origin = "zero://app", // default - .spa_fallback = true, // default -}), +const CounterApp = native_sdk.UiApp(Model, Msg); + +pub fn main(init: std.process.Init) !void { + // `create` heap-allocates the multi-MB app struct and constructs the + // Model in place — neither ever rides the stack. + const app_state = try CounterApp.create(std.heap.page_allocator, .{ + .name = "my_app", + .scene = shell_scene, // one window, one gpu_surface view + .canvas_label = "main-canvas", // must match the scene's view label + .update = update, + .markup = .{ .source = app_markup, .watch_path = "src/app.native", .io = init.io }, + }); + defer app_state.destroy(); + app_state.model = initialModel(); // boot state: assign through the pointer + + try runner.runWithOptions(app_state.app(), .{ ... }, init); +} ``` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDefaultDescription
root_pathrequiredPath to the directory containing frontend assets
entry"index.html"HTML entry point within the root path
origin"zero://app"Origin used for asset URLs
spa_fallbacktrueServe entry for unknown routes (SPA mode)
+`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. -## Lifecycle events +## Rebuilds and widget identity -The runtime dispatches `LifecycleEvent` values through your `event_fn`: +After every `update`, the runtime rebuilds the view from the model. Rebuilds are cheap and safe by design: -- **`start`** -- the app runtime has started and startup scene or source loading is about to run -- **`activate`** -- the app became the active foreground app -- **`deactivate`** -- the app resigned active foreground status -- **`frame`** -- a frame has been requested (for animations or state updates) -- **`stop`** -- the app is shutting down +- **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 `` 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. -Native hosts also emit `app:activate` and `app:deactivate` to each open `window.zero` instance: +## Hot reload in development -```ts -window.zero.on("app:activate", () => { - refreshForegroundState(); -}); -``` +With `watch_path` set, the runtime watches the `.native` file while the app runs. Edits apply within about two seconds, preserving model state and widget identity. Parse failures keep the last good view on screen and record a diagnostic (`app_state.markup_diagnostic` carries line, column, and message). -Native file drops dispatch `Event.files_dropped` to `event_fn` and emit `drop:files` to trusted `window.zero` instances: +## Compile the markup for release -```ts -window.zero.on("drop:files", (event) => { - console.log(event.paths); -}); -``` - -## The runner pattern - -The generated `src/runner.zig` wires the runtime with platform services: - -1. Selects the platform (macOS, Linux, or null for headless tests) -2. Sets up trace sinks (stdout + file) via `FanoutTraceSink` -3. Installs panic capture so crashes write `last-panic.txt` -4. Initializes window state persistence from `windows.zon` -5. Creates the `Runtime` with all options and calls `runtime.run(app)` +In release builds the markup compiles at comptime — no parser in the binary, and markup or binding mistakes become compile errors with line and column: ```zig -var runtime = zero_native.Runtime.init(.{ - .platform = my_platform, - .trace_sink = fanout.sink(), - .bridge = my_app.bridge(), - .builtin_bridge = .{ .enabled = true, .commands = &builtin_policies }, - .security = .{ - .permissions = &app_permissions, - .navigation = .{ .allowed_origins = &.{ "zero://app" } }, - }, - .js_window_api = true, - .window_state_store = state_store, - .automation = if (build_options.automation) automation_server else null, -}); -try runtime.run(my_app.app()); +const dev = @import("builtin").mode == .Debug; +const App = native_sdk.UiAppWithFeatures(Model, Msg, .{ .runtime_markup = dev }); +const CompiledView = canvas.CompiledMarkupView(Model, Msg, @embedFile("app.native")); +// options: +.view = CompiledView.build, +.markup = if (dev) .{ .source = app_markup, .watch_path = "src/app.native", .io = init.io } else null, ``` -## RuntimeOptions +Both engines produce the identical widget tree — same structural ids, same typed handler table — so tests, automation scripts, and goldens hold across dev and release. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDefaultDescription
platformPlatformrequiredPlatform abstraction (macOS, Linux, or NullPlatform)
trace_sink?trace.SinknullDestination for structured trace records
log_path?[]const u8nullPath for persistent log file
extensions?ModuleRegistrynullExtension modules with lifecycle hooks
bridge?BridgeDispatchernullApp-defined bridge commands and handlers
builtin_bridgeBridgePolicy.{}Policy for built-in commands (dialogs, windows)
securitySecurityPolicy.{}Navigation allowlist, external links, permissions
automation?automation.ServernullFile-based automation server for testing
window_state_store?window_state.StorenullPersistent window geometry and state
js_window_apiboolfalseExpose built-in window.zero helper namespaces for trusted app chrome. Command helpers require origin and command checks, view helpers require origin and view checks, and window, WebView, and platform helpers require origin and window checks. Dialog, OS, clipboard, and credential helpers still require explicit builtin_bridge policy.
+## Hybrid views: a Zig root composing markup fragments -## Runtime methods +Compiled markup views compose the other way too: a hand-written Zig builder root can build markup fragments as ordinary children. This is the pattern for any UI that mixes custom Zig panes with declarative markup — the root places what the closed grammar cannot express (a scaled `ui.paragraph` display block, a `.band`-series `ui.chart`, per-row native context menus), and the markup keeps everything it can: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MethodDescription
init(options) RuntimeCreate a runtime
run(app) !voidEnter the platform event loop
createWindow(options) !WindowInfoOpen a new window
listWindows() []WindowInfoList open windows
focusWindow(id) !voidBring a window to front
closeWindow(id) !voidClose a window
invalidate()Request a redraw
invalidateFor(reason, dirty_region)Request a redraw with reason and optional dirty region
frameDiagnostics() FrameDiagnosticsReturn stats from the last frame
dispatchEvent(event)Inject a synthetic event
dispatchPlatformEvent(app, event)Forward a platform event
automationSnapshot()Write state to automation directory
+```zig +const CompiledHeaderView = canvas.CompiledMarkupView(Model, Msg, @embedFile("header.native")); + +pub fn rootView(ui: *Ui, model: *const Model) Ui.Node { + return ui.column(.{ .gap = 12, .grow = 1 }, .{ + CompiledHeaderView.build(ui, model), // the markup fragment, as a child + ui.paragraph(.{}, &.{ + .{ .text = model.readout(ui.arena), .monospace = true, .scale = 1.6 }, + }), + }); +} +// options: .view = rootView, +``` + +`examples/calculator` is the smallest live reference (`CompiledKeypadView.build(ui, model)` inside a hand-written root); `system-monitor`, `soundboard`, and `deck` use the same shape. Widget ids, handlers, and dispatch are identical to a pure-markup tree, so tests and automation address the fragment's widgets the usual way. + +A Zig-root app keeps dev-time hot reload for its embedded fragments too: build with `UiAppWithFeatures(Model, Msg, .{ .runtime_markup = dev_markup_reload })` and pass `.markup = .{ .source = ..., .watch_path = "src/header.native", .io = init.io }` gated on `builtin.mode == .Debug` (`null` otherwise) — the wiring in `examples/notes`. Debug builds then reload edits to the watched file in place; release builds compile the runtime engine out entirely. + +## Side effects + +`update` stays pure by routing anything asynchronous — subprocesses, HTTP, file persistence, timers, clipboard — through the effects channel: declare `.update_fx` instead of `.update` and spawn from message arms; results come back as ordinary messages. Boot-time work goes in `.init_fx`, which runs exactly once before the first paint. See [Native UI: Effects](/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). diff --git a/docs/src/app/app-zon/page.mdx b/docs/src/app/app-zon/page.mdx index 51794a75..fb6fdb9b 100644 --- a/docs/src/app/app-zon/page.mdx +++ b/docs/src/app/app-zon/page.mdx @@ -1,28 +1,71 @@ -# app.zon Reference +# Config -The `app.zon` manifest declares app metadata, permissions, bridge policies, security rules, and window layout. It is read by the CLI and tooling at build, package, and validation time. +The `app.zon` manifest declares app metadata, permissions, security rules, window layout, and packaging inputs. It is read by the CLI and tooling at build, package, and validation time. -## Example +## Example: native-rendered app + +The manifest `native init` generates — identity, one shell window with a GPU surface view, and the minimal permission set: ```zig .{ - .id = "dev.zero_native", - .name = "zero-native", - .display_name = "zero-native", + .id = "dev.native_sdk.my-app", + .name = "my-app", + .display_name = "My App", + .description = "A counter that lives in one native window.", .version = "0.1.0", - .icons = .{ "assets/icon.icns", "assets/icon.ico" }, + .icons = .{"assets/icon.png"}, + .platforms = .{"macos"}, + .permissions = .{ "view", "command" }, + .capabilities = .{ "native_views", "gpu_surfaces" }, + .shell = .{ + .windows = .{ + .{ + .label = "main", + .title = "My App", + .width = 480, + .height = 320, + .restore_state = false, + .restore_policy = "center_on_primary", + .views = .{ + .{ .label = "main-canvas", .kind = "gpu_surface", .fill = true, .role = "Counter canvas", .accessibility_label = "Counter", .gpu_backend = "metal", .gpu_pixel_format = "bgra8_unorm", .gpu_present_mode = "timer", .gpu_alpha_mode = "opaque", .gpu_color_space = "srgb", .gpu_vsync = true }, + }, + }, + }, + }, + .security = .{ + .navigation = .{ + .allowed_origins = .{ "zero://app", "zero://inline" }, + .external_links = .{ .action = "deny" }, + }, + }, + .web_engine = "system", + .cef = .{ .dir = "third_party/cef/macos", .auto_install = false }, +} +``` + +## 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: + +```zig +.{ + .id = "dev.native_sdk", + .name = "native-sdk", + .display_name = "native-sdk", + .version = "0.1.0", + .icons = .{"assets/icon.png"}, .platforms = .{ "macos" }, .permissions = .{ "command", "view", "dialog", "window", "clipboard", "credentials" }, - .capabilities = .{ "webview", "js_bridge", "native_views", "menus", "shortcuts", "dialog", "clipboard", "credentials" }, + .capabilities = .{ "webview", "js_bridge", "native_views", "gpu_surfaces", "menus", "shortcuts", "dialog", "clipboard", "credentials" }, .bridge = .{ .commands = .{ .{ .name = "native.ping", .origins = .{ "zero://app" } }, - .{ .name = "zero-native.window.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.command.invoke", .permissions = .{ "command" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.view.list", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.dialog.showMessage", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.clipboard.readText", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.credentials.get", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.window.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.command.invoke", .permissions = .{ "command" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.view.list", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.dialog.showMessage", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.clipboard.readText", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.credentials.get", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } }, }, }, .security = .{ @@ -34,7 +77,7 @@ The `app.zon` manifest declares app metadata, permissions, bridge policies, secu .web_engine = "system", .cef = .{ .dir = "third_party/cef/macos", .auto_install = false }, .windows = .{ - .{ .label = "main", .title = "zero-native", .width = 720, .height = 480, .restore_state = true }, + .{ .label = "main", .title = "native-sdk", .width = 720, .height = 480, .restore_state = true }, }, .commands = .{ .{ .id = "command.palette", .title = "Command Palette" }, @@ -62,7 +105,7 @@ The `app.zon` manifest declares app metadata, permissions, bridge policies, secu }, }, .url_schemes = .{ - .{ .scheme = "zero-native" }, + .{ .scheme = "native-sdk" }, }, } ``` @@ -87,15 +130,19 @@ The `app.zon` manifest declares app metadata, permissions, bridge policies, secu display_name - Human-readable app name (menu bar, window title fallback) + Human-readable app name — the application menu, Dock, app switcher, and About panel all use it, in dev runs and packaged bundles alike + + + description + Optional one-line description shown in the About panel (max 256 bytes, single line) version - Semver version string + Semver version string — also the version the About panel shows icons - Paths to icon files for packaging + The app icon: one square .png (1:1, ideally 1024x1024) or .svg source that packaging turns into every platform's icon artifacts — the macOS .icns gets the platform's rounded-rect icon shape applied automatically. A prebuilt .icns/.ico entry ships untouched for its platform instead platforms @@ -121,6 +168,10 @@ The `app.zon` manifest declares app metadata, permissions, bridge policies, secu web_engine system or chromium; Chromium is currently supported for macOS builds (see Web Engines) + + theme + Built-in theme pack: house (default) or geist; an unknown name is a build/check error (see Theming) + cef CEF runtime config for Chromium apps: dir and auto_install @@ -162,12 +213,14 @@ The `app.zon` manifest declares app metadata, permissions, bridge policies, secu ## `shell` -The optional `shell` block declares native-first windows with explicit view trees. Existing `windows` entries remain the simple compatibility path; `shell.windows` is the richer contract for native chrome, native controls, WebViews, and future GPU surfaces. +The optional `shell` block declares native-first windows with explicit view trees. Existing `windows` entries remain the simple compatibility path; `shell.windows` is the richer contract for native chrome, native controls, WebViews, and GPU surfaces. Tooling parses and validates the schema. Runtime code can return the same shape from `App.scene_fn`, materialize a parsed shell window with `runtime.createShellWindow(...)`, or attach a shell view list to an existing window with `runtime.createShellViews(...)`; platform hosts implement view kinds progressively, so unsupported native kinds fail at runtime with an explicit unsupported error until that backend grows support. When an app uses both `windows` and `shell.windows`, labels must stay unique across both lists. Use `windows` for the simple compatibility path or `shell.windows` for native-first structure; do not define two window entries with the same label. +For a scene-first app — a `UiApp` passing its Zig scene (`shell_scene`) to the runner — the scene is authoritative at runtime: it re-applies size, title, and views when it loads. `app.zon`'s `.shell.windows[0]` exists because the host creates the startup window before the scene loads, and create-time-only properties must come from the manifest: `titlebar` chrome, `min_width`/`min_height` floors, and the show mode (canvas-first windows are created hidden and shown after the first frame presents). The numbers appearing in both places is by design — edit the scene for anything that can change after create (size, title, views), and the manifest for create-time chrome and floors. + ```zig .shell = .{ .windows = .{ @@ -183,6 +236,7 @@ When an app uses both `windows` and `shell.windows`, labels must stay unique acr .{ .label = "sidebar-stack", .kind = "stack", .parent = "sidebar", .x = 16, .y = 16, .width = 220, .height = 120, .axis = "column" }, .{ .label = "sidebar-live", .kind = "checkbox", .parent = "sidebar-stack", .accessibility_label = "Toggle live updates", .text = "Live updates" }, .{ .label = "content", .kind = "webview", .parent = "body", .url = "zero://app/index.html", .fill = true }, + .{ .label = "canvas", .kind = "gpu_surface", .parent = "body", .width = 480, .gpu_backend = "metal", .gpu_pixel_format = "bgra8_unorm", .gpu_present_mode = "timer", .gpu_alpha_mode = "opaque", .gpu_color_space = "srgb", .gpu_vsync = true }, .{ .label = "status", .kind = "statusbar", .edge = "bottom", .height = 24, .text = "Ready" }, }, }, @@ -190,9 +244,11 @@ When an app uses both `windows` and `shell.windows`, labels must stay unique acr }, ``` +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 Native UI). Platforms without the concept keep standard chrome. The same `titlebar` 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`. 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 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`. `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. @@ -281,7 +337,7 @@ URL schemes require a lowercase custom `scheme`. Reserved schemes such as `http` ## `frontend.dev` -The optional `frontend.dev` block configures the managed dev server for `zero-native dev` and `zig build dev`: +The optional `frontend.dev` block configures the managed dev server for `native dev` and `zig build dev`: ```zig .frontend = .{ @@ -327,6 +383,6 @@ The optional `frontend.dev` block configures the managed dev server for `zero-na ## Validation ```bash -zero-native validate app.zon -zero-native doctor --manifest app.zon --strict +native validate app.zon +native doctor --manifest app.zon --strict ``` diff --git a/docs/src/app/automation/page.mdx b/docs/src/app/automation/page.mdx index 3b0f3088..9c70ffee 100644 --- a/docs/src/app/automation/page.mdx +++ b/docs/src/app/automation/page.mdx @@ -1,26 +1,26 @@ # Automation -The automation server exposes runtime state and accepts commands via a file-based protocol. Use it for integration testing, CI smoke tests, and inspecting running apps. +The automation server exposes runtime state and accepts commands via a file-based protocol. A running app publishes an accessibility snapshot — every widget with its id, role, name, bounds, and state — and accepts scripted clicks, keys, drags, and assertions against it. Use it for integration testing, CI smoke tests, inspecting running apps, and letting AI agents verify their own UI work. ## Enabling automation -Build with the automation flag: +Build with the automation flag (`native` verbs forward `-D` flags to the underlying `zig build`): ```bash -zig build run-webview -Dautomation=true +native build -Dautomation=true ``` In your runner, pass an `automation.Server` to `RuntimeOptions`: ```zig -const server = zero_native.automation.Server.init(io, ".zig-cache/zero-native-automation", "My App"); -var runtime = zero_native.Runtime.init(.{ +const server = native_sdk.automation.Server.init(io, ".zig-cache/native-sdk-automation", "My App"); +var runtime = native_sdk.Runtime.init(.{ .platform = my_platform, .automation = server, }); ``` -The default directory is `.zig-cache/zero-native-automation`. +The default directory is `.zig-cache/native-sdk-automation`. ## File protocol @@ -36,34 +36,38 @@ When the runtime publishes a snapshot, it writes these files to the automation d snapshot.txt - Runtime state: source kind, window metadata, native/WebView metadata including role, accessibility label, text, and focus state, ready=true/false + Runtime state: source kind, window metadata, native/WebView metadata including role, accessibility label, text, and focus state, ready=true/false, and markup_watch=armed|off in the header — whether the markup hot-reload watch is armed (only in builds where the app wired .markup with a watch_path and io, or registered compiled fragments through fragment_watch — i.e. Debug dev builds) accessibility.txt - Accessibility tree summary with native view roles and accessible names. Explicit accessibility labels are used before visible text. + Accessibility tree summary with native view roles and accessible names. An explicit label= REPLACES the visible text as the accessible name — snapshot greps and screen readers see the label, not the text, so don't label an element whose visible text your assertions grep for. windows.txt Window list: window @w{"{id}"} "{"{title}"}" focused={"{bool}"} per line - screenshot.ppm - Screenshot in PPM format (currently a 2x2 placeholder) + screenshot-<view-label>.png + Deterministic PNG of a gpu_surface view, rendered through the CPU reference renderer on screenshot <view-label> [scale] - command.txt - Command input: written by the CLI, consumed by the runtime + command-<n>.txt + Command queue: one entry per command, written by the CLI, consumed oldest-first by the runtime (which deletes the entry as its consumption ack) bridge-response.txt JSON response from the last bridge command + + provenance.txt + Response to the last provenance command: where a live widget was authored (file, byte span, line:column, template chain, iteration keys) + ## Commands -The runtime polls `command.txt` and processes these actions: +The runtime watches the command queue and processes these actions: @@ -85,6 +89,10 @@ The runtime polls `command.txt` and processes these actions: + + + + @@ -97,10 +105,46 @@ The runtime polls `command.txt` and processes these actions: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -109,46 +153,70 @@ The runtime polls `command.txt` and processes these actions: + + + +
resize <width> <height> [<scale>] Dispatch a main-window resize event and relayout native/WebView surfaces
provenance <view-label> (<widget-id> | at <x> <y>)Report where a live widget was authored into provenance.txt: markup file, byte span, line:column, template use-site chain, and iteration keys. Point queries hit-test view-local coordinates.
bridge <json> Send a bridge command with origin zero://inlinenative-command <id> [<view-label>] Dispatch a native view command event for the main window
widget-action <view-label> <widget-id> <action> [<value>]Invoke a retained canvas widget action
widget-click <view-label> <widget-id>Dispatch pointer down/up at a retained canvas widget
widget-hold <view-label> <widget-id>Press-and-hold: pointer down, the reserved hold timer fired, then the suppressed release — the widget's on_hold Msg through the real gesture path
widget-context-press <view-label> <widget-id>Secondary click (right/ctrl-click): presents the widget's context menu, or dispatches on_hold immediately when the route has none
widget-context-menu <view-label> <widget-id> <item-index>Invoke a declared context-menu item by 0-based index (as the snapshot's context_menu=[...] lists them) — the selection dispatches as the same context_menu_action platform event a real pick produces; presentation is skipped because the OS menu's tracking loop cannot be driven. Refuses undeclared menus, out-of-range indices, separators, and disabled items by name
widget-drag <view-label> <widget-id> <start-x-ratio> <end-x-ratio> [<start-y-ratio> <end-y-ratio>]Dispatch pointer down/drag/up across a retained canvas widget
widget-wheel <view-label> <widget-id> <delta-y>Dispatch wheel input at a retained canvas widget
widget-key <view-label> <key> [<text>]Dispatch key input to the focused retained canvas widget
shortcut <id> Dispatch a shortcut command event for the main window
tray-action <item-id>Select a status-item dropdown row (ids from the snapshot's tray-item #id lines)
focus <view-label> Focus a native or WebView-backed view in the main windowfocus-next / focus-previous Move focus through visible, enabled views in runtime order
profile <on|off>Toggle per-stage frame timing. While on, the snapshot carries a frame_profile line with rolling p50/p90/max microseconds per pipeline stage (rebuild, layout, reconcile, emit, a11y, plan, patch, encode, present, host_decode, host_draw)
-After processing a command, the runtime writes `done` to `command.txt`. +Commands queue as `command-.txt` entries: each `native automate ` claims the next sequence number exclusively (rapid back-to-back invocations can never overwrite each other), the app consumes one entry per presented frame in strict arrival order, and deleting the entry is the consumption ack. The queue is bounded to a handful of entries — a writer finding it full retries until the app drains a slot and fails loudly (non-zero exit) if it never does. `native automate ` prints `delivered -> ` only after the app consumed its entry, so a dead or frozen app is always a loud failure, never a silently dropped command. + +Widget verbs and `screenshot` address a `gpu_surface` view by its label across ALL open windows — snapshots enumerate every window's views and widgets, so a model-declared secondary window's canvas (a settings window) is drivable and capturable exactly like the main one, with no window argument. ## CLI usage -The `zero-native automate` subcommand interacts with the automation directory: +The `native automate` subcommand interacts with the automation directory: ```bash # Wait for the app to be ready (polls snapshot.txt for ready=true) -zero-native automate wait +native automate wait + +# Assert on the snapshot: each argument is a regex that must match +# (polls up to --timeout-ms, default 30000; --absent inverts) +native automate assert 'gpu_nonblank=true' 'role=button name="Reset"' +native automate assert --absent 'error event=' # List running automation-enabled apps -zero-native automate list +native automate list # Dump the current snapshot -zero-native automate snapshot +native automate snapshot -# Capture a screenshot -zero-native automate screenshot +# Render a gpu_surface view to screenshot-main-canvas.png +native automate screenshot main-canvas # Reload the WebView -zero-native automate reload +native automate reload # Resize the main window surface -zero-native automate resize 900 640 +native automate resize 900 640 # Dispatch command-source events -zero-native automate menu-command app.refresh -zero-native automate native-command app.refresh refresh-button -zero-native automate shortcut app.refresh +native automate menu-command app.refresh +native automate native-command app.refresh refresh-button +native automate shortcut app.refresh + +# Drive retained canvas widgets +native automate widget-action canvas 2 press +native automate widget-click canvas 3 +native automate widget-hold canvas 3 # press-and-hold (on_hold) +native automate widget-context-press canvas 3 # right-click (presents the menu) +native automate widget-context-menu canvas 3 1 # pick declared menu item 1 # Drive focus between native controls and WebViews -zero-native automate focus refresh-button -zero-native automate focus-next -zero-native automate focus-previous +native automate focus refresh-button +native automate focus-next +native automate focus-previous + +# Toggle per-stage frame timing; the snapshot then carries a +# frame_profile line (rolling p50/p90/max us per pipeline stage) +native automate profile on +native automate snapshot | grep -o 'frame_profile.*' +native automate profile off # Send a bridge command and get the response -zero-native automate bridge '{"id":"1","command":"native.ping","payload":{"source":"automation"}}' +native automate bridge '{"id":"1","command":"native.ping","payload":{"source":"automation"}}' ``` ## Testing with automation @@ -156,21 +224,45 @@ zero-native automate bridge '{"id":"1","command":"native.ping","payload":{"sourc The WebView and native-shell smoke build steps demonstrate full automation test flows: 1. Build and start the app with `-Dautomation=true` -2. Run `zero-native automate wait` to block until the app is ready -3. Run `zero-native automate snapshot` to verify window, source, and native/WebView metadata -4. Run `zero-native automate resize ...`, `zero-native automate bridge '...'`, focus traversal, or command-source actions such as `zero-native automate native-command ...` +2. Run `native automate wait` to block until the app is ready +3. Run `native automate snapshot` to verify window, source, and native/WebView metadata +4. Run `native automate resize ...`, `native automate bridge '...'`, focus traversal, or command-source actions such as `native automate native-command ...` 5. Verify bridge responses, relayout bounds, focus state, and updated snapshots ```bash zig build test-webview-smoke -Dplatform=macos ``` +## Write-back + +Because views are data, automation can go the other way too: select a widget in the running app, jump to the markup that authored it, and write an edit back into the source file — the app's own hot-reload watch picks the change up and repaints. + +The read half is `provenance`. Every markup-built widget's structural id maps back to its authored source, captured at view build time: the file, the node's byte span and line:column, the template instantiation chain (a widget inside a template reports both its definition site and every `` that put it there), and the iteration keys that say which `` row it is. Widgets built with the Zig builder report `authored=zig` honestly — write-back edits markup files only. + +```bash +# Where does this widget come from? (id from the snapshot, or hit-test a point) +native automate provenance kanban-canvas 6624116744891006388 +native automate provenance kanban-canvas at 760 30 +``` + +The write half is `edit`: typed, minimal-diff operations on the file the widget came from. An operation changes only bytes inside the target node's span — whitespace, comments, attribute order, and every other node survive byte for byte, proven by reparsing and diffing the parse trees before anything is written. + +```bash +native automate edit kanban-canvas 6624116744891006388 set-text "Add task" +native automate edit kanban-canvas 6624116744891006388 set-attr variant secondary +native automate edit kanban-canvas 6624116744891006388 remove-attr variant +``` + +Edits refuse rather than guess: a widget authored in Zig, a file that changed on disk since the app loaded it (the provenance response carries the loaded bytes' hash, so concurrent edits are never clobbered), or an edit that would fail markup validation all stop with a teaching error and leave the file untouched. A successful edit needs no reload command — the markup watch (`MarkupOptions.watch_path`, a dev/Debug feature) reloads within its poll interval, so `native automate assert` on the snapshot is the way to await the repaint. + +`zig build test-writeback-smoke` (macOS) drives the whole loop against the kanban example: query provenance, flip the button label through the verb, assert the repaint, verify the byte-exact diff, and flip it back. + ## Custom directory Pass a custom path to `automation.Server.init()`: ```zig -const server = zero_native.automation.Server.init(io, "/tmp/my-app-automation", "My App"); +const server = native_sdk.automation.Server.init(io, "/tmp/my-app-automation", "My App"); ``` -The CLI reads from the default `.zig-cache/zero-native-automation` unless you specify a directory via the automation subcommand. +The CLI reads from the default `.zig-cache/native-sdk-automation` unless you specify a directory via the automation subcommand. diff --git a/docs/src/app/bridge/builtin-commands/page.mdx b/docs/src/app/bridge/builtin-commands/page.mdx index b408e4f5..5144de0f 100644 --- a/docs/src/app/bridge/builtin-commands/page.mdx +++ b/docs/src/app/bridge/builtin-commands/page.mdx @@ -1,6 +1,6 @@ # Builtin Commands -zero-native provides built-in bridge commands for app command routing, window management, generic native views, layered WebViews, platform support queries, native dialogs, selected OS capabilities, and credential storage. These are controlled by the `builtin_bridge` policy in `RuntimeOptions`, separate from app-defined bridge handlers. +The Native SDK provides built-in bridge commands for app command routing, window management, generic native views, layered WebViews, platform support queries, native dialogs, selected OS capabilities, and credential storage. These are controlled by the `builtin_bridge` policy in `RuntimeOptions`, separate from app-defined bridge handlers. ## Command Routing @@ -14,12 +14,12 @@ zero-native provides built-in bridge commands for app command routing, window ma - zero-native.command.invoke + native-sdk.command.invoke command Dispatch an Event.command into the app runtime from the calling WebView - zero-native.command.list + native-sdk.command.list command List the manifest command catalog loaded into the runtime @@ -42,7 +42,7 @@ Native controls can also bind a `command` when created with `runtime.createView( - zero-native.platform.supports + native-sdk.platform.supports window Return whether the current platform and web engine support a feature @@ -63,22 +63,22 @@ Platform support queries are available through `window.zero.platform.supports(.. - zero-native.window.list + native-sdk.window.list window List all open windows - zero-native.window.create + native-sdk.window.create window Create a new window - zero-native.window.focus + native-sdk.window.focus window Focus a window by ID - zero-native.window.close + native-sdk.window.close window Close a window by ID @@ -99,49 +99,49 @@ Window commands are available through `window.zero.windows.*` when `js_window_ap - zero-native.view.create + native-sdk.view.create view Create a generic native view or WebView-backed view in the calling window - zero-native.view.list + native-sdk.view.list view List generic views and WebViews in the calling window - zero-native.view.update + native-sdk.view.update view Patch frame, layer, visibility, enabled state, role, text, command, or WebView URL - zero-native.view.setFrame + native-sdk.view.setFrame view Move or resize a view - zero-native.view.setVisible + native-sdk.view.setVisible view Show or hide a view - zero-native.view.focus + native-sdk.view.focus view Focus a view when the backend supports native focus for that kind - zero-native.view.focusNext / zero-native.view.focusPrevious + native-sdk.view.focusNext / native-sdk.view.focusPrevious view Move focus through visible, enabled native controls and WebView-backed views - zero-native.view.close + native-sdk.view.close view Close a generic view or child WebView -View commands are available through `window.zero.views.*` when `js_window_api` is `true`. They use origin checks and the `view` permission when runtime permissions are configured; the legacy `window` permission is still accepted for compatibility. `windowId` must match the calling window when provided. The `update(label, patch)`, `focus(label)`, and `close(label)` helpers accept string labels for the calling window; pass selector objects when you need an explicit `windowId`. View responses include frame, layer, visibility, enabled, focus, role, accessibility label, text, command, and open state. `kind: "webview"` routes through the WebView backend. The macOS, Linux, and Windows system-WebView backends support native `toolbar`, `titlebar_accessory`, `sidebar`, `statusbar`, `split`, `stack`, `button`, `icon_button`, `list_item`, `checkbox`, `toggle`, `segmented_control`, `text_field`, `search_field`, `label`, `spacer`, and `progress_indicator` views; Chromium hosts and unsupported native kinds return explicit unsupported-backend errors until implemented. +View commands are available through `window.zero.views.*` when `js_window_api` is `true`. They use origin checks and the `view` permission when runtime permissions are configured; the legacy `window` permission is still accepted for compatibility. `windowId` must match the calling window when provided. The `update(label, patch)`, `focus(label)`, and `close(label)` helpers accept string labels for the calling window; pass selector objects when you need an explicit `windowId`. View responses include frame, layer, visibility, enabled, focus, role, accessibility label, text, command, cursor, and open state. GPU responses also include surface presentation fields, input latency budget fields, retained canvas frame counters, and retained widget counters. `kind: "webview"` routes through the WebView backend. The macOS, Linux, and Windows system-WebView backends support native `toolbar`, `titlebar_accessory`, `sidebar`, `statusbar`, `split`, `stack`, `button`, `icon_button`, `list_item`, `checkbox`, `toggle`, `segmented_control`, `text_field`, `search_field`, `label`, `spacer`, and `progress_indicator` views. The macOS system-WebView backend also supports `gpu_surface`; Chromium hosts and unsupported native kinds return explicit unsupported-backend errors until implemented. ## WebView Commands @@ -155,37 +155,37 @@ View commands are available through `window.zero.views.*` when `js_window_api` i - zero-native.webview.create + native-sdk.webview.create window Create a named WebView in a window - zero-native.webview.list + native-sdk.webview.list window List WebViews in the calling window - zero-native.webview.setFrame + native-sdk.webview.setFrame window Move or resize a WebView - zero-native.webview.navigate + native-sdk.webview.navigate window Navigate a WebView - zero-native.webview.setZoom + native-sdk.webview.setZoom window Set page zoom from 0.25 to 5.0 - zero-native.webview.setLayer + native-sdk.webview.setLayer window Change native stack order - zero-native.webview.close + native-sdk.webview.close window Close a WebView @@ -208,17 +208,17 @@ If `windowId` is omitted, the runtime uses the window that sent the bridge messa - zero-native.dialog.openFile + native-sdk.dialog.openFile dialog Show a file open dialog - zero-native.dialog.saveFile + native-sdk.dialog.saveFile dialog Show a file save dialog - zero-native.dialog.showMessage + native-sdk.dialog.showMessage dialog Show a message dialog @@ -239,34 +239,34 @@ Dialog commands are **always default-deny** and require an explicit `builtin_bri - zero-native.os.openUrl + native-sdk.os.openUrl network Open an allowed http:// or https:// URL in the system browser - zero-native.os.showNotification + native-sdk.os.showNotification notifications Show a native system notification with a title, optional subtitle, and optional body - zero-native.os.revealPath + native-sdk.os.revealPath filesystem Reveal a local file or folder path in the platform file manager - zero-native.os.addRecentDocument + native-sdk.os.addRecentDocument filesystem Add a local path to the platform recent documents list - zero-native.os.clearRecentDocuments + native-sdk.os.clearRecentDocuments filesystem Clear recent documents registered by the application where the platform supports it -OS commands are **always default-deny** and require an explicit `builtin_bridge` policy. `zero-native.os.openUrl` also checks `security.navigation.external_links`; the URL must match the external link allowlist before the platform service is called. macOS, Linux, and Windows system WebView hosts implement `openUrl`, `revealPath`, notifications, and recent-document commands; macOS Chromium also implements the current OS command set. Other platform hosts return `invalid_request` with the standard unsupported-service message until implemented. +OS commands are **always default-deny** and require an explicit `builtin_bridge` policy. `native-sdk.os.openUrl` also checks `security.navigation.external_links`; the URL must match the external link allowlist before the platform service is called. macOS, Linux, and Windows system WebView hosts implement `openUrl`, `revealPath`, notifications, and recent-document commands; macOS Chromium also implements the current OS command set. Other platform hosts return `invalid_request` with the standard unsupported-service message until implemented. ## Credential Commands @@ -280,17 +280,17 @@ OS commands are **always default-deny** and require an explicit `builtin_bridge` - zero-native.credentials.set + native-sdk.credentials.set credentials Store a secret by service and account - zero-native.credentials.get + native-sdk.credentials.get credentials Read a secret by service and account, returning null when it is missing - zero-native.credentials.delete + native-sdk.credentials.delete credentials Delete a stored secret by service and account @@ -311,22 +311,22 @@ Credential commands are **always default-deny** and require an explicit `builtin - zero-native.clipboard.readText + native-sdk.clipboard.readText clipboard Read text/plain from the system clipboard - zero-native.clipboard.writeText + native-sdk.clipboard.writeText clipboard Write text/plain to the system clipboard - zero-native.clipboard.read + native-sdk.clipboard.read clipboard Read clipboard data by MIME type and return {'{ mimeType, data }'} - zero-native.clipboard.write + native-sdk.clipboard.write clipboard Write clipboard data by MIME type @@ -339,15 +339,15 @@ Clipboard commands are **always default-deny** and require an explicit `builtin_ ```zig const app_permissions = [_][]const u8{ - zero_native.security.permission_command, - zero_native.security.permission_view, - zero_native.security.permission_dialog, - zero_native.security.permission_window, - zero_native.security.permission_network, - zero_native.security.permission_filesystem, - zero_native.security.permission_clipboard, - zero_native.security.permission_notifications, - zero_native.security.permission_credentials, + native_sdk.security.permission_command, + native_sdk.security.permission_view, + native_sdk.security.permission_dialog, + native_sdk.security.permission_window, + native_sdk.security.permission_network, + native_sdk.security.permission_filesystem, + native_sdk.security.permission_clipboard, + native_sdk.security.permission_notifications, + native_sdk.security.permission_credentials, }; .security = .{ @@ -363,42 +363,42 @@ const app_permissions = [_][]const u8{ .builtin_bridge = .{ .enabled = true, .commands = &.{ - .{ .name = "zero-native.window.list", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.command.invoke", .permissions = .{ "command" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.command.list", .permissions = .{ "command" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.platform.supports", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.window.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.view.create", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.view.list", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.view.update", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.view.setFrame", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.view.setVisible", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.view.focus", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.view.focusNext", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.view.focusPrevious", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.view.close", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.webview.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.webview.list", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.webview.setFrame", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.webview.navigate", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.webview.setZoom", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.webview.setLayer", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.webview.close", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.dialog.openFile", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.dialog.saveFile", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.dialog.showMessage", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.os.openUrl", .permissions = .{ "network" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.os.showNotification", .permissions = .{ "notifications" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.os.revealPath", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.os.addRecentDocument", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.os.clearRecentDocuments", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.clipboard.readText", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.clipboard.writeText", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.clipboard.read", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.clipboard.write", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.credentials.set", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.credentials.get", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } }, - .{ .name = "zero-native.credentials.delete", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.window.list", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.command.invoke", .permissions = .{ "command" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.command.list", .permissions = .{ "command" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.platform.supports", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.window.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.view.create", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.view.list", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.view.update", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.view.setFrame", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.view.setVisible", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.view.focus", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.view.focusNext", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.view.focusPrevious", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.view.close", .permissions = .{ "view" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.webview.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.webview.list", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.webview.setFrame", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.webview.navigate", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.webview.setZoom", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.webview.setLayer", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.webview.close", .permissions = .{ "window" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.dialog.openFile", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.dialog.saveFile", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.dialog.showMessage", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.os.openUrl", .permissions = .{ "network" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.os.showNotification", .permissions = .{ "notifications" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.os.revealPath", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.os.addRecentDocument", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.os.clearRecentDocuments", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.clipboard.readText", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.clipboard.writeText", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.clipboard.read", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.clipboard.write", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.credentials.set", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.credentials.get", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } }, + .{ .name = "native-sdk.credentials.delete", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } }, }, }, ``` @@ -449,12 +449,12 @@ const commands = await window.zero.commands.list(); const hasNativeViews = await window.zero.platform.supports("native_views"); -const files = await window.zero.invoke("zero-native.dialog.openFile", { +const files = await window.zero.invoke("native-sdk.dialog.openFile", { title: "Select a file", allowMultiple: true, }); -const result = await window.zero.invoke("zero-native.dialog.showMessage", { +const result = await window.zero.invoke("native-sdk.dialog.showMessage", { style: "warning", title: "Confirm", message: "Are you sure?", @@ -465,18 +465,18 @@ const result = await window.zero.invoke("zero-native.dialog.showMessage", { await window.zero.os.openUrl("https://example.com/docs/start"); await window.zero.os.showNotification({ title: "Build finished", - subtitle: "zero-native", + subtitle: "native-sdk", body: "All checks passed.", }); await window.zero.os.revealPath("/Users/me/Downloads/report.pdf"); await window.zero.os.addRecentDocument("/Users/me/Downloads/report.pdf"); await window.zero.os.clearRecentDocuments(); -await window.zero.clipboard.writeText("Copied from zero-native"); +await window.zero.clipboard.writeText("Copied from Native SDK"); const text = await window.zero.clipboard.readText(); await window.zero.clipboard.write({ mimeType: "text/html", - data: "Copied from zero-native", + data: "Copied from Native SDK", }); const html = await window.zero.clipboard.read({ mimeType: "text/html" }); diff --git a/docs/src/app/bridge/page.mdx b/docs/src/app/bridge/page.mdx index 4259fb1e..629f2503 100644 --- a/docs/src/app/bridge/page.mdx +++ b/docs/src/app/bridge/page.mdx @@ -1,6 +1,6 @@ # Bridge -The bridge connects JavaScript in the WebView to native Zig handlers via JSON messages. +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)). ## Architecture @@ -19,7 +19,7 @@ window.zero.invoke(cmd, payload) ## Defining a handler ```zig -fn ping(context: *anyopaque, invocation: zero_native.bridge.Invocation, output: []u8) anyerror![]const u8 { +fn ping(context: *anyopaque, invocation: native_sdk.bridge.Invocation, output: []u8) anyerror![]const u8 { _ = invocation; const self: *App = @ptrCast(@alignCast(context)); self.ping_count += 1; @@ -30,13 +30,13 @@ fn ping(context: *anyopaque, invocation: zero_native.bridge.Invocation, output: The handler writes its JSON result into the provided `output` buffer (max 12 KiB) and returns a slice of it. Results must be valid JSON values; invalid raw text is rejected with `handler_failed`. When returning user data as a string, use the bridge helper so quotes and control characters are escaped: ```zig -return zero_native.bridge.writeJsonStringValue(output, user_supplied_name); +return native_sdk.bridge.writeJsonStringValue(output, user_supplied_name); ``` ## Wiring the dispatcher ```zig -fn bridge(self: *App) zero_native.BridgeDispatcher { +fn bridge(self: *App) native_sdk.BridgeDispatcher { self.handlers = .{.{ .name = "native.ping", .context = self, .invoke_fn = ping }}; return .{ .policy = .{ .enabled = true, .commands = &policies }, @@ -175,4 +175,4 @@ try { -See also: [Builtin Commands](/bridge/builtin-commands) for `zero-native.command.*`, `zero-native.window.*`, `zero-native.view.*`, `zero-native.webview.*`, `zero-native.dialog.*`, `zero-native.os.*`, `zero-native.clipboard.*`, and `zero-native.credentials.*`. +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.*`. diff --git a/docs/src/app/building-components/layout.tsx b/docs/src/app/building-components/layout.tsx new file mode 100644 index 00000000..1ada31bc --- /dev/null +++ b/docs/src/app/building-components/layout.tsx @@ -0,0 +1,7 @@ +import { pageMetadata } from "@/lib/page-metadata"; + +export const metadata = pageMetadata("building-components"); + +export default function BuildingComponentsLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/docs/src/app/building-components/page.mdx b/docs/src/app/building-components/page.mdx new file mode 100644 index 00000000..b6ab5e14 --- /dev/null +++ b/docs/src/app/building-components/page.mdx @@ -0,0 +1,166 @@ +# 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. 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 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. + +## A component in markup + +A dashboard needs the same labeled stat tile three times. That repetition is a `