Compare commits

...

11 Commits

Author SHA1 Message Date
Chris Tate bb97110886 docs(windows): clarify restore policy support 2026-08-17 17:33:51 -05:00
Chris Tate 7379550a27 feat(windows): expose model restore policy
- Forward restore policy from model-declared windows into platform options.

- Expose matching TypeScript types, defaults, validation, and documentation.

- Cover fresh center-on-primary windows through the null platform.
2026-08-17 17:08:33 -05:00
Chris Tate b25cefe318 chore(release): prepare v0.9.3 (#380)
- Synchronize CLI, core, platform package, and example versions to 0.9.3.
- Add release notes and contributors for all changes since v0.9.2.
2026-08-17 15:14:54 -05:00
Chris Tate ef3ba18168 Add platform-correct Command Backspace (#377)
* Add platform-correct Command Backspace

- Add semantic line-start deletion across Zig and TypeScript text mirrors.
- Preserve platform keymaps, controlled state, undo, and replay behavior.
- Cover all editable widgets and document the shortcut.

Co-authored-by: Mohak Bajaj <77928693+MohakBajaj@users.noreply.github.com>

* Fix Command Backspace edge cases

* Fix platform-specific Command Backspace test

---------

Co-authored-by: Mohak Bajaj <77928693+MohakBajaj@users.noreply.github.com>
2026-08-17 14:46:23 -05:00
Chris Tate 465a163e27 feat: add model-driven theme state (#378)
* feat: add model-driven theme state

- Add the TypeScript themeState helper across checker, generated ABI, and UiApp runtime.
- Preserve system accessibility behavior while supporting model pack, scheme, and accent precedence.
- Document the API and migrate gpu-components with runtime and replay coverage.

Co-authored-by: MohakBajaj <77928693+MohakBajaj@users.noreply.github.com>

* fix: constrain theme state optional fields

---------

Co-authored-by: MohakBajaj <77928693+MohakBajaj@users.noreply.github.com>
2026-08-17 08:51:57 -05:00
Chris Tate a33d579177 fix(macos): preserve file drop targets (#374)
* fix(macos): preserve file drop targets

- Route AppKit drops with labeled, view-local coordinates across canvas, WebView, and window fallback paths.
- Preserve drop metadata through Zig and cover widget routing, TypeScript contracts, docs, and examples.

Co-authored-by: Mohak Bajaj <77928693+MohakBajaj@users.noreply.github.com>

* fix(macos): preserve file drop coordinates

* fix: keep file drop targets sized

* fix: keep capability webview interactive

* fix: constrain capabilities window width

---------

Co-authored-by: Mohak Bajaj <77928693+MohakBajaj@users.noreply.github.com>
2026-08-16 23:11:12 -05:00
Chris Tate 393a0ed36e fix(app-runner): load manifest menus and commands (#376)
* fix(app-runner): load manifest menus and commands

- Resolve app.zon commands, menus, and shortcuts consistently across live and replay runners.
- Add zero-config TypeScript coverage, automation, and documentation.

Co-authored-by: MohakBajaj <77928693+MohakBajaj@users.noreply.github.com>

* test(app-runner): verify manifest menu registration

* fix(automation): escape menu snapshot catalogs

---------

Co-authored-by: MohakBajaj <77928693+MohakBajaj@users.noreply.github.com>
2026-08-16 23:04:07 -05:00
Chris Tate e8f9e4ee50 Fix large TypeScript Msg union compilation (#375)
- Derive comptime scan quotas from Msg shape and identifier bytes across generated shims, persistence, channels, and environment routing.
- Keep tag-skew diagnostics precise and teach future generated-code quota failures without blaming contracts.
- Add 160-arm compile/link and full TypeScript pipeline regression coverage.

Co-authored-by: Mohak Bajaj <77928693+MohakBajaj@users.noreply.github.com>
2026-08-16 21:46:23 -05:00
Chris Tate 659c893b29 Fix combobox Enter submit precedence (#373)
- Prefer bound combobox submit handlers over Enter trigger activation.
- Cover builder, markup, runtime, menu-selection, and command dispatch paths.
- Document the TypeScript-first on-submit composition and keyboard behavior.

Co-authored-by: Mohak Bajaj <77928693+MohakBajaj@users.noreply.github.com>
2026-08-16 21:46:14 -05:00
Chris Tate 8d0da34e62 chore(release): prepare v0.9.2 (#372)
- Synchronize CLI, core, platform package, and example versions to 0.9.2.
- Add release notes and contributors for all changes since v0.9.1.
2026-08-16 20:22:43 -05:00
Chris Tate 1c1fba0c0f fix(macos): place fresh windows correctly (#369)
* fix(macos): place fresh windows correctly

- Separate restored, explicit, and default window placement from persistence policy.
- Honor explicit origins and restore policy across platform seams and both macOS hosts.
- Add placement coverage, update docs, and remove obsolete example workarounds.

* fix(macos): honor window placement policies

* fix(macos): correct window placement policies

* fix(macos): preserve window placement contracts

* fix(runtime): restore secondary window placement
2026-08-16 19:06:29 -05:00
178 changed files with 4110 additions and 385 deletions
+3
View File
@@ -74,6 +74,9 @@ jobs:
- run: zig build test-example-mobile-canvas-lib-ios-store
- run: zig build test-webview-system-link
- run: zig build test-webview-smoke
# The zero-config TypeScript runner must load app.zon menus before
# automation can select their registered command ids.
- run: zig build test-menu-bar-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).
+46 -3
View File
@@ -2,12 +2,57 @@
All notable changes to the Native SDK (formerly zero-native) will be documented in this file.
## 0.9.1
## 0.9.3
<!-- release:start -->
### New Features
- **Model-driven TypeScript theme state**: Zero-config TypeScript apps can now derive the built-in pack, color scheme, and accent from committed model state while preserving manifest fallback, live system accessibility settings, deterministic replay, and the existing `themePack` helper (#378).
- **Platform-correct line deletion**: Command+Backspace on macOS now deletes to the start of a field or logical textarea line across every editable canvas control, with matching TypeScript text helpers, controlled-state behavior, undo, and replay (#377).
### Bug Fixes
- **Precise macOS file-drop routing**: AppKit drops now retain labeled canvas and WebView targets with top-left, view-local coordinates, while unlabeled window regions fall back to content coordinates (#374).
- **Manifest menus in generated runners**: Zero-config TypeScript and Zig-core apps now load `app.zon` commands, shortcuts, and menus consistently in live and replay runners, including ejected-runner fallbacks (#376).
- **Large TypeScript message unions compile reliably**: Generated shims now derive comptime scan quotas from message shape and identifier size, allowing wide unions to compile across persistence, channels, environment routing, and the full external-core pipeline (#375).
- **Correct combobox Enter precedence**: A bound `on-submit` now handles Enter before trigger activation, so query submission no longer opens the picker or dispatches the wrong command (#373).
### Contributors
- @ctate
- @MohakBajaj
<!-- release:end -->
## 0.9.2
### New Features
- **Flash-free accessory startup**: Apps can opt into accessory activation from `app.zon` to launch without a Dock icon or foreground flash, with tray-affordance validation, runtime composition, packaging support, and an updated menu-bar example (#358).
- **Logical canvas radio groups**: Nested radios now form accessible single-selection groups with roving focus and consistent keyboard, pointer, handler, and naming semantics (#361).
- **Budget-aware photo decoding**: Dynamic encoded images are downsampled across desktop and mobile codecs to fit a configurable registered-pixel budget, with independent source bounds, deterministic replay, and platform-level regression coverage (#366).
### Bug Fixes
- **Correct anchored surfaces**: Floating and modal surfaces now dismiss without requiring focus, relayout after scroll restoration, resolve against the correct root, and behave consistently across window contexts (#363).
- **Reliable autofocus and caret reveal**: Keyboard focus, autofocus, and automation now transactionally reveal offscreen targets while preserving collapsed end-caret selections in text editors (#364).
- **Explicit link decoration**: Linked text spans now honor their underline flag while Markdown-generated links retain conventional underlines (#368).
- **Stable macOS window geometry**: Fresh windows now distinguish restored, explicit, and default placement, while AppKit and CEF frame events consistently report content geometry without titlebar drift (#369, #370).
### Improvements
- **Consistent canvas controls and surfaces**: Checkbox and radio labels can contain markup consistently, while actionable states, disabled colors, variant accents, selection geometry, compact layouts, and zero-width strokes now render uniformly across the schema, runtime, accessibility tree, and documentation (#367).
### Contributors
- @ctate
- @sepehr-safari
## 0.9.1
### New Features
- **Multi-item macOS menu bars**: Apps can now manage independent, keyed status items with model-driven updates, events, automation, journaling, and regression coverage (#343).
- **Complete TypeScript file effects**: Secure, permission-gated effects now support bounded streaming reads, atomic writes, stat, append, and deletion while preserving deterministic record and replay behavior (#339, #350).
- **Actionable desktop notifications**: Notification replacement identifiers and actions dispatch through the ordinary command path on macOS, Windows, and Linux (#347).
@@ -31,8 +76,6 @@ All notable changes to the Native SDK (formerly zero-native) will be documented
- @ElSebas41
- @johnlindquist
<!-- release:end -->
## 0.9.0
### New Features
+99 -1
View File
@@ -206,6 +206,27 @@ pub fn build(b: *std.Build) void {
else
sqliteCompileFlags(),
});
const app_runner_window_placement_mod = module(b, target, optimize, "src/app_runner/window_placement.zig");
app_runner_window_placement_mod.addImport("native_sdk", desktop_mod);
const app_runner_window_placement_tests = testArtifact(b, app_runner_window_placement_mod);
const app_runner_options = b.addOptions();
app_runner_options.addOption([]const u8, "platform", "null");
app_runner_options.addOption([]const u8, "trace", "off");
app_runner_options.addOption([]const u8, "web_engine", "system");
app_runner_options.addOption(bool, "debug_overlay", false);
app_runner_options.addOption(bool, "automation", false);
app_runner_options.addOption(bool, "web_layer", false);
const app_runner_mod = module(b, target, optimize, "src/app_runner/root.zig");
app_runner_mod.addImport("native_sdk", desktop_mod);
app_runner_mod.addImport("build_options", app_runner_options.createModule());
app_runner_mod.addImport("app_manifest_zon", b.createModule(.{ .root_source_file = b.path("tests/app-runner/menu_commands_fixture.zon") }));
const app_runner_migrations_mod = module(b, target, optimize, "src/app_runner/no_migrations.zig");
app_runner_migrations_mod.addImport("native_sdk", desktop_mod);
app_runner_mod.addImport("relational_migrations", app_runner_migrations_mod);
const app_runner_tests = testArtifact(b, app_runner_mod);
const app_runner_test_run = b.addRunArtifact(app_runner_tests);
const app_runner_test_step = b.step("test-app-runner", "Run framework app-runner manifest fallback tests");
app_runner_test_step.dependOn(&app_runner_test_run.step);
desktop_mod.link_libc = true;
if (target.result.os.tag == .macos) {
const flags: []const []const u8 = if (b.sysroot) |sysroot|
@@ -603,6 +624,8 @@ pub fn build(b: *std.Build) void {
test_step.dependOn(&b.addRunArtifact(platform_info_tests).step);
test_step.dependOn(&b.addRunArtifact(json_tests).step);
test_step.dependOn(&b.addRunArtifact(app_runner_assets_tests).step);
test_step.dependOn(&b.addRunArtifact(app_runner_window_placement_tests).step);
test_step.dependOn(&app_runner_test_run.step);
test_step.dependOn(&b.addRunArtifact(canvas_tests).step);
test_step.dependOn(&b.addRunArtifact(record_store_tests).step);
test_step.dependOn(&file_crash_run.step);
@@ -834,6 +857,31 @@ pub fn build(b: *std.Build) void {
.{ .path = "build/app.zig", .pattern = "test_step.dependOn(&analysis_obj.step);" },
.{ .path = "src/runtime/ui_app.zig", .pattern = "has no default value - give every Model field a default" },
});
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-owned-runner-window-placement", "Verify owned example runners preserve explicit origins and distinguish successful state restoration", &.{
.{ .path = "examples/hello/src/runner.zig", .pattern = ".initial_placement = if (@hasField(@TypeOf(window), \"x\") or @hasField(@TypeOf(window), \"y\")) .explicit else .default" },
.{ .path = "examples/hello/src/runner.zig", .pattern = "window.initial_placement = .restored;" },
.{ .path = "examples/hello/src/runner.zig", .pattern = "app_info.main_window.initial_placement = .restored;" },
.{ .path = "examples/capabilities/src/runner.zig", .pattern = "info.main_window.default_frame = manifestShellStartupFrame(info.main_window.default_frame);" },
.{ .path = "examples/capabilities/src/runner.zig", .pattern = "info.main_window.restore_state = manifestShellStartupRestoreState(info.main_window.restore_state);" },
.{ .path = "examples/capabilities/src/runner.zig", .pattern = "info.main_window.restore_policy = manifestShellStartupRestorePolicy(info.main_window.restore_policy);" },
.{ .path = "examples/capabilities/src/runner.zig", .pattern = "info.main_window.initial_placement = manifestShellStartupInitialPlacement(info.main_window.initial_placement);" },
});
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-macos-window-placement-contracts", "Verify both macOS hosts use the primary display and restore persisted content frames without titlebar growth", &.{
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "return [NSScreen screens].firstObject ?: [NSScreen mainScreen];" },
.{ .path = "src/platform/macos/cef_host.mm", .pattern = "return [NSScreen screens].firstObject ?: [NSScreen mainScreen];" },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "[window frameRectForContentRect:restoredContentFrame]" },
.{ .path = "src/platform/macos/cef_host.mm", .pattern = "[window frameRectForContentRect:restoredContentFrame]" },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "[window setFrame:restoredWindowFrame display:NO];" },
.{ .path = "src/platform/macos/cef_host.mm", .pattern = "[window setFrame:restoredWindowFrame display:NO];" },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "[window setFrame:NativeSdkCenterFrameOnScreen(window.frame, primaryScreen) display:NO];" },
.{ .path = "src/platform/macos/cef_host.mm", .pattern = "[window setFrame:NativeSdkCenterFrameOnScreen(window.frame, primaryScreen) display:NO];" },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "else if (initialPlacement == 1) {\n // Fresh authored dimensions are content size" },
.{ .path = "src/platform/macos/cef_host.mm", .pattern = "else if (initialPlacement == 1) {\n // AppKit adds titlebar chrome" },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "[window setFrame:NativeSdkConstrainFrame(window.frame) display:NO];" },
.{ .path = "src/platform/macos/cef_host.mm", .pattern = "[window setFrame:NativeSdkConstrainFrame(window.frame) display:NO];" },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "initWithFrame:window.contentView.bounds" },
.{ .path = "src/platform/macos/cef_host.mm", .pattern = "initWithFrame:window.contentView.bounds" },
});
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)" },
@@ -1497,6 +1545,7 @@ 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-app-runner-window-placement", "Run app-runner window placement decision tests", app_runner_window_placement_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| {
@@ -2128,6 +2177,52 @@ 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 menu_bar_smoke_step = b.step("test-menu-bar-smoke", "Run zero-config TypeScript app-menu automation smoke test");
const menu_bar_smoke_build = managedExampleRun(b, cli_exe, &.{ "build", "-Dplatform=macos", "-Dweb-engine=system", "-Dautomation=true", "-Doptimize=Debug" });
menu_bar_smoke_build.setCwd(b.path("examples/menu-bar"));
const menu_bar_smoke_run = b.addSystemCommand(&.{
"sh", "-c",
\\set -eu
\\cd examples/menu-bar
\\app="zig-out/bin/menu-bar"
\\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
\\"$app" > .zig-cache/native-sdk-menu-bar-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-menu-bar-smoke.log) ----" >&2; cat .zig-cache/native-sdk-menu-bar-smoke.log >&2 2>/dev/null || true; fi' EXIT
\\ready="$("$cli" automate wait 2>&1)"
\\case "$ready" in *"ready=true"*) ;; *) echo "menu-bar automation snapshot was not ready" >&2; exit 1 ;; esac
\\before="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)"
\\case "$before" in *'command id="player.next" title="Next Track" enabled=true checked=false'*) ;; *) echo "app.zon command catalog was not loaded by the zero-config runner" >&2; exit 1 ;; esac
\\case "$before" in *'app-menu title="Player" items=6'*) ;; *) echo "app.zon Player menu was not loaded by the zero-config runner" >&2; exit 1 ;; esac
\\case "$before" in *'app-menu-item label="Next Track" command="player.next" enabled=true checked=false key="n" modifiers=(primary=true,command=false,control=false,option=false,shift=false)'*) ;; *) echo "app.zon player.next menu item was not loaded by the zero-config runner" >&2; exit 1 ;; esac
\\case "$before" in
\\ *'Ambient Coast'*) expected='Night Drive' ;;
\\ *'Night Drive'*) expected='Paper Planes' ;;
\\ *'Paper Planes'*) expected='Ambient Coast' ;;
\\ *) echo "menu-bar snapshot did not expose the current TypeScript model track" >&2; exit 1 ;;
\\esac
\\"$cli" automate menu-command player.next >/dev/null 2>&1
\\attempts=0
\\while [ "$attempts" -lt 50 ]; do
\\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)"
\\ case "$snapshot" in *"$expected"*) break ;; esac
\\ attempts=$((attempts + 1))
\\ sleep 0.1
\\done
\\case "$snapshot" in *"$expected"*) ;; *) echo "app.zon menu command did not reach the zero-config TypeScript commandMsg mapper" >&2; exit 1 ;; esac
\\echo "menu-bar smoke ok"
,
"sh",
});
menu_bar_smoke_run.addFileArg(cli_exe.getEmittedBin());
menu_bar_smoke_run.step.dependOn(&menu_bar_smoke_build.step);
menu_bar_smoke_run.step.dependOn(&cli_exe.step);
menu_bar_smoke_step.dependOn(&menu_bar_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
@@ -2618,7 +2713,7 @@ pub fn build(b: *std.Build) void {
\\case "$ready_snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'gpu_nonblank=true'*'canvas_frame_gpu_packet_representable=true'*) ;; *) echo "component gallery GPU surface was not ready" >&2; exit 1 ;; esac
\\case "$ready_snapshot" in *'view @w1/main kind=webview'*) echo "component gallery created an implicit WebView" >&2; exit 1 ;; *) ;; esac
\\"$cli" automate assert 'role=tree name="Components"' 'role=treeitem name="Components".*state=\[expanded\]' 'role=treeitem name="Accordion".*state=\[selected\]' 'role=group name="Details".*state=\[selected,expanded\]' 'name="Accordion details are visible. The model owns this expanded state."'
\\"$cli" automate assert 'role=group name="Theme"' 'role=button name="Default".*state=\[selected\]' 'role=button name="Geist"'
\\"$cli" automate assert 'role=group name="Theme pack"' 'role=group name="Color scheme"' 'role=group name="Theme accent"' 'role=button name="Default".*state=\[selected\]' 'role=button name="Geist"' 'role=button name="System".*state=\[selected\]' 'role=button name="Pink"' 'role=button name="Teal"'
\\"$cli" automate screenshot components-canvas >/dev/null 2>&1
\\cp "$automation_dir/screenshot-components-canvas.png" "$automation_dir/screenshot-components-house.png"
\\rm -f "$automation_dir/screenshot-components-canvas.png"
@@ -3485,6 +3580,9 @@ fn tsCoreE2eArtifact(
// against (the same module the generated shims stage).
conformance_mod.addImport("corewire_rt", module(b, target, optimize, "tools/corewire/shim_rt.zig"));
conformance_mod.addImport("shim_markup_core", sidecarShimModule(b, target, optimize, corewire_exe, b.path("tests/sidecar/markup_fixture.contract.json")));
// Compile-cost guard: this generated mirror carries 160 realistically
// named Msg arms and must need no quota setting in app or test code.
conformance_mod.addImport("shim_wide_core", sidecarShimModule(b, target, optimize, corewire_exe, b.path("tests/sidecar/wide_msg_fixture.contract.json")));
// The integer-class fixture: a hand-written sidecar attesting mixed
// i64/u64 slot classes, so the suite drives boundary and full-range
// integer values through a generated mirror's decode paths.
+4 -6
View File
@@ -24,8 +24,6 @@ The manifest `native init` generates — identity, one shell window with a GPU s
.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 },
},
@@ -285,7 +283,7 @@ For a scene-first app — a `UiApp` passing its Zig scene (`shell_scene`) to the
},
```
Each window takes a `label` plus optional `title`, `width`, `height`, `x`, `y`, `resizable`, `restore_state`, `restore_policy` (`clamp_to_visible_screen` or `center_on_primary`), `initially_hidden` (default false; create the native window ordered out until an explicit show/focus), `allows_fullscreen` (default true; false disables native fullscreen on macOS without disabling ordinary resizing), `min_width`/`min_height` (a content min-size floor the window itself enforces — macOS `contentMinSize`; the first shell window's declaration threads through the startup create like `titlebar`, negative values are a manifest error, 0 means no floor), and `titlebar` (`standard`, `hidden_inset`, `hidden_inset_tall` — the tall variant centers macOS's traffic lights in the 52pt unified band for toolbar-height headers — or `chromeless`, the fully-skinned opt-in that removes all OS chrome including the system buttons; only for apps that draw their own working window controls, see `examples/deck`). `titlebar = "hidden_inset"` hides the titlebar and extends content under it (macOS keeps the traffic lights) — the first shell window's declaration threads through the STARTUP window create, so the main window's chrome is right from the first frame; the app's own header then takes over dragging and inset padding through the `window-drag` attribute and the `on_chrome` hook (see <a href="/docs/native-ui">Native UI</a>). Platforms without the concept keep standard chrome. The same fields are accepted on top-level `windows` entries. `dock_visible` is top-level app policy, not a window field: setting it false removes the Dock/app-switcher presence but does not hide a window; pair it with `initially_hidden = true` when the app should launch behind its status item.
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`), `initially_hidden` (default false; create the native window ordered out until an explicit show/focus), `allows_fullscreen` (default true; false disables native fullscreen on macOS without disabling ordinary resizing), `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`). `restore_state` defaults to true and controls only whether the state store is consulted: a store hit restores the saved frame, an authored `x` or `y` is explicit placement, and a fresh window with neither is default placement. On macOS the default `clamp_to_visible_screen` policy keeps restored and explicit frames on their matching or nearest display, centers the main fresh window, and cascades fresh secondary windows; `center_on_primary` centers restored and fresh default frames on the primary display. `titlebar = "hidden_inset"` hides the titlebar and extends content under it (macOS keeps the traffic lights) — the first shell window's declaration threads through the STARTUP window create, so the main window's chrome is right from the first frame; the app's own header then takes over dragging and inset padding through the `window-drag` attribute and the `on_chrome` hook (see <a href="/docs/native-ui">Native UI</a>). Platforms without the concept keep standard chrome. The same fields are accepted on top-level `windows` entries. `dock_visible` is top-level app policy, not a window field: setting it false removes the Dock/app-switcher presence but does not hide a window; pair it with `initially_hidden = true` when the app should launch behind its status item.
Overlay presentation is also fixed at create time: `transparent` makes the top-level window alpha-capable, `always_on_top` selects its floating/topmost level, `click_through` passes pointer input to windows underneath, `activate_on_show = false` reveals it without activating the app or taking focus, and `allows_fullscreen = false` removes macOS fullscreen participation while keeping the window resizable. Canvas windows already use present-before-show, so these flags are applied while the window is hidden and its first alpha-correct frame becomes the first visible frame. `initially_hidden = true` is stronger: it suppresses that reveal until `Cmd.showWindow` or explicit focus. Pair `transparent = true` with a non-opaque `gpu_alpha_mode`; `UiApp.WindowDescriptor` makes that canvas-alpha choice and uses an alpha-zero clear automatically. See the <a href="/docs/windows#overlay-windows">overlay window recipe</a>. These fields are accepted on top-level `windows` and `shell.windows`. Runtime `WindowCreateOptions` exposes the same controls but spells the hidden mode `.show = .hidden`; `UiApp.WindowDescriptor` exposes the overlay controls but not the initially-hidden mode.
@@ -313,11 +311,11 @@ The optional `commands` list declares shared command metadata. The runtime still
An app can define up to 256 commands. Command ids can be up to 128 bytes and titles can be up to 128 bytes.
Generated runners load manifest commands into `RuntimeOptions.commands`. Native code can read the active catalog with `runtime.listCommands(...)`, and trusted WebView code can read it with `window.zero.commands.list()` when the built-in command bridge allows it. Use the catalog to keep menus, shortcuts, toolbar controls, tray items, and bridge callers aligned with the same command ids.
Generated zero-config TypeScript and Zig-core runners load manifest commands into `RuntimeOptions.commands`; ejected runners use the same fallback. Native code can read the active catalog with `runtime.listCommands(...)`, and trusted WebView code can read it with `window.zero.commands.list()` when the built-in command bridge allows it. Use the catalog to keep menus, shortcuts, toolbar controls, tray items, and bridge callers aligned with the same command ids.
## `shortcuts`
The optional `shortcuts` list defines app-level keyboard shortcuts. Generated runners load these automatically:
The optional `shortcuts` list defines app-level keyboard shortcuts. Generated zero-config TypeScript and Zig-core runners load these automatically, as do ejected runners:
```zig
.shortcuts = .{
@@ -336,7 +334,7 @@ Chromium builds are currently macOS-only; use the Linux system WebView backend w
## `menus`
The optional `menus` list defines native app menus. Generated runners load these automatically:
The optional `menus` list defines native app menus. Generated zero-config TypeScript and Zig-core runners load these automatically, as do ejected runners:
```zig
.menus = .{
+1 -1
View File
@@ -36,7 +36,7 @@ When the runtime publishes a snapshot, it writes these files to the automation d
<tbody>
<tr>
<td><code>snapshot.txt</code></td>
<td>Runtime state: source kind, window metadata, native/WebView metadata including role, accessibility label, text, and focus state, <code>ready=true/false</code>, and <code>markup_watch=armed|off</code> in the header — whether the markup hot-reload watch is armed (only in builds where the app wired <code>.markup</code> with a <code>watch_path</code> and <code>io</code>, or registered compiled fragments through <code>fragment_watch</code> — i.e. Debug dev builds)</td>
<td>Runtime state: source kind, window metadata, native/WebView metadata including role, accessibility label, text, and focus state, the configured command and app-menu catalogs, <code>ready=true/false</code>, and <code>markup_watch=armed|off</code> in the header — whether the markup hot-reload watch is armed (only in builds where the app wired <code>.markup</code> with a <code>watch_path</code> and <code>io</code>, or registered compiled fragments through <code>fragment_watch</code> — i.e. Debug dev builds)</td>
</tr>
<tr>
<td><code>accessibility.txt</code></td>
+19 -2
View File
@@ -5,6 +5,8 @@ import { AttrTable } from "@/components/attr-table";
`combobox` is a trigger-only primitive like [select](/docs/components/select), but the trigger is a text entry with a menu affordance: `on-input` names a Msg variant that receives every edit as a text-input event (`canvas.TextInputEvent` in a Zig core; the `TextInputEvent` union from `@native-sdk/core/text` in a TypeScript core), and the model filters the options as the user types. The options themselves are composed the same way as the select's — an anchored [dropdown-menu](/docs/components/dropdown-menu) of menu-items beside the trigger in a `stack`, rendered under an `if`, with `on-dismiss` clearing the model's open flag when Escape or a click outside closes the surface.
Enter submits when `on-submit` is bound; otherwise Enter opens the picker. Space and the open-arrow keys (Down/Up) always open it. Once focus moves into the open menu, Enter selects the focused `menu-item` as usual.
<ComponentPreview name="combobox" alt="A combobox rendered by the engine" caption="a combobox trigger with its search placeholder" />
## Markup
@@ -13,8 +15,8 @@ The model owns the query and the open flag; the `for` source is the model-filter
```html
<stack width="240">
<combobox placeholder="Search frameworks" text="{framework_query}" on-input="framework_edited" on-press="open_framework_menu" />
<if test="{framework_menu_open}">
<combobox placeholder="Search frameworks" text="{frameworkQuery}" on-input="framework_edited" on-submit="commit_framework_query" on-press="open_framework_menu" />
<if test="{frameworkMenuOpen}">
<dropdown-menu anchor="below" anchor-alignment="stretch" on-dismiss="close_framework_menu">
<for each="matchingFrameworks" key="id" as="f">
<menu-item on-press="pick_framework:{f.id}">{f.name}</menu-item>
@@ -24,6 +26,19 @@ The model owns the query and the open flag; the `for` source is the model-filter
</stack>
```
In the primary TypeScript core, the submit arm commits the current model-owned query. The open and submit messages remain separate, so Enter can commit without toggling the picker:
```ts
export type Msg =
| { readonly kind: "framework_edited"; readonly edit: TextInputEvent }
| { readonly kind: "open_framework_menu" }
| { readonly kind: "close_framework_menu" }
| { readonly kind: "commit_framework_query" };
case "commit_framework_query":
return { ...model, committedFrameworkQuery: model.frameworkQuery, frameworkMenuOpen: false };
```
## Programmatic construction (Zig)
In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically. `on_input` takes a comptime message constructor: `Ui.inputMsg(.tag)` builds `Msg{ .tag = edit }` for each `canvas.TextInputEvent`.
@@ -34,6 +49,7 @@ ui.stack(.{ .width = 240 }, .{
.placeholder = "Search frameworks",
.text = model.framework_query,
.on_input = Ui.inputMsg(.framework_edited),
.on_submit = .commit_framework_query,
.on_press = .open_framework_menu,
}, .{}),
if (model.framework_menu_open) ui.el(.dropdown_menu, .{
@@ -56,6 +72,7 @@ ui.stack(.{ .width = 240 }, .{
"disabled",
"on-press",
"on-input",
"on-submit",
"on-dismiss",
]}
/>
@@ -43,6 +43,10 @@ pub fn email(model: *const Model) []const u8 {
</CodeToggle>
## Editing keys
The built-in editor follows the platform keymap in `input`, `text-field`, `search-field`, and `combobox`: Backspace/Delete remove one caret unit, Option+Backspace/Delete on macOS (Ctrl+Backspace/Delete elsewhere) remove one word, and Command+Backspace on macOS (with or without Shift) deletes from the caret to the beginning of the field. A non-empty selection always wins and is deleted by itself. The semantic edit still arrives through `on-input`, so `applyTextInputEvent` and `TextBuffer` keep controlled fields synchronized and one undo restores the whole deletion.
## Search field
`search-field` renders the search affordance but binds exactly like an input; pair it with a model-filtered list. Whenever the field holds text it also shows a built-in clear affordance — a small x inside its trailing edge — and pressing it (or pressing Escape while focused) clears through the standard text-edit path, so the `on-input` handler receives the clear like any other edit and a model-owned buffer empties with it. No attribute enables or disables this; searchable fields simply carry it. For text entry that opens a menu of suggestions, see [combobox](/docs/components/combobox).
@@ -5,6 +5,8 @@ import { AttrTable } from "@/components/attr-table";
Multi-line text entry. Like [input](/docs/components/input), `text` and `placeholder` bind from the model and `on-input` names a Msg variant that receives every edit as a text-input event — see [input](/docs/components/input) for the core-side contract in both languages. By default, Enter (and Shift+Enter) inserts a newline; when a textarea carries `on-submit`, submission rides Cmd+Enter on macOS or Ctrl+Enter elsewhere. Chat composers can set `submit-on-enter="true"`: plain Enter then submits, Shift+Enter still inserts a newline, and the primary chord still submits. Give it a definite `width` and `height` (or a `grow`) to size the editing box.
Textarea editing uses the same platform shortcuts as [input](/docs/components/input#editing-keys). On macOS, Command+Backspace (with or without Shift) deletes from the caret to the beginning of the current hard-newline-delimited line (or deletes the active selection); soft-wrapped visual lines use their logical line start in this first version.
<ComponentPreview name="textarea" alt="A textarea rendered by the engine" />
## Markup
+1 -1
View File
@@ -14,7 +14,7 @@ The Native SDK can configure native app menus from `app.zon` or typed Zig data.
},
```
Generated runners load `app.zon` menus automatically. Pass `menus` to `runWithOptions` when an app needs to override the manifest at runtime:
Generated zero-config runners load `app.zon` menus automatically for both TypeScript cores and Zig cores. Ejected runners use the same fallback. Pass a non-null `menus` slice to `runWithOptions` when lower-level Zig wiring needs to override the manifest at runtime (an explicit empty slice disables manifest menus):
```zig
const view_items = [_]native_sdk.MenuItem{
+2 -2
View File
@@ -320,7 +320,7 @@ export function dropMsg(drop: FileDropEvent): Msg | null {
}
```
The platform event is journaled before either route, so record/replay delivers the identical source, point, and path bytes. A host that cannot resolve the target view leaves `viewLabel` empty and `point` null.
The platform event is journaled before either route, so record/replay delivers the identical source, point, and path bytes. The macOS system host reports labeled, view-local points for canvas and WebView drops; an unlabeled window region keeps an empty `viewLabel` and reports a window-content point. A host that cannot resolve either leaves `viewLabel` empty and `point` null.
## Native scrolling and context menus
@@ -484,7 +484,7 @@ For timestamps, the facade owns the clocks (Zig 0.16 puts `std.time` behind `std
## Secondary windows
In a default TypeScript app, export `windows(model): readonly WindowDescriptor[]` and put each possible window's markup at `src/windows/<label>.native`; the generated launcher compiles those files, maps descriptor labels to them, and includes shared components they import from nested paths under `src/windows/`. Construct descriptors with `windowDescriptor`. `closePolicy: "quit"` routes `onCloseCommand` through `commandMsg`; `"hide"` retains the window and dispatches no close command. `titlebar` includes `"chromeless"` for fully skinned windows. `examples/system-monitor-ts` is the complete default-path reference.
In a default TypeScript app, export `windows(model): readonly WindowDescriptor[]` and put each possible window's markup at `src/windows/<label>.native`; the generated launcher compiles those files, maps descriptor labels to them, and includes shared components they import from nested paths under `src/windows/`. Construct descriptors with `windowDescriptor`. `restorePolicy: "center_on_primary"` centers a fresh descriptor with no `x`/`y` on macOS; Windows and Linux currently keep their native default placement. The default policy is `"clamp_to_visible_screen"`. `closePolicy: "quit"` routes `onCloseCommand` through `commandMsg`; `"hide"` retains the window and dispatches no close command. `titlebar` includes `"chromeless"` for fully skinned windows. `examples/system-monitor-ts` is the complete default-path reference.
In Zig cores and custom wiring, windows are model-declared exactly like the tray: `Options.windows_fn` returns the window descriptors that should exist right now (presence IS liveness), and `Options.window_view` builds each declared window's whole canvas tree, keyed by the descriptor's window label. After every dispatched Msg the runtime reconciles: windows the model started declaring are created (a source-less native window wearing one `gpu_surface` view with the descriptor's `canvas_label`, inheriting the main canvas's gpu options), windows it stopped declaring close, and every open window's view rebuilds from the same model — a theme picked in the settings window restyles the main window on the same dispatch. There is no `visible` flag: transient visibility is host state changed through `hideWindow`/`showWindow` or a `.hide` close policy; stop declaring a window to really close it and release its retained views.
+68 -5
View File
@@ -28,21 +28,84 @@ const MyApp = native_sdk.UiApp(Model, Msg);
.theme = app_runner.manifestThemePack(),
```
In a zero-config TypeScript app, export a single-model `themePack` helper when the pack itself belongs in live app state. The generated launcher recognizes this helper and wires the stock-token path automatically:
In a zero-config TypeScript app, export `themeState(model)` when pack, color scheme, or accent belongs in live app state. The generated launcher recognizes the helper and wires the stock-token path automatically:
```ts
import { type ThemeState } from "@native-sdk/core/events";
export type ThemePack = "house" | "geist";
export type ThemePreference = "system" | "light" | "dark";
export interface Model {
readonly theme: ThemePack;
readonly themePreference: ThemePreference;
readonly pinkAccent: boolean;
}
export function themePack(model: Model): ThemePack {
return model.theme;
export function themeState(model: Model): ThemeState {
if (model.pinkAccent) {
return {
pack: model.theme,
colorScheme: model.themePreference,
accent: "#df2670",
};
}
return { pack: model.theme, colorScheme: model.themePreference };
}
```
Change `model.theme` through ordinary messages (for example, from a pair of model-driven `toggle-button`s). The helper is evaluated on every rebuild. It changes only the built-in pack: system light/dark, high contrast, reduced motion, manifest `theme_accent`, and each surface's scale remain live runtime inputs. Without the helper, `app.zon` remains the static pack choice.
`ThemeState` has three optional fields:
```ts
export type ThemeState = {
readonly pack?: "house" | "geist";
readonly colorScheme?: "light" | "dark" | "system";
readonly accent?: string; // exactly #rrggbb
};
```
Change the model through ordinary messages (for example, model-driven `toggle-button`s). The helper is evaluated after every committed update. Omitted fields inherit the next lower layer; omitted `colorScheme` and `"system"` both follow the OS. A malformed accent is a loud runtime teaching error, never a silent fallback. High contrast and reduced motion remain live OS inputs; high contrast suppresses both manifest and model accent overrides so accessibility wins.
The stock-theme precedence is:
<table>
<thead>
<tr>
<th>Axis</th>
<th>Highest to lowest precedence</th>
</tr>
</thead>
<tbody>
<tr>
<td>Complete tokens</td>
<td><code>tokens_fn</code> → static <code>tokens</code> → stock theme composition below</td>
</tr>
<tr>
<td>Pack</td>
<td><code>themeState.pack</code> → <code>app.zon theme</code> → <code>house</code></td>
</tr>
<tr>
<td>Color scheme</td>
<td>forced <code>themeState.colorScheme</code> → OS appearance</td>
</tr>
<tr>
<td>Accent</td>
<td><code>themeState.accent</code> → <code>app.zon theme_accent</code> → selected pack</td>
</tr>
<tr>
<td>High contrast / reduced motion</td>
<td>OS appearance (high contrast suppresses accent overrides)</td>
</tr>
<tr>
<td>Surface scale / text measurement</td>
<td>Runtime-stamped last</td>
</tr>
</tbody>
</table>
`themeState` deliberately controls canvas design tokens in v1. Native title bars and WebViews still follow the platform's effective appearance; forcing dark canvas content does not call `NSApp.appearance` or impose a scheme on embedded web content.
The earlier `themePack(model): "house" | "geist"` helper remains supported unchanged for apps that only switch packs. It preserves live OS scheme, manifest accent, high contrast, reduced motion, and surface scale. Export `themePack` or `themeState`, never both; the checker and adapter report that conflict as a teaching error.
Apps that derive their own tokens select the pack directly — `ThemeOptions.pack` is just another theme axis, exactly as switchable at runtime as the scheme:
@@ -113,7 +176,7 @@ pub fn brandTokens(scheme: canvas.ColorScheme, contrast: canvas.ColorContrast) c
}
```
Hand it to your app via `tokens_fn` (model-owned, follows the system scheme through your model) or `tokens` (fixed). The runtime stamps `pixel_snap.scale` and text measurement after your function runs, so never cache those.
Hand it to your app via `tokens_fn` (model-owned, follows the system scheme through your model) or `tokens` (fixed). These complete-token paths take precedence over `themeState`; the runtime stamps `pixel_snap.scale` and text measurement after your function runs, so never cache those.
## What themes cannot do
+3 -1
View File
@@ -482,6 +482,8 @@ Export `windows(model): readonly WindowDescriptor[]` to derive the live secondar
`closePolicy` accepts `"quit"` (the default) or `"hide"`. A `"quit"` user close routes `onCloseCommand` through `commandMsg`, where the app maps it to the Msg that clears its open flag. A `"hide"` close retains the same native window and view and dispatches no close command; `Cmd.showWindow(label)` reveals it. Model-declared secondary windows are desktop-only. See `examples/system-monitor-ts`.
`restorePolicy` accepts `"clamp_to_visible_screen"` (the default) or `"center_on_primary"`. Model-declared windows do not restore persisted frames. On macOS, `"center_on_primary"` centers a fresh descriptor with no authored `x`/`y`; Windows and Linux currently keep their native default placement.
`titlebar` accepts `"standard"`, `"hidden_inset"`, `"hidden_inset_tall"`, or `"chromeless"`. Transparent Windows windows require `"chromeless"`; because that removes the system buttons, fully skinned windows must draw working close/minimize controls.
## Subscriptions are Sub data
@@ -536,7 +538,7 @@ A markup text control (`<text-field text="{draft}" on-input="draft_edit" />`) ne
## Splitting a core into modules
A core that outgrows one file splits into modules under `src/` except `src/services/`: relative imports spelled with their real filenames (`./parsers.ts` — the same file runs under node, whose loader resolves real files), `src/` as the hard boundary (`../` and npm packages are teaching errors), and no runtime cycles (`import type` back-edges are fine and idiomatic — a helper module typically type-imports `Model` from the entry). The core may not import service files, even type-only; shared subset-legal shapes live in an ordinary core-class module which a service may import. Export lists and value re-exports are ordinary module surface: `export { helper, doneCount as remaining }` binds names over existing declarations, and `export { parsePs } from "./parsers.ts"` forwards another module's export by name — what stays out is `export default`, `export =`, and `export * from` (the core's flat namespace resolves by name, so every export names what it binds). `core.ts` stays the entry module and the app's public face: `update`, `initialModel`, `subscriptions`, the wiring channels, `themePack` / `statusItem` / `statusItems` / `windows`, and the exported binding helpers live there (declared and exported under their own names — a rename or re-export cannot bind an entry point), and imported modules hold the machinery they call. The SDK also ships library modules in the same subset — `@native-sdk/core/text` is the byte-splice text engine (caret, selection, IME composition, ASCII case-insensitive compare), and `@native-sdk/core/events` is the canonical event and shell vocabulary (`TextInputEvent` re-exported, `ScrollState`, `FrameEvent`, `KeyEvent`, `PinchPhase`/`PinchEvent`, `ColorScheme`, the chrome records, `AudioState`/`AudioEvent`, status-item records, and `WindowDescriptor`) so no core re-types it — compiled into your core when imported and absent when not.
A core that outgrows one file splits into modules under `src/` except `src/services/`: relative imports spelled with their real filenames (`./parsers.ts` — the same file runs under node, whose loader resolves real files), `src/` as the hard boundary (`../` and npm packages are teaching errors), and no runtime cycles (`import type` back-edges are fine and idiomatic — a helper module typically type-imports `Model` from the entry). The core may not import service files, even type-only; shared subset-legal shapes live in an ordinary core-class module which a service may import. Export lists and value re-exports are ordinary module surface: `export { helper, doneCount as remaining }` binds names over existing declarations, and `export { parsePs } from "./parsers.ts"` forwards another module's export by name — what stays out is `export default`, `export =`, and `export * from` (the core's flat namespace resolves by name, so every export names what it binds). `core.ts` stays the entry module and the app's public face: `update`, `initialModel`, `subscriptions`, the wiring channels, `themeState` / `themePack` / `statusItem` / `statusItems` / `windows`, and the exported binding helpers live there (declared and exported under their own names — a rename or re-export cannot bind an entry point), and imported modules hold the machinery they call. The SDK also ships library modules in the same subset — `@native-sdk/core/text` is the byte-splice text engine (caret, selection, IME composition, ASCII case-insensitive compare), and `@native-sdk/core/events` is the canonical event and shell vocabulary (`TextInputEvent` re-exported, `ScrollState`, `FrameEvent`, `KeyEvent`, `PinchPhase`/`PinchEvent`, `ColorScheme`, `ThemeState`, the chrome records, `AudioState`/`AudioEvent`, status-item records, and `WindowDescriptor`) so no core re-types it — compiled into your core when imported and absent when not.
<CodeToggle>
+4 -3
View File
@@ -8,7 +8,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
.title = "My App",
.width = 480,
.height = 320,
.restore_state = false,
.views = &shell_views,
}};
const shell_scene: native_sdk.ShellConfig = .{ .windows = &shell_windows };
@@ -16,6 +15,8 @@ const shell_scene: native_sdk.ShellConfig = .{ .windows = &shell_windows };
The first scene window adopts the startup window; additional scene windows are created through the window service. Secondary windows can also be created imperatively from Zig — or, in apps that [embed web content](/docs/frontend), from trusted JavaScript.
Fresh windows use three-way placement. A successful state-store lookup restores the saved frame. Otherwise, an authored `x` or `y` places the window at that origin (constrained to the matching or nearest visible screen on macOS). With neither saved state nor an explicit origin, macOS centers the main window. Under the default `clamp_to_visible_screen` policy, restored frames stay on their matching or nearest display and fresh secondary windows cascade from the active window; `center_on_primary` centers restored and fresh default frames on the primary display. `restore_state = true` is only the persistence opt-in—it no longer makes a first launch look like a restored frame at `(0, 0)`.
## Overlay windows
Window creation accepts four overlay controls:
@@ -35,7 +36,6 @@ const overlay: native_sdk.ShellWindow = .{
.always_on_top = true,
.click_through = true,
.activate_on_show = false,
.restore_state = false,
.views = &.{.{
.label = "overlay-canvas",
.kind = .gpu_surface,
@@ -68,6 +68,7 @@ const info = try runtime.createWindow(.{
.label = "tools",
.title = "Tools",
.default_frame = native_sdk.geometry.RectF.init(80, 80, 420, 320),
.initial_placement = .explicit,
});
try runtime.focusWindow(info.id);
```
@@ -155,7 +156,7 @@ JavaScript creation also accepts `titlebar: "standard" | "hidden_inset" | "hidde
</tr>
<tr>
<td><code>WindowRestorePolicy</code></td>
<td>How restored frames are placed, such as clamping to the visible screen or centering on the primary display</td>
<td>How macOS places restored and fresh default frames: clamp to the matching/nearest visible screen or center on the primary display</td>
</tr>
<tr>
<td><code>WindowClosePolicy</code></td>
+1
View File
@@ -118,6 +118,7 @@ fn manifestWindow(comptime window: anytype, comptime index: usize) native_sdk.Wi
.resizable = windowBool(window, "resizable", true),
.restore_state = windowBool(window, "restore_state", true),
.restore_policy = windowRestorePolicy(window),
.initial_placement = if (@hasField(@TypeOf(window), "x") or @hasField(@TypeOf(window), "y")) .explicit else .default,
};
}
-1
View File
@@ -18,7 +18,6 @@
.width = 320,
.height = 490,
.resizable = false,
.restore_state = false,
.restore_policy = "center_on_primary",
.titlebar = "hidden_inset",
.views = .{
-2
View File
@@ -56,7 +56,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
// the in-canvas drag band carries the window (see view.zig), and
// app.zon's startup window declares the same style.
.resizable = false,
.restore_state = false,
.titlebar = .hidden_inset,
.views = &shell_views,
}};
@@ -152,7 +151,6 @@ pub fn main(init: std.process.Init) !void {
.window_title = "Calculator",
.bundle_id = "dev.native_sdk.calculator",
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
.restore_state = false,
.js_window_api = false,
.shortcuts = &app_shortcuts,
.security = .{
-1
View File
@@ -12,7 +12,6 @@
.title = "Native SDK Canvas Preview",
.width = 960,
.height = 640,
.restore_state = false,
.restore_policy = "center_on_primary",
.views = .{
.{ .label = "preview-canvas", .kind = "gpu_surface", .fill = true, .role = "Canvas chrome", .accessibility_label = "Canvas Preview chrome", .gpu_backend = "metal", .gpu_pixel_format = "bgra8_unorm", .gpu_present_mode = "timer", .gpu_alpha_mode = "opaque", .gpu_color_space = "srgb", .gpu_vsync = true },
-2
View File
@@ -49,7 +49,6 @@ pub const shell_windows = [_]native_sdk.ShellWindow{.{
.title = "Native SDK Canvas Preview",
.width = window_width,
.height = window_height,
.restore_state = false,
.views = &shell_views,
}};
pub const shell_scene: native_sdk.ShellConfig = .{ .windows = &shell_windows };
@@ -196,7 +195,6 @@ pub fn main(init: std.process.Init) !void {
.window_title = "Native SDK Canvas Preview",
.bundle_id = "dev.native_sdk.canvas_preview",
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
.restore_state = false,
.js_window_api = false,
.security = .{
.navigation = .{ .allowed_origins = &.{ "zero://inline", "zero://app", "https://example.com", "https://native-sdk.dev" } },
+3 -1
View File
@@ -8,7 +8,7 @@ This example shows guarded OS capabilities from trusted WebView code:
- Clipboard text read and write.
- Message dialogs.
- Credential set, get, and delete.
- File-drop events delivered to Zig and the WebView event bridge.
- File-drop events delivered to Zig and the WebView event bridge, plus a real canvas `drop_files` target.
- File association and custom URL scheme packaging metadata.
- App activation and deactivation events.
@@ -24,6 +24,8 @@ Run the headless test path:
zig build test -Dplatform=null
```
For the macOS host integration check, run the app with the system backend and drag a Finder file onto the right-hand **Drop files here** canvas. The status bar must report `Widget target 2 fired` and the dropped path. Dropping over the left WebView must still report the ordinary app-level drop without a widget target. The guest-VM harness cannot synthesize an AppKit drag session yet, so this is the documented manual receipt for the real host path.
Run all native-first example tests from the repository root:
```sh
+4 -1
View File
@@ -9,6 +9,7 @@
"webview",
"js_bridge",
"native_views",
"gpu_surfaces",
"open_url",
"reveal_path",
"recent_documents",
@@ -39,11 +40,13 @@
.title = "Native SDK Capabilities",
.width = 900,
.height = 620,
.min_width = 770,
.restore_policy = "center_on_primary",
.views = .{
.{ .label = "main", .kind = "webview", .url = "zero://inline", .fill = true },
.{ .label = "statusbar", .kind = "statusbar", .edge = "bottom", .height = 34, .role = "Status" },
.{ .label = "status-label", .kind = "label", .parent = "statusbar", .x = 14, .y = 8, .width = 640, .height = 18, .text = "Ready." },
.{ .label = "drop-canvas", .kind = "gpu_surface", .edge = "right", .width = 250, .min_width = 220, .role = "File drop canvas", .accessibility_label = "File drop target", .gpu_backend = "metal" },
.{ .label = "main", .kind = "webview", .url = "zero://inline", .fill = true, .min_width = 520 },
},
},
},
+125 -7
View File
@@ -10,7 +10,10 @@ const manifest_url_schemes = if (@hasField(@TypeOf(app_manifest), "url_schemes")
const window_width: f32 = 900;
const window_height: f32 = 620;
const window_min_width: f32 = 770;
const statusbar_height: f32 = 34;
const drop_canvas_label = "drop-canvas";
const drop_target_id: native_sdk.canvas.ObjectId = 2;
const html =
\\<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
@@ -54,24 +57,30 @@ const builtin_policies = [_]native_sdk.BridgeCommandPolicy{
.{ .name = "native-sdk.credentials.delete", .permissions = &credential_permission, .origins = &bridge_origins },
};
const shell_views = [_]native_sdk.ShellView{
.{ .label = "main", .kind = .webview, .url = "zero://inline", .fill = true },
.{ .label = "statusbar", .kind = .statusbar, .edge = .bottom, .height = statusbar_height, .layer = 20, .role = "Status" },
.{ .label = "status-label", .kind = .label, .parent = "statusbar", .x = 14, .y = 8, .width = 640, .height = 18, .layer = 21, .text = "Ready." },
.{ .label = drop_canvas_label, .kind = .gpu_surface, .edge = .right, .width = 250, .min_width = 220, .role = "File drop canvas", .accessibility_label = "File drop target", .gpu_backend = .metal },
.{ .label = "main", .kind = .webview, .url = "zero://inline", .fill = true, .min_width = 520 },
};
const shell_windows = [_]native_sdk.ShellWindow{.{
.label = "main",
.title = "Native SDK Capabilities",
.width = window_width,
.height = window_height,
.min_width = window_min_width,
.views = &shell_views,
}};
const shell_scene: native_sdk.ShellConfig = .{ .windows = &shell_windows };
const CapabilitiesApp = struct {
drop_count: u32 = 0,
widget_drop_count: u32 = 0,
activation_count: u32 = 0,
deactivation_count: u32 = 0,
last_drop_paths: []const []const u8 = &.{},
last_drop_target_id: native_sdk.canvas.ObjectId = 0,
pending_drop_target_id: ?native_sdk.canvas.ObjectId = null,
drop_target_installed: bool = false,
fn app(self: *@This()) native_sdk.App {
return .{
@@ -96,9 +105,34 @@ const CapabilitiesApp = struct {
self.last_drop_paths = drop.paths;
var status_buffer: [160]u8 = undefined;
const first_path = if (drop.paths.len > 0) drop.paths[0] else "";
const status = try std.fmt.bufPrint(&status_buffer, "Received file drop {d}: {d} file(s): {s}", .{ self.drop_count, drop.paths.len, first_path });
const drop_target_id_value = self.pending_drop_target_id;
self.pending_drop_target_id = null;
const status = if (drop_target_id_value) |target_id|
try std.fmt.bufPrint(&status_buffer, "Widget target {d} fired; app drop {d}: {d} file(s): {s}", .{ target_id, self.drop_count, drop.paths.len, first_path })
else
try std.fmt.bufPrint(&status_buffer, "Received file drop {d}: {d} file(s): {s}", .{ self.drop_count, drop.paths.len, first_path });
_ = try runtime.updateView(drop.window_id, "status-label", .{ .text = status });
},
.canvas_widget_file_drop => |drop| {
self.widget_drop_count += 1;
if (drop.target) |target| {
self.last_drop_target_id = target.id;
self.pending_drop_target_id = target.id;
} else {
self.pending_drop_target_id = null;
}
},
.gpu_surface_frame => |frame| {
if (!self.drop_target_installed and std.mem.eql(u8, frame.label, drop_canvas_label)) {
try installDropTarget(runtime, frame.window_id, frame.label, frame.size);
self.drop_target_installed = true;
}
},
.gpu_surface_resized => |resize| {
if (self.drop_target_installed and std.mem.eql(u8, resize.label, drop_canvas_label)) {
try installDropTarget(runtime, resize.window_id, resize.label, resize.frame.size());
}
},
.lifecycle => |lifecycle| switch (lifecycle) {
.activate => {
self.activation_count += 1;
@@ -110,11 +144,31 @@ const CapabilitiesApp = struct {
},
else => {},
},
.appearance_changed, .command, .shortcut, .timer, .effects_wake, .audio, .video, .gpu_surface_frame, .gpu_surface_resized, .gpu_surface_input, .canvas_widget_pointer, .canvas_widget_keyboard, .canvas_widget_scroll, .canvas_widget_file_drop, .canvas_widget_drag, .canvas_widget_context_menu, .canvas_widget_context_menu_shown, .canvas_widget_context_menu_dismissed, .canvas_widget_context_menu_request, .canvas_widget_dismiss, .canvas_widget_context_press, .canvas_widget_resize, .canvas_widget_change, .window_closed, .automation_provenance => {},
.appearance_changed, .command, .shortcut, .timer, .effects_wake, .audio, .video, .gpu_surface_input, .canvas_widget_pointer, .canvas_widget_keyboard, .canvas_widget_scroll, .canvas_widget_drag, .canvas_widget_context_menu, .canvas_widget_context_menu_shown, .canvas_widget_context_menu_dismissed, .canvas_widget_context_menu_request, .canvas_widget_dismiss, .canvas_widget_context_press, .canvas_widget_resize, .canvas_widget_change, .window_closed, .automation_provenance => {},
}
}
};
fn installDropTarget(runtime: *native_sdk.Runtime, window_id: native_sdk.WindowId, label: []const u8, size: native_sdk.geometry.SizeF) !void {
const canvas = native_sdk.canvas;
const margin: f32 = 24;
const target = canvas.Widget{
.id = drop_target_id,
.kind = .button,
.frame = native_sdk.geometry.RectF.init(margin, margin, @max(1, size.width - margin * 2), @max(1, size.height - margin * 2)),
.text = "Drop files here",
.semantics = .{ .label = "Drop files here", .actions = .{ .drop_files = true } },
};
var nodes: [2]canvas.WidgetLayoutNode = undefined;
const layout = try canvas.layoutWidgetTree(
.{ .id = 1, .kind = .panel, .children = &.{target} },
native_sdk.geometry.RectF.init(0, 0, size.width, size.height),
&nodes,
);
_ = try runtime.setCanvasWidgetLayout(window_id, label, layout);
_ = try runtime.emitCanvasWidgetDisplayList(window_id, label, .{});
}
pub fn main(init: std.process.Init) !void {
var app = CapabilitiesApp{};
try runner.runWithOptions(app.app(), .{
@@ -139,6 +193,7 @@ pub fn main(init: std.process.Init) !void {
test "capabilities bridge gates native services and dispatches file drops" {
const harness = try native_sdk.TestHarness().create(std.testing.allocator, .{ .size = native_sdk.geometry.SizeF.init(window_width, window_height) });
defer harness.destroy(std.testing.allocator);
harness.null_platform.gpu_surfaces = true;
harness.runtime.options.builtin_bridge = .{ .enabled = true, .commands = &builtin_policies };
harness.runtime.options.security = .{
.permissions = &app_permissions,
@@ -155,6 +210,30 @@ test "capabilities bridge gates native services and dispatches file drops" {
const app = app_state.app();
try harness.start(app);
var views_buffer: [8]native_sdk.ViewInfo = undefined;
const views = harness.runtime.listViews(1, &views_buffer);
const webview = viewByLabel(views, "main").?;
const drop_canvas = viewByLabel(views, drop_canvas_label).?;
const statusbar = viewByLabel(views, "statusbar").?;
try std.testing.expect(webview.parent == null);
try std.testing.expect(drop_canvas.parent == null);
try std.testing.expectEqual(native_sdk.geometry.RectF.init(0, 0, 650, window_height - statusbar_height), webview.frame);
try std.testing.expectEqual(native_sdk.geometry.RectF.init(650, 0, 250, window_height - statusbar_height), drop_canvas.frame);
try std.testing.expectEqual(native_sdk.geometry.RectF.init(0, window_height - statusbar_height, window_width, statusbar_height), statusbar.frame);
try harness.runtime.dispatchPlatformEvent(app, .{ .gpu_surface_frame = .{
.window_id = 1,
.label = drop_canvas_label,
.size = native_sdk.geometry.SizeF.init(250, window_height - statusbar_height),
.frame_index = 1,
.nonblank = true,
} });
try harness.runtime.dispatchPlatformEvent(app, .{ .gpu_surface_resized = .{
.window_id = 1,
.label = drop_canvas_label,
.frame = native_sdk.geometry.RectF.init(0, 0, 350, window_height - statusbar_height),
} });
try dispatchBridge(harness, app, "{\"id\":\"notify\",\"command\":\"native-sdk.os.showNotification\",\"payload\":{\"title\":\"Capabilities\",\"subtitle\":\"native-sdk\",\"body\":\"Done\"}}");
try std.testing.expect(std.mem.indexOf(u8, harness.null_platform.lastBridgeResponse(), "\"ok\":true") != null);
try std.testing.expectEqual(@as(usize, 1), harness.null_platform.notificationCount());
@@ -194,15 +273,37 @@ test "capabilities bridge gates native services and dispatches file drops" {
try std.testing.expect(std.mem.indexOf(u8, harness.null_platform.lastBridgeResponse(), "\"result\":true") != null);
const dropped_paths = [_][]const u8{ "/tmp/one\nname.txt", "/tmp/two.txt" };
try harness.runtime.dispatchPlatformEvent(app, .{ .files_dropped = .{
.window_id = 1,
.paths = &dropped_paths,
} });
try harness.runtime.dispatchPlatformEvent(app, .{
.files_dropped = .{
.window_id = 1,
.view_label = drop_canvas_label,
// x=300 is outside the initial 250-point surface and proves the
// resize event rebuilt the retained widget hit-test geometry.
.point = native_sdk.geometry.PointF.init(300, 40),
.paths = &dropped_paths,
},
});
try std.testing.expectEqual(@as(u32, 1), app_state.widget_drop_count);
try std.testing.expectEqual(drop_target_id, app_state.last_drop_target_id);
try std.testing.expectEqual(@as(u32, 1), app_state.drop_count);
try std.testing.expectEqual(@as(usize, 2), app_state.last_drop_paths.len);
try std.testing.expectEqualStrings("/tmp/one\nname.txt", app_state.last_drop_paths[0]);
try std.testing.expectEqualStrings("/tmp/two.txt", app_state.last_drop_paths[1]);
try std.testing.expectEqualStrings("drop:files", harness.null_platform.lastWindowEventName());
try std.testing.expect(std.mem.startsWith(u8, nullViewText(harness, "status-label"), "Widget target 2 fired"));
try std.testing.expect(app_state.pending_drop_target_id == null);
const webview_paths = [_][]const u8{"/tmp/webview.txt"};
try harness.runtime.dispatchPlatformEvent(app, .{ .files_dropped = .{
.window_id = 1,
.view_label = "main",
.point = native_sdk.geometry.PointF.init(40, 40),
.paths = &webview_paths,
} });
try std.testing.expectEqual(@as(u32, 1), app_state.widget_drop_count);
try std.testing.expect(app_state.pending_drop_target_id == null);
try std.testing.expectEqual(@as(u32, 2), app_state.drop_count);
try std.testing.expect(std.mem.startsWith(u8, nullViewText(harness, "status-label"), "Received file drop 2"));
try harness.runtime.dispatchPlatformEvent(app, .app_activated);
try std.testing.expectEqual(@as(u32, 1), app_state.activation_count);
@@ -213,6 +314,9 @@ test "capabilities bridge gates native services and dispatches file drops" {
}
test "capabilities manifest declares package integration metadata" {
try std.testing.expectEqual(window_min_width, app_manifest.shell.windows[0].min_width);
try std.testing.expectEqual(window_min_width, shell_windows[0].min_width);
try std.testing.expectEqual(@as(usize, 1), manifest_file_associations.len);
try std.testing.expectEqualStrings("Native SDK Capability Document", manifest_file_associations[0].name);
try std.testing.expectEqualStrings("viewer", manifest_file_associations[0].role);
@@ -231,3 +335,17 @@ fn dispatchBridge(harness: *native_sdk.TestHarness(), app: native_sdk.App, bytes
.webview_label = "main",
} });
}
fn nullViewText(harness: *native_sdk.TestHarness(), label: []const u8) []const u8 {
for (harness.null_platform.views[0..harness.null_platform.view_count]) |view| {
if (std.mem.eql(u8, view.label, label)) return view.text;
}
return "";
}
fn viewByLabel(views: []const native_sdk.ViewInfo, label: []const u8) ?native_sdk.ViewInfo {
for (views) |view| {
if (std.mem.eql(u8, view.label, label)) return view;
}
return null;
}
+63 -1
View File
@@ -58,6 +58,11 @@ pub const RunOptions = struct {
if (windows.len > 0) {
info.main_window = windows[0];
info.windows = windows;
} else {
info.main_window.default_frame = manifestShellStartupFrame(info.main_window.default_frame);
info.main_window.restore_state = manifestShellStartupRestoreState(info.main_window.restore_state);
info.main_window.restore_policy = manifestShellStartupRestorePolicy(info.main_window.restore_policy);
info.main_window.initial_placement = manifestShellStartupInitialPlacement(info.main_window.initial_placement);
}
return info;
}
@@ -138,6 +143,7 @@ fn manifestWindow(comptime window: anytype, comptime index: usize) native_sdk.Wi
.resizable = windowBool(window, "resizable", true),
.restore_state = windowBool(window, "restore_state", true),
.restore_policy = windowRestorePolicy(window),
.initial_placement = if (@hasField(@TypeOf(window), "x") or @hasField(@TypeOf(window), "y")) .explicit else .default,
};
}
@@ -171,6 +177,57 @@ fn windowRestorePolicy(comptime window: anytype) native_sdk.WindowRestorePolicy
@compileError("unknown app.zon window restore_policy");
}
fn windowHasExplicitOrigin(comptime window: anytype) bool {
return @hasField(@TypeOf(window), "x") or @hasField(@TypeOf(window), "y");
}
fn manifestShellStartupFrame(fallback: native_sdk.geometry.RectF) native_sdk.geometry.RectF {
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
const shell = app_manifest.shell;
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
if (comptime shell.windows.len == 0) return fallback;
const window = shell.windows[0];
return native_sdk.geometry.RectF.init(
windowFloatFallback(window, "x", fallback.x),
windowFloatFallback(window, "y", fallback.y),
windowFloatFallback(window, "width", fallback.width),
windowFloatFallback(window, "height", fallback.height),
);
}
fn windowFloatFallback(comptime window: anytype, comptime field: []const u8, fallback: f32) f32 {
if (comptime @hasField(@TypeOf(window), field)) return @field(window, field);
return fallback;
}
fn manifestShellStartupRestoreState(fallback: bool) bool {
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
const shell = app_manifest.shell;
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
if (comptime shell.windows.len == 0) return fallback;
const window = shell.windows[0];
if (comptime @hasField(@TypeOf(window), "restore_state")) return window.restore_state;
return fallback;
}
fn manifestShellStartupRestorePolicy(fallback: native_sdk.WindowRestorePolicy) native_sdk.WindowRestorePolicy {
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
const shell = app_manifest.shell;
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
if (comptime shell.windows.len == 0) return fallback;
const window = shell.windows[0];
if (comptime !@hasField(@TypeOf(window), "restore_policy")) return fallback;
return windowRestorePolicy(window);
}
fn manifestShellStartupInitialPlacement(fallback: native_sdk.WindowInitialPlacement) native_sdk.WindowInitialPlacement {
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
const shell = app_manifest.shell;
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
if (comptime shell.windows.len == 0) return fallback;
return if (windowHasExplicitOrigin(shell.windows[0])) .explicit else fallback;
}
fn shortcutModifiers(comptime shortcut: anytype) native_sdk.ShortcutModifiers {
const values = if (@hasField(@TypeOf(shortcut), "modifiers")) shortcut.modifiers else .{};
var modifiers: native_sdk.ShortcutModifiers = .{};
@@ -408,12 +465,17 @@ fn prepareStateStore(io: std.Io, env_map: *std.process.Environ.Map, app_info: *n
if (!window.restore_state) continue;
if (store.loadWindow(window.label, &buffers.read) catch null) |saved| {
window.default_frame = saved.frame;
if (index == 0) app_info.main_window.default_frame = saved.frame;
window.initial_placement = .restored;
if (index == 0) {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
}
} else if (app_info.main_window.restore_state) {
if (store.loadWindow(app_info.main_window.label, &buffers.read) catch null) |saved| {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
return store;
-1
View File
@@ -12,7 +12,6 @@
.title = "Native SDK Channel Monitor",
.width = 560,
.height = 420,
.restore_state = false,
.restore_policy = "center_on_primary",
.views = .{
.{ .label = "monitor-canvas", .kind = "gpu_surface", .fill = true, .role = "Channel monitor canvas", .accessibility_label = "Channel monitor", .gpu_backend = "metal", .gpu_pixel_format = "bgra8_unorm", .gpu_present_mode = "timer", .gpu_alpha_mode = "opaque", .gpu_color_space = "srgb", .gpu_vsync = true },
-2
View File
@@ -38,7 +38,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
.title = "Native SDK Channel Monitor",
.width = window_width,
.height = window_height,
.restore_state = false,
.views = &shell_views,
}};
const shell_scene: native_sdk.ShellConfig = .{ .windows = &shell_windows };
@@ -306,7 +305,6 @@ pub fn main(init: std.process.Init) !void {
.window_title = "Native SDK Channel Monitor",
.bundle_id = "dev.native_sdk.channel_monitor",
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
.restore_state = false,
.js_window_api = false,
.security = .{
.navigation = .{ .allowed_origins = &.{ "zero://inline", "zero://app" } },
-1
View File
@@ -21,7 +21,6 @@
.height = 640,
.min_width = 560,
.min_height = 420,
.restore_state = false,
.restore_policy = "center_on_primary",
.titlebar = "hidden_inset_tall",
.views = .{
-1
View File
@@ -24,7 +24,6 @@
.height = 720,
.min_width = 760,
.min_height = 480,
.restore_state = false,
.restore_policy = "center_on_primary",
.titlebar = "hidden_inset_tall",
.views = .{
-2
View File
@@ -65,7 +65,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
.height = window_height,
.min_width = window_min_width,
.min_height = window_min_height,
.restore_state = false,
.titlebar = .hidden_inset_tall,
.views = &shell_views,
}};
@@ -2038,7 +2037,6 @@ pub fn main(init: std.process.Init) !void {
.window_title = "Native SDK Code Editor",
.bundle_id = "dev.native_sdk.code_editor",
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
.restore_state = false,
.js_window_api = false,
.shortcuts = &app_shortcuts,
.security = .{
+63 -1
View File
@@ -61,6 +61,11 @@ pub const RunOptions = struct {
if (windows.len > 0) {
info.main_window = windows[0];
info.windows = windows;
} else {
info.main_window.default_frame = manifestShellStartupFrame(info.main_window.default_frame);
info.main_window.restore_state = manifestShellStartupRestoreState(info.main_window.restore_state);
info.main_window.restore_policy = manifestShellStartupRestorePolicy(info.main_window.restore_policy);
info.main_window.initial_placement = manifestShellStartupInitialPlacement(info.main_window.initial_placement);
}
return info;
}
@@ -207,6 +212,7 @@ fn manifestWindow(comptime window: anytype, comptime index: usize) native_sdk.Wi
.resizable = windowBool(window, "resizable", true),
.restore_state = windowBool(window, "restore_state", true),
.restore_policy = windowRestorePolicy(window),
.initial_placement = if (@hasField(@TypeOf(window), "x") or @hasField(@TypeOf(window), "y")) .explicit else .default,
};
}
@@ -240,6 +246,57 @@ fn windowRestorePolicy(comptime window: anytype) native_sdk.WindowRestorePolicy
@compileError("unknown app.zon window restore_policy");
}
fn windowHasExplicitOrigin(comptime window: anytype) bool {
return @hasField(@TypeOf(window), "x") or @hasField(@TypeOf(window), "y");
}
fn manifestShellStartupFrame(fallback: native_sdk.geometry.RectF) native_sdk.geometry.RectF {
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
const shell = app_manifest.shell;
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
if (comptime shell.windows.len == 0) return fallback;
const window = shell.windows[0];
return native_sdk.geometry.RectF.init(
windowFloatFallback(window, "x", fallback.x),
windowFloatFallback(window, "y", fallback.y),
windowFloatFallback(window, "width", fallback.width),
windowFloatFallback(window, "height", fallback.height),
);
}
fn windowFloatFallback(comptime window: anytype, comptime field: []const u8, fallback: f32) f32 {
if (comptime @hasField(@TypeOf(window), field)) return @field(window, field);
return fallback;
}
fn manifestShellStartupRestoreState(fallback: bool) bool {
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
const shell = app_manifest.shell;
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
if (comptime shell.windows.len == 0) return fallback;
const window = shell.windows[0];
if (comptime @hasField(@TypeOf(window), "restore_state")) return window.restore_state;
return fallback;
}
fn manifestShellStartupRestorePolicy(fallback: native_sdk.WindowRestorePolicy) native_sdk.WindowRestorePolicy {
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
const shell = app_manifest.shell;
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
if (comptime shell.windows.len == 0) return fallback;
const window = shell.windows[0];
if (comptime !@hasField(@TypeOf(window), "restore_policy")) return fallback;
return windowRestorePolicy(window);
}
fn manifestShellStartupInitialPlacement(fallback: native_sdk.WindowInitialPlacement) native_sdk.WindowInitialPlacement {
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
const shell = app_manifest.shell;
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
if (comptime shell.windows.len == 0) return fallback;
return if (windowHasExplicitOrigin(shell.windows[0])) .explicit else fallback;
}
fn menuItem(comptime item: anytype) native_sdk.MenuItem {
return .{
.label = if (@hasField(@TypeOf(item), "label")) item.label else "",
@@ -509,12 +566,17 @@ fn prepareStateStore(io: std.Io, env_map: *std.process.Environ.Map, app_info: *n
if (!window.restore_state) continue;
if (store.loadWindow(window.label, &buffers.read) catch null) |saved| {
window.default_frame = saved.frame;
if (index == 0) app_info.main_window.default_frame = saved.frame;
window.initial_placement = .restored;
if (index == 0) {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
}
} else if (app_info.main_window.restore_state) {
if (store.loadWindow(app_info.main_window.label, &buffers.read) catch null) |saved| {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
return store;
-1
View File
@@ -23,7 +23,6 @@
.height = 264,
.resizable = false,
.titlebar = "chromeless",
.restore_state = false,
.restore_policy = "center_on_primary",
.views = .{
.{ .label = "deck-canvas", .kind = "gpu_surface", .fill = true, .role = "Deck canvas", .accessibility_label = "Deck music player", .gpu_backend = "metal", .gpu_pixel_format = "bgra8_unorm", .gpu_present_mode = "timer", .gpu_alpha_mode = "opaque", .gpu_color_space = "srgb", .gpu_vsync = true },
-2
View File
@@ -66,7 +66,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
// styles instead, which keep the real OS buttons.
.resizable = false,
.titlebar = .chromeless,
.restore_state = false,
.views = &shell_views,
}};
pub const shell_scene: native_sdk.ShellConfig = .{ .windows = &shell_windows };
@@ -320,7 +319,6 @@ pub fn main(init: std.process.Init) !void {
.window_title = "Native SDK Deck",
.bundle_id = "dev.native_sdk.deck",
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
.restore_state = false,
.js_window_api = false,
.security = .{
.permissions = &app_permissions,
-1
View File
@@ -12,7 +12,6 @@
.title = "Native SDK Effects Probe",
.width = 560,
.height = 480,
.restore_state = false,
.restore_policy = "center_on_primary",
.views = .{
.{ .label = "probe-canvas", .kind = "gpu_surface", .fill = true, .role = "Effects probe canvas", .accessibility_label = "Effects probe", .gpu_backend = "metal", .gpu_pixel_format = "bgra8_unorm", .gpu_present_mode = "timer", .gpu_alpha_mode = "opaque", .gpu_color_space = "srgb", .gpu_vsync = true },
-2
View File
@@ -29,7 +29,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
.title = "Native SDK Effects Probe",
.width = window_width,
.height = window_height,
.restore_state = false,
.views = &shell_views,
}};
const shell_scene: native_sdk.ShellConfig = .{ .windows = &shell_windows };
@@ -206,7 +205,6 @@ pub fn main(init: std.process.Init) !void {
.window_title = "Native SDK Effects Probe",
.bundle_id = "dev.native_sdk.effects_probe",
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
.restore_state = false,
.js_window_api = false,
.security = .{
.navigation = .{ .allowed_origins = &.{ "zero://inline", "zero://app" } },
-1
View File
@@ -17,7 +17,6 @@
// (header + fixed-extent timeline rows).
.min_width = 520,
.min_height = 480,
.restore_state = false,
.restore_policy = "center_on_primary",
.titlebar = "hidden_inset_tall",
.views = .{
-2
View File
@@ -56,7 +56,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
.height = window_height,
.min_width = window_min_width,
.min_height = window_min_height,
.restore_state = false,
// Tall hidden-inset titlebar (declared in app.zon too, which threads
// it through the STARTUP window create): the header bar IS the
// titlebar — it pads its leading edge past the traffic lights via
@@ -468,7 +467,6 @@ pub fn main(init: std.process.Init) !void {
.window_title = "Native SDK Feed",
.bundle_id = "dev.native_sdk.feed",
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
.restore_state = false,
.js_window_api = false,
.security = .{
.navigation = .{ .allowed_origins = &.{ "zero://inline", "zero://app" } },
+1 -1
View File
@@ -2,7 +2,7 @@
An isolated gallery of the built-in Native UI components, authored entirely in **TypeScript + Native markup**. There is no app-owned Zig: `src/core.ts` owns controlled state, `src/app.native` owns the component tree and specimens, and `app.zon` describes the desktop shell.
The left pane has a live Default/Geist theme-pack selector and a real disclosure `tree` whose rows use the built-in roving keyboard focus and scroll-into-view behavior. The right pane renders only the selected component. The selector changes the pack in the TypeScript model while the runtime keeps following system appearance. Accordion disclosure, dropdown/select/combobox menus, modal surfaces, fields, sliders, tabs, lists, and the focused Tree specimen are all interactive examples of the public markup API.
The left pane has live pack, System/Light/Dark scheme, and accent selectors backed by the TypeScript model and one `themeState(model)` helper. “Default” accent inherits the purple `app.zon` accent; Pink and Teal override it. The pane also contains a real disclosure `tree` whose rows use the built-in roving keyboard focus and scroll-into-view behavior. The right pane renders only the selected component. Accordion disclosure, dropdown/select/combobox menus, modal surfaces, fields, sliders, tabs, lists, and the focused Tree specimen are all interactive examples of the public markup API.
Run the app with the repository CLI:
+1 -1
View File
@@ -4,6 +4,7 @@
.display_name = "GPU Components",
.description = "An isolated gallery of interactive Native UI components authored in TypeScript and Native markup.",
.version = "0.1.0",
.theme_accent = "#7c3aed",
.platforms = .{"macos"},
.permissions = .{ "view", "command" },
.capabilities = .{ "native_views", "gpu_surfaces" },
@@ -16,7 +17,6 @@
.height = 760,
.min_width = 760,
.min_height = 520,
.restore_state = false,
.restore_policy = "center_on_primary",
.views = .{
.{ .label = "components-canvas", .kind = "gpu_surface", .fill = true, .min_width = 640, .role = "Native component gallery", .accessibility_label = "Interactive Native component gallery", .gpu_backend = "metal", .gpu_pixel_format = "bgra8_unorm", .gpu_present_mode = "timer", .gpu_alpha_mode = "opaque", .gpu_color_space = "srgb", .gpu_vsync = true },
+1 -1
View File
@@ -3,6 +3,6 @@
"private": true,
"description": "Editor surface for the TypeScript core; the native CLI builds without node_modules.",
"dependencies": {
"@native-sdk/core": "0.9.1"
"@native-sdk/core": "0.9.3"
}
}
+16 -4
View File
@@ -4,10 +4,22 @@
<row background="background">
<column width="248" padding="12" gap="8" background="surface" label="Component navigation">
<column gap="6">
<text foreground="text_muted">Theme</text>
<toggle-group gap="2" label="Theme">
<toggle-button size="sm" selected="{themePack == 'house'}" on-toggle="theme_house">Default</toggle-button>
<toggle-button size="sm" selected="{themePack == 'geist'}" on-toggle="theme_geist">Geist</toggle-button>
<text foreground="text_muted">Pack</text>
<toggle-group gap="2" label="Theme pack">
<toggle-button size="sm" selected="{theme == 'house'}" on-toggle="theme_house">Default</toggle-button>
<toggle-button size="sm" selected="{theme == 'geist'}" on-toggle="theme_geist">Geist</toggle-button>
</toggle-group>
<text foreground="text_muted">Scheme</text>
<toggle-group gap="2" label="Color scheme">
<toggle-button size="sm" selected="{themeColorScheme == 'system'}" on-toggle="theme_system">System</toggle-button>
<toggle-button size="sm" selected="{themeColorScheme == 'light'}" on-toggle="theme_light">Light</toggle-button>
<toggle-button size="sm" selected="{themeColorScheme == 'dark'}" on-toggle="theme_dark">Dark</toggle-button>
</toggle-group>
<text foreground="text_muted">Accent</text>
<toggle-group gap="2" label="Theme accent">
<toggle-button size="sm" selected="{themeAccent == 'manifest'}" on-toggle="accent_manifest">Default</toggle-button>
<toggle-button size="sm" selected="{themeAccent == 'pink'}" on-toggle="accent_pink">Pink</toggle-button>
<toggle-button size="sm" selected="{themeAccent == 'teal'}" on-toggle="accent_teal">Teal</toggle-button>
</toggle-group>
</column>
<separator />
+36 -6
View File
@@ -3,6 +3,7 @@
// controlled component state and the messages produced by interaction.
import { Cmd, asciiBytes } from "@native-sdk/core";
import { type ThemeState } from "@native-sdk/core/events";
import {
applyTextInputEvent,
clampedInsertEvent,
@@ -109,6 +110,8 @@ function applyDraft(value: Draft, event: TextInputEvent): Draft {
export type Density = "default" | "comfortable";
export type ThemePack = "house" | "geist";
export type ThemeColorScheme = "system" | "light" | "dark";
export type ThemeAccent = "manifest" | "pink" | "teal";
export type DropdownChoice = "none" | "duplicate" | "rename" | "download" | "delete";
export type SelectChoice = "production" | "staging" | "development";
export type Tab = "account" | "password" | "team";
@@ -120,6 +123,8 @@ export interface Model {
readonly components: readonly ComponentItem[];
readonly selectedComponentId: number;
readonly theme: ThemePack;
readonly themeColorScheme: ThemeColorScheme;
readonly themeAccent: ThemeAccent;
readonly catalogExpanded: boolean;
readonly accordionOpen: boolean;
readonly checkboxChecked: boolean;
@@ -157,6 +162,12 @@ export type Msg =
| { readonly kind: "select_component"; readonly componentId: number }
| { readonly kind: "theme_house" }
| { readonly kind: "theme_geist" }
| { readonly kind: "theme_system" }
| { readonly kind: "theme_light" }
| { readonly kind: "theme_dark" }
| { readonly kind: "accent_manifest" }
| { readonly kind: "accent_pink" }
| { readonly kind: "accent_teal" }
| { readonly kind: "toggle_catalog" }
| { readonly kind: "action" }
| { readonly kind: "toggle_accordion" }
@@ -220,7 +231,6 @@ export type Msg =
// These records are intentionally read only through binding helpers.
export const viewUnbound = [
"theme",
"comboboxDraft",
"inputDraft",
"textareaDraft",
@@ -232,6 +242,8 @@ export function initialModel(): Model {
components: COMPONENTS,
selectedComponentId: 1,
theme: "house",
themeColorScheme: "system",
themeAccent: "manifest",
catalogExpanded: true,
accordionOpen: true,
checkboxChecked: true,
@@ -285,11 +297,17 @@ export function selectedLabel(model: Model): Uint8Array {
return asciiBytes("Component");
}
// The default TypeScript launcher recognizes this exported single-model
// helper and selects the built-in pack on every rebuild. System light/dark,
// contrast, reduced-motion, accent, and surface scale remain runtime-owned.
export function themePack(model: Model): ThemePack {
return model.theme;
// One model helper owns the stock theme's author-facing axes. Omitting the
// accent inherits app.zon's theme_accent; `system` follows the OS. High
// contrast/reduced motion and each surface's scale stay runtime-owned.
export function themeState(model: Model): ThemeState {
if (model.themeAccent === "pink") {
return { pack: model.theme, colorScheme: model.themeColorScheme, accent: "#df2670" };
}
if (model.themeAccent === "teal") {
return { pack: model.theme, colorScheme: model.themeColorScheme, accent: "#00786f" };
}
return { pack: model.theme, colorScheme: model.themeColorScheme };
}
export function selectLabel(model: Model): Uint8Array {
@@ -338,6 +356,18 @@ export function update(model: Model, msg: Msg): [Model, Cmd<Msg>] {
return [{ ...model, theme: "house" }, Cmd.none];
case "theme_geist":
return [{ ...model, theme: "geist" }, Cmd.none];
case "theme_system":
return [{ ...model, themeColorScheme: "system" }, Cmd.none];
case "theme_light":
return [{ ...model, themeColorScheme: "light" }, Cmd.none];
case "theme_dark":
return [{ ...model, themeColorScheme: "dark" }, Cmd.none];
case "accent_manifest":
return [{ ...model, themeAccent: "manifest" }, Cmd.none];
case "accent_pink":
return [{ ...model, themeAccent: "pink" }, Cmd.none];
case "accent_teal":
return [{ ...model, themeAccent: "teal" }, Cmd.none];
case "toggle_catalog":
return [{ ...model, catalogExpanded: !model.catalogExpanded }, Cmd.none];
case "action":
-1
View File
@@ -17,7 +17,6 @@
// (toolbar + hero rail + tiles + forecast form).
.min_width = 1080,
.min_height = 640,
.restore_state = false,
.restore_policy = "center_on_primary",
.titlebar = "hidden_inset_tall",
.views = .{
-2
View File
@@ -122,7 +122,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
.height = window_height,
.min_width = window_min_width,
.min_height = window_min_height,
.restore_state = false,
// Tall hidden-inset titlebar (declared in app.zon too, which threads
// it through the STARTUP window create): the toolbar IS the titlebar
// — its leading reserve shifts right by the live chrome inset so the
@@ -865,7 +864,6 @@ pub fn main(init: std.process.Init) !void {
.window_title = "Native SDK GPU Dashboard",
.bundle_id = "dev.native_sdk.gpu_dashboard",
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
.restore_state = false,
.js_window_api = false,
.security = .{
.permissions = &app_permissions,
-1
View File
@@ -14,7 +14,6 @@
.title = "Native SDK Habits",
.width = 720,
.height = 520,
.restore_state = false,
.restore_policy = "center_on_primary",
.titlebar = "hidden_inset_tall",
.views = .{
-2
View File
@@ -41,7 +41,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
.title = "Native SDK Habits",
.width = window_width,
.height = window_height,
.restore_state = false,
// Tall hidden-inset titlebar (declared in app.zon too, which threads
// it through the STARTUP window create): the header row IS the
// titlebar — it pads its leading edge past the traffic lights via
@@ -225,7 +224,6 @@ pub fn main(init: std.process.Init) !void {
.window_title = "Native SDK Habits",
.bundle_id = "dev.native_sdk.habits",
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
.restore_state = false,
.js_window_api = false,
.security = .{
.permissions = &app_permissions,
+7 -1
View File
@@ -129,6 +129,7 @@ fn manifestWindow(comptime window: anytype, comptime index: usize) native_sdk.Wi
.resizable = windowBool(window, "resizable", true),
.restore_state = windowBool(window, "restore_state", true),
.restore_policy = windowRestorePolicy(window),
.initial_placement = if (@hasField(@TypeOf(window), "x") or @hasField(@TypeOf(window), "y")) .explicit else .default,
};
}
@@ -391,12 +392,17 @@ fn prepareStateStore(io: std.Io, env_map: *std.process.Environ.Map, app_info: *n
if (!window.restore_state) continue;
if (store.loadWindow(window.label, &buffers.read) catch null) |saved| {
window.default_frame = saved.frame;
if (index == 0) app_info.main_window.default_frame = saved.frame;
window.initial_placement = .restored;
if (index == 0) {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
}
} else if (app_info.main_window.restore_state) {
if (store.loadWindow(app_info.main_window.label, &buffers.read) catch null) |saved| {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
return store;
+9
View File
@@ -4,6 +4,15 @@ This three-column board is authored entirely in TypeScript + Native markup. `src
Drop one or more files anywhere on the board to add their basenames as Todo cards. The desktop host sends the native file-drop event through the runtime, and the core's `dropMsg` maps the full path list into one deterministic `files_dropped` message before `update` changes the board.
To manually verify the real macOS host path (the guest-VM harness cannot synthesize an AppKit drag session yet):
1. Run `native dev` on macOS.
2. Drag a file from Finder onto a visible card in the canvas, not the titlebar.
3. Verify a new Todo card appears with the dropped file's basename.
4. Repeat over empty board space; the app-level drop still works there.
The first drop traverses the labeled `kanban-canvas` destination with a view-local, top-left-origin point before the app-level `dropMsg` runs. That is the same host data widget `drop_files` hit-testing consumes; the second verifies the ordinary canvas-level fallback remains intact.
Drag any card within a column or across Todo, Doing, and Done. The card itself lifts under the pointer at full opacity, leaving one blank, card-sized slot behind. As the pointer reaches another candidate position, that same reserved slot moves from the source to the candidate and neighboring cards glide around it—there are never two spaces for one card. On release, the same floating card eases from the pointer into the slot. Press Escape during a drag to cancel it and carry the card back to its source slot. Cards can move forwards, backwards, or directly across the board.
Each card represents an agent-owned ticket: the title sits above a compact metadata row with a Jira/Linear-style issue key and the assigned OpenAI or Claude avatar. The avatar artwork is rasterized from SVGL's [OpenAI](https://svgl.app/library/openai.svg) and [Claude AI](https://svgl.app/library/claude-ai-icon.svg) SVGs so it can travel through the app manifest's static image channel.
-1
View File
@@ -27,7 +27,6 @@
// (proven by the layout audit sweep).
.min_width = 840,
.min_height = 560,
.restore_state = false,
.restore_policy = "center_on_primary",
.titlebar = "hidden_inset_tall",
.views = .{
+1 -1
View File
@@ -3,6 +3,6 @@
"private": true,
"description": "Editor surface for the TypeScript core; the native CLI builds without node_modules.",
"dependencies": {
"@native-sdk/core": "0.9.1"
"@native-sdk/core": "0.9.3"
}
}
-1
View File
@@ -19,7 +19,6 @@
// resizing here instead of clipping the panes below it.
.min_width = 960,
.min_height = 560,
.restore_state = false,
.restore_policy = "center_on_primary",
.titlebar = "hidden_inset_tall",
.views = .{
-2
View File
@@ -88,7 +88,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
.height = window_height,
.min_width = window_min_width,
.min_height = window_min_height,
.restore_state = false,
// Tall hidden-inset titlebar (declared in app.zon too, which
// threads it through the STARTUP window create): the toolbar row is
// toolbar-height, so the TALL band centers the traffic lights
@@ -845,7 +844,6 @@ pub fn main(init: std.process.Init) !void {
.window_title = "Native SDK Markdown",
.bundle_id = "dev.native_sdk.markdown_viewer",
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
.restore_state = false,
.js_window_api = false,
.security = .{
.permissions = &app_permissions,
+2 -1
View File
@@ -5,7 +5,8 @@ This app is the complete hide-to-tray lifecycle in the default TypeScript + Nati
- `app.zon` sets `dock_visible = false`, so macOS selects Accessory before creating a window and no Dock tile flashes.
- The main window starts with `initially_hidden = true` and uses `close_policy = "hide"`; the status item is the only open/re-show affordance.
- `src/core.ts` exports `statusItem(model)`, whose presentation and menu update from committed playback state.
- Tray commands pass through `commandMsg`; Open uses `Cmd.showWindow("main")`, and Quit uses `Cmd.quitApp()` for graceful termination.
- `app.zon` declares the app command catalog, native Player menu, and keyboard shortcuts; the zero-config runner loads all three automatically.
- App-menu, shortcut, and tray commands pass through `commandMsg`; Open uses `Cmd.showWindow("main")`, and Quit uses `Cmd.quitApp()` for graceful termination.
- Playback changes issue `Cmd.persist()`, so the engine restores the last playing/track state from its atomic app-data snapshot on the next launch.
- `src/app.native` is the ordinary player window. No app-owned Zig glue is involved.
+27 -2
View File
@@ -4,7 +4,7 @@
.display_name = "Menu Bar",
.version = "0.1.0",
.platforms = .{"macos"},
.capabilities = .{ "native_views", "gpu_surfaces", "tray", "persist" },
.capabilities = .{ "native_views", "gpu_surfaces", "tray", "persist", "menus", "shortcuts" },
// Launch as an Accessory app. The activation policy is selected before
// the startup window exists, so no Dock tile or cmd+Tab entry flashes.
.dock_visible = false,
@@ -16,6 +16,32 @@
.err = "restore_failed",
},
},
// The zero-config framework runner reads all three declarations and
// routes them through core.ts's commandMsg mapper. No runner.zig is
// needed in the app tree.
.commands = .{
.{ .id = "app.open", .title = "Open Player" },
.{ .id = "player.toggle", .title = "Toggle Playback" },
.{ .id = "player.next", .title = "Next Track" },
.{ .id = "app.quit", .title = "Quit" },
},
.shortcuts = .{
.{ .id = "player.toggle", .key = "p", .modifiers = .{ "primary" } },
.{ .id = "player.next", .key = "n", .modifiers = .{ "primary" } },
},
.menus = .{
.{
.title = "Player",
.items = .{
.{ .label = "Open Player", .command = "app.open" },
.{ .separator = true },
.{ .label = "Play or Pause", .command = "player.toggle", .key = "p", .modifiers = .{ "primary" } },
.{ .label = "Next Track", .command = "player.next", .key = "n", .modifiers = .{ "primary" } },
.{ .separator = true },
.{ .label = "Quit", .command = "app.quit", .key = "q", .modifiers = .{ "primary" } },
},
},
},
.shell = .{
.windows = .{
.{
@@ -23,7 +49,6 @@
.title = "Menu Bar Player",
.width = 420,
.height = 260,
.restore_state = false,
.restore_policy = "center_on_primary",
// A true menu-bar app starts behind its status item. The
// tray's Open Player command is the initial reveal path.
+1 -1
View File
@@ -3,6 +3,6 @@
"private": true,
"description": "TypeScript + Native markup menu-bar lifecycle example.",
"dependencies": {
"@native-sdk/core": "0.9.1"
"@native-sdk/core": "0.9.3"
}
}
+63 -1
View File
@@ -58,6 +58,11 @@ pub const RunOptions = struct {
if (windows.len > 0) {
info.main_window = windows[0];
info.windows = windows;
} else {
info.main_window.default_frame = manifestShellStartupFrame(info.main_window.default_frame);
info.main_window.restore_state = manifestShellStartupRestoreState(info.main_window.restore_state);
info.main_window.restore_policy = manifestShellStartupRestorePolicy(info.main_window.restore_policy);
info.main_window.initial_placement = manifestShellStartupInitialPlacement(info.main_window.initial_placement);
}
return info;
}
@@ -138,6 +143,7 @@ fn manifestWindow(comptime window: anytype, comptime index: usize) native_sdk.Wi
.resizable = windowBool(window, "resizable", true),
.restore_state = windowBool(window, "restore_state", true),
.restore_policy = windowRestorePolicy(window),
.initial_placement = if (@hasField(@TypeOf(window), "x") or @hasField(@TypeOf(window), "y")) .explicit else .default,
};
}
@@ -171,6 +177,57 @@ fn windowRestorePolicy(comptime window: anytype) native_sdk.WindowRestorePolicy
@compileError("unknown app.zon window restore_policy");
}
fn windowHasExplicitOrigin(comptime window: anytype) bool {
return @hasField(@TypeOf(window), "x") or @hasField(@TypeOf(window), "y");
}
fn manifestShellStartupFrame(fallback: native_sdk.geometry.RectF) native_sdk.geometry.RectF {
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
const shell = app_manifest.shell;
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
if (comptime shell.windows.len == 0) return fallback;
const window = shell.windows[0];
return native_sdk.geometry.RectF.init(
windowFloatFallback(window, "x", fallback.x),
windowFloatFallback(window, "y", fallback.y),
windowFloatFallback(window, "width", fallback.width),
windowFloatFallback(window, "height", fallback.height),
);
}
fn windowFloatFallback(comptime window: anytype, comptime field: []const u8, fallback: f32) f32 {
if (comptime @hasField(@TypeOf(window), field)) return @field(window, field);
return fallback;
}
fn manifestShellStartupRestoreState(fallback: bool) bool {
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
const shell = app_manifest.shell;
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
if (comptime shell.windows.len == 0) return fallback;
const window = shell.windows[0];
if (comptime @hasField(@TypeOf(window), "restore_state")) return window.restore_state;
return fallback;
}
fn manifestShellStartupRestorePolicy(fallback: native_sdk.WindowRestorePolicy) native_sdk.WindowRestorePolicy {
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
const shell = app_manifest.shell;
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
if (comptime shell.windows.len == 0) return fallback;
const window = shell.windows[0];
if (comptime !@hasField(@TypeOf(window), "restore_policy")) return fallback;
return windowRestorePolicy(window);
}
fn manifestShellStartupInitialPlacement(fallback: native_sdk.WindowInitialPlacement) native_sdk.WindowInitialPlacement {
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
const shell = app_manifest.shell;
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
if (comptime shell.windows.len == 0) return fallback;
return if (windowHasExplicitOrigin(shell.windows[0])) .explicit else fallback;
}
fn shortcutModifiers(comptime shortcut: anytype) native_sdk.ShortcutModifiers {
const values = if (@hasField(@TypeOf(shortcut), "modifiers")) shortcut.modifiers else .{};
var modifiers: native_sdk.ShortcutModifiers = .{};
@@ -408,12 +465,17 @@ fn prepareStateStore(io: std.Io, env_map: *std.process.Environ.Map, app_info: *n
if (!window.restore_state) continue;
if (store.loadWindow(window.label, &buffers.read) catch null) |saved| {
window.default_frame = saved.frame;
if (index == 0) app_info.main_window.default_frame = saved.frame;
window.initial_placement = .restored;
if (index == 0) {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
}
} else if (app_info.main_window.restore_state) {
if (store.loadWindow(app_info.main_window.label, &buffers.read) catch null) |saved| {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
return store;
+63 -1
View File
@@ -61,6 +61,11 @@ pub const RunOptions = struct {
if (windows.len > 0) {
info.main_window = windows[0];
info.windows = windows;
} else {
info.main_window.default_frame = manifestShellStartupFrame(info.main_window.default_frame);
info.main_window.restore_state = manifestShellStartupRestoreState(info.main_window.restore_state);
info.main_window.restore_policy = manifestShellStartupRestorePolicy(info.main_window.restore_policy);
info.main_window.initial_placement = manifestShellStartupInitialPlacement(info.main_window.initial_placement);
}
return info;
}
@@ -207,6 +212,7 @@ fn manifestWindow(comptime window: anytype, comptime index: usize) native_sdk.Wi
.resizable = windowBool(window, "resizable", true),
.restore_state = windowBool(window, "restore_state", true),
.restore_policy = windowRestorePolicy(window),
.initial_placement = if (@hasField(@TypeOf(window), "x") or @hasField(@TypeOf(window), "y")) .explicit else .default,
};
}
@@ -240,6 +246,57 @@ fn windowRestorePolicy(comptime window: anytype) native_sdk.WindowRestorePolicy
@compileError("unknown app.zon window restore_policy");
}
fn windowHasExplicitOrigin(comptime window: anytype) bool {
return @hasField(@TypeOf(window), "x") or @hasField(@TypeOf(window), "y");
}
fn manifestShellStartupFrame(fallback: native_sdk.geometry.RectF) native_sdk.geometry.RectF {
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
const shell = app_manifest.shell;
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
if (comptime shell.windows.len == 0) return fallback;
const window = shell.windows[0];
return native_sdk.geometry.RectF.init(
windowFloatFallback(window, "x", fallback.x),
windowFloatFallback(window, "y", fallback.y),
windowFloatFallback(window, "width", fallback.width),
windowFloatFallback(window, "height", fallback.height),
);
}
fn windowFloatFallback(comptime window: anytype, comptime field: []const u8, fallback: f32) f32 {
if (comptime @hasField(@TypeOf(window), field)) return @field(window, field);
return fallback;
}
fn manifestShellStartupRestoreState(fallback: bool) bool {
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
const shell = app_manifest.shell;
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
if (comptime shell.windows.len == 0) return fallback;
const window = shell.windows[0];
if (comptime @hasField(@TypeOf(window), "restore_state")) return window.restore_state;
return fallback;
}
fn manifestShellStartupRestorePolicy(fallback: native_sdk.WindowRestorePolicy) native_sdk.WindowRestorePolicy {
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
const shell = app_manifest.shell;
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
if (comptime shell.windows.len == 0) return fallback;
const window = shell.windows[0];
if (comptime !@hasField(@TypeOf(window), "restore_policy")) return fallback;
return windowRestorePolicy(window);
}
fn manifestShellStartupInitialPlacement(fallback: native_sdk.WindowInitialPlacement) native_sdk.WindowInitialPlacement {
if (comptime !@hasField(@TypeOf(app_manifest), "shell")) return fallback;
const shell = app_manifest.shell;
if (comptime !@hasField(@TypeOf(shell), "windows")) return fallback;
if (comptime shell.windows.len == 0) return fallback;
return if (windowHasExplicitOrigin(shell.windows[0])) .explicit else fallback;
}
fn menuItem(comptime item: anytype) native_sdk.MenuItem {
return .{
.label = if (@hasField(@TypeOf(item), "label")) item.label else "",
@@ -509,12 +566,17 @@ fn prepareStateStore(io: std.Io, env_map: *std.process.Environ.Map, app_info: *n
if (!window.restore_state) continue;
if (store.loadWindow(window.label, &buffers.read) catch null) |saved| {
window.default_frame = saved.frame;
if (index == 0) app_info.main_window.default_frame = saved.frame;
window.initial_placement = .restored;
if (index == 0) {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
}
} else if (app_info.main_window.restore_state) {
if (store.loadWindow(app_info.main_window.label, &buffers.read) catch null) |saved| {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
return store;
+7 -1
View File
@@ -129,6 +129,7 @@ fn manifestWindow(comptime window: anytype, comptime index: usize) native_sdk.Wi
.resizable = windowBool(window, "resizable", true),
.restore_state = windowBool(window, "restore_state", true),
.restore_policy = windowRestorePolicy(window),
.initial_placement = if (@hasField(@TypeOf(window), "x") or @hasField(@TypeOf(window), "y")) .explicit else .default,
};
}
@@ -391,12 +392,17 @@ fn prepareStateStore(io: std.Io, env_map: *std.process.Environ.Map, app_info: *n
if (!window.restore_state) continue;
if (store.loadWindow(window.label, &buffers.read) catch null) |saved| {
window.default_frame = saved.frame;
if (index == 0) app_info.main_window.default_frame = saved.frame;
window.initial_placement = .restored;
if (index == 0) {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
}
} else if (app_info.main_window.restore_state) {
if (store.loadWindow(app_info.main_window.label, &buffers.read) catch null) |saved| {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
return store;
-1
View File
@@ -37,7 +37,6 @@
// resizing here instead of clipping the panes below it.
.min_width = 760,
.min_height = 520,
.restore_state = false,
.restore_policy = "center_on_primary",
.titlebar = "hidden_inset_tall",
.views = .{
-2
View File
@@ -51,7 +51,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
.height = window_height,
.min_width = window_min_width,
.min_height = window_min_height,
.restore_state = false,
// Tall hidden-inset titlebar (declared in app.zon too, which threads
// it through the STARTUP window create): the header row IS the
// titlebar — it pads its leading edge past the traffic lights via
@@ -273,7 +272,6 @@ pub fn main(init: std.process.Init) !void {
.bundle_id = "dev.native_sdk.notes",
.icon_path = "assets/icon.png",
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
.restore_state = false,
.js_window_api = false,
.security = .{
.permissions = &app_permissions,
+7 -1
View File
@@ -129,6 +129,7 @@ fn manifestWindow(comptime window: anytype, comptime index: usize) native_sdk.Wi
.resizable = windowBool(window, "resizable", true),
.restore_state = windowBool(window, "restore_state", true),
.restore_policy = windowRestorePolicy(window),
.initial_placement = if (@hasField(@TypeOf(window), "x") or @hasField(@TypeOf(window), "y")) .explicit else .default,
};
}
@@ -391,12 +392,17 @@ fn prepareStateStore(io: std.Io, env_map: *std.process.Environ.Map, app_info: *n
if (!window.restore_state) continue;
if (store.loadWindow(window.label, &buffers.read) catch null) |saved| {
window.default_frame = saved.frame;
if (index == 0) app_info.main_window.default_frame = saved.frame;
window.initial_placement = .restored;
if (index == 0) {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
}
} else if (app_info.main_window.restore_state) {
if (store.loadWindow(app_info.main_window.label, &buffers.read) catch null) |saved| {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
return store;
-1
View File
@@ -16,7 +16,6 @@
.height = 480,
.min_width = 520,
.min_height = 360,
.restore_state = false,
.restore_policy = "center_on_primary",
.views = .{
.{ .label = "record-store-canvas", .kind = "gpu_surface", .fill = true, .role = "Record store example", .accessibility_label = "Record store example", .gpu_backend = "metal", .gpu_pixel_format = "bgra8_unorm", .gpu_present_mode = "timer", .gpu_alpha_mode = "opaque", .gpu_color_space = "srgb", .gpu_vsync = true },
+1 -1
View File
@@ -3,6 +3,6 @@
"private": true,
"description": "Editor surface for the TypeScript core; the native CLI builds without node_modules.",
"dependencies": {
"@native-sdk/core": "0.9.1"
"@native-sdk/core": "0.9.3"
}
}
-1
View File
@@ -15,7 +15,6 @@
.title = "Relational Notes",
.width = 860,
.height = 620,
.restore_state = false,
.restore_policy = "center_on_primary",
.views = .{
.{ .label = "main-canvas", .kind = "gpu_surface", .fill = true, .role = "Relational notes canvas", .accessibility_label = "Relational Notes", .gpu_backend = "metal", .gpu_pixel_format = "bgra8_unorm", .gpu_present_mode = "timer", .gpu_alpha_mode = "opaque", .gpu_color_space = "srgb", .gpu_vsync = true },
+1 -1
View File
@@ -3,6 +3,6 @@
"private": true,
"description": "Editor and versioning surface only: stock TypeScript tooling resolves @native-sdk/core from here. The native CLI never reads it and builds with node_modules absent.",
"dependencies": {
"@native-sdk/core": "0.9.1"
"@native-sdk/core": "0.9.3"
}
}
-1
View File
@@ -17,7 +17,6 @@
.height = 480,
.min_width = 460,
.min_height = 320,
.restore_state = false,
.restore_policy = "center_on_primary",
.views = .{.{
.label = "feed-canvas",
-1
View File
@@ -53,7 +53,6 @@
// instead of clipping the grid.
.min_width = 1056,
.min_height = 600,
.restore_state = false,
.restore_policy = "center_on_primary",
// Tall hidden-inset titlebar, the Zig original's window
// chrome: the markup header is the drag surface and pads
+1 -1
View File
@@ -3,6 +3,6 @@
"private": true,
"description": "Editor and versioning surface only: stock TypeScript tooling resolves @native-sdk/core from here. The native CLI never reads it and builds with node_modules absent.",
"dependencies": {
"@native-sdk/core": "0.9.1"
"@native-sdk/core": "0.9.3"
}
}
-1
View File
@@ -25,7 +25,6 @@
// instead of clipping the grid.
.min_width = 1056,
.min_height = 600,
.restore_state = false,
.restore_policy = "center_on_primary",
.titlebar = "hidden_inset_tall",
.views = .{
-2
View File
@@ -59,7 +59,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
.height = window_height,
.min_width = window_min_width,
.min_height = window_min_height,
.restore_state = false,
// Tall hidden-inset titlebar (declared in app.zon too, which threads
// it through the STARTUP window create): the header bar IS the
// titlebar — it pads its leading edge past the traffic lights via
@@ -491,7 +490,6 @@ pub fn main(init: std.process.Init) !void {
.window_title = "Native SDK Soundboard",
.bundle_id = "dev.native_sdk.soundboard",
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
.restore_state = false,
.js_window_api = false,
.security = .{
.permissions = &app_permissions,
-1
View File
@@ -14,7 +14,6 @@
.title = "Split Collapse",
.width = 800,
.height = 520,
.restore_state = false,
.views = .{
.{ .label = "split-canvas", .kind = "gpu_surface", .fill = true, .role = "Split canvas", .accessibility_label = "Split collapse", .gpu_backend = "metal", .gpu_pixel_format = "bgra8_unorm", .gpu_present_mode = "timer", .gpu_alpha_mode = "opaque", .gpu_color_space = "srgb", .gpu_vsync = true },
},
-2
View File
@@ -302,7 +302,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
.title = "Split Collapse",
.width = window_width,
.height = window_height,
.restore_state = false,
.views = &shell_views,
}};
pub const shell_scene: native_sdk.ShellConfig = .{ .windows = &shell_windows };
@@ -361,7 +360,6 @@ pub fn main(init: std.process.Init) !void {
.window_title = "Split Collapse",
.bundle_id = "dev.native_sdk.split_collapse",
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
.restore_state = false,
.js_window_api = false,
.security = .{
.permissions = &app_permissions,
+7 -1
View File
@@ -129,6 +129,7 @@ fn manifestWindow(comptime window: anytype, comptime index: usize) native_sdk.Wi
.resizable = windowBool(window, "resizable", true),
.restore_state = windowBool(window, "restore_state", true),
.restore_policy = windowRestorePolicy(window),
.initial_placement = if (@hasField(@TypeOf(window), "x") or @hasField(@TypeOf(window), "y")) .explicit else .default,
};
}
@@ -391,12 +392,17 @@ fn prepareStateStore(io: std.Io, env_map: *std.process.Environ.Map, app_info: *n
if (!window.restore_state) continue;
if (store.loadWindow(window.label, &buffers.read) catch null) |saved| {
window.default_frame = saved.frame;
if (index == 0) app_info.main_window.default_frame = saved.frame;
window.initial_placement = .restored;
if (index == 0) {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
}
} else if (app_info.main_window.restore_state) {
if (store.loadWindow(app_info.main_window.label, &buffers.read) catch null) |saved| {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
return store;
-1
View File
@@ -25,7 +25,6 @@
// begin (the Zig original's rule).
.min_width = 1144,
.min_height = 720,
.restore_state = false,
.restore_policy = "center_on_primary",
// The header bar IS the titlebar: the core's chromeMsg
// channel delivers the band geometry, and the header row
+1 -1
View File
@@ -3,6 +3,6 @@
"private": true,
"description": "Editor and versioning surface only: stock TypeScript tooling resolves @native-sdk/core from here. The native CLI never reads it and builds with node_modules absent.",
"dependencies": {
"@native-sdk/core": "0.9.1"
"@native-sdk/core": "0.9.3"
}
}
-1
View File
@@ -25,7 +25,6 @@
// begin (proven by the layout audit sweep).
.min_width = 1144,
.min_height = 720,
.restore_state = false,
.restore_policy = "center_on_primary",
.titlebar = "hidden_inset_tall",
.views = .{
-2
View File
@@ -85,7 +85,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
// sweep in tests.zig, which sweeps from exactly this floor.
.min_width = window_min_width,
.min_height = window_min_height,
.restore_state = false,
// Tall hidden-inset titlebar (declared in app.zon too, which threads
// it through the STARTUP window create): the header bar IS the
// titlebar — it pads its leading edge past the traffic lights via
@@ -196,7 +195,6 @@ pub fn main(init: std.process.Init) !void {
.window_title = "System Monitor",
.bundle_id = "dev.native_sdk.system_monitor",
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
.restore_state = false,
.js_window_api = false,
.security = .{
.permissions = &app_permissions,
-1
View File
@@ -16,7 +16,6 @@
.height = 640,
.min_width = 640,
.min_height = 420,
.restore_state = false,
.restore_policy = "center_on_primary",
.titlebar = "hidden_inset",
.views = .{
-2
View File
@@ -100,7 +100,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
.height = window_height,
.min_width = window_min_width,
.min_height = window_min_height,
.restore_state = false,
.titlebar = .hidden_inset_tall,
.views = &shell_views,
}};
@@ -1019,7 +1018,6 @@ pub fn main(init: std.process.Init) !void {
.window_title = "Terminal",
.bundle_id = "dev.native_sdk.terminal",
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
.restore_state = false,
.js_window_api = false,
.security = .{
.permissions = &app_permissions,
-1
View File
@@ -18,7 +18,6 @@
// (toolbar + filter chips + task rows).
.min_width = 520,
.min_height = 400,
.restore_state = false,
.restore_policy = "center_on_primary",
.titlebar = "hidden_inset_tall",
.views = .{
-2
View File
@@ -44,7 +44,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
.height = window_height,
.min_width = window_min_width,
.min_height = window_min_height,
.restore_state = false,
// Tall hidden-inset titlebar (declared in app.zon too, which threads
// it through the STARTUP window create): the header row IS the
// titlebar — it pads its leading edge past the traffic lights via
@@ -283,7 +282,6 @@ pub fn main(init: std.process.Init) !void {
.window_title = "Native SDK Inbox",
.bundle_id = "dev.native_sdk.ui_inbox",
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
.restore_state = false,
.js_window_api = false,
.security = .{
.permissions = &app_permissions,
-1
View File
@@ -12,7 +12,6 @@
.title = "Video Player",
.width = 760,
.height = 560,
.restore_state = false,
.restore_policy = "center_on_primary",
.views = .{
.{ .label = "player-canvas", .kind = "gpu_surface", .fill = true, .role = "Video player canvas", .accessibility_label = "Video player", .gpu_backend = "metal", .gpu_pixel_format = "bgra8_unorm", .gpu_present_mode = "timer", .gpu_alpha_mode = "opaque", .gpu_color_space = "srgb", .gpu_vsync = true },
-2
View File
@@ -31,7 +31,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
.title = "Video Player",
.width = window_width,
.height = window_height,
.restore_state = false,
.views = &shell_views,
}};
const shell_scene: native_sdk.ShellConfig = .{ .windows = &shell_windows };
@@ -361,7 +360,6 @@ pub fn main(init: std.process.Init) !void {
.window_title = "Video Player",
.bundle_id = "dev.native_sdk.video_player",
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
.restore_state = false,
.js_window_api = false,
.security = .{
.navigation = .{ .allowed_origins = &.{ "zero://inline", "zero://app" } },
-1
View File
@@ -19,7 +19,6 @@
.height = 760,
.min_width = 640,
.min_height = 620,
.restore_state = false,
.restore_policy = "center_on_primary",
.views = .{
.{ .label = "memo-canvas", .kind = "gpu_surface", .fill = true, .role = "Voice memo canvas", .accessibility_label = "Voice Memo recorder", .gpu_backend = "software", .gpu_pixel_format = "bgra8_unorm", .gpu_present_mode = "timer", .gpu_alpha_mode = "opaque", .gpu_color_space = "srgb", .gpu_vsync = true },
+1 -1
View File
@@ -3,6 +3,6 @@
"private": true,
"description": "Editor and versioning surface only: stock TypeScript tooling resolves @native-sdk/core from here. The native CLI never reads it and builds with node_modules absent.",
"dependencies": {
"@native-sdk/core": "0.9.1"
"@native-sdk/core": "0.9.3"
}
}
+7 -1
View File
@@ -129,6 +129,7 @@ fn manifestWindow(comptime window: anytype, comptime index: usize) native_sdk.Wi
.resizable = windowBool(window, "resizable", true),
.restore_state = windowBool(window, "restore_state", true),
.restore_policy = windowRestorePolicy(window),
.initial_placement = if (@hasField(@TypeOf(window), "x") or @hasField(@TypeOf(window), "y")) .explicit else .default,
};
}
@@ -391,12 +392,17 @@ fn prepareStateStore(io: std.Io, env_map: *std.process.Environ.Map, app_info: *n
if (!window.restore_state) continue;
if (store.loadWindow(window.label, &buffers.read) catch null) |saved| {
window.default_frame = saved.frame;
if (index == 0) app_info.main_window.default_frame = saved.frame;
window.initial_placement = .restored;
if (index == 0) {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
}
} else if (app_info.main_window.restore_state) {
if (store.loadWindow(app_info.main_window.label, &buffers.read) catch null) |saved| {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
return store;
+7 -1
View File
@@ -129,6 +129,7 @@ fn manifestWindow(comptime window: anytype, comptime index: usize) native_sdk.Wi
.resizable = windowBool(window, "resizable", true),
.restore_state = windowBool(window, "restore_state", true),
.restore_policy = windowRestorePolicy(window),
.initial_placement = if (@hasField(@TypeOf(window), "x") or @hasField(@TypeOf(window), "y")) .explicit else .default,
};
}
@@ -391,12 +392,17 @@ fn prepareStateStore(io: std.Io, env_map: *std.process.Environ.Map, app_info: *n
if (!window.restore_state) continue;
if (store.loadWindow(window.label, &buffers.read) catch null) |saved| {
window.default_frame = saved.frame;
if (index == 0) app_info.main_window.default_frame = saved.frame;
window.initial_placement = .restored;
if (index == 0) {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
}
} else if (app_info.main_window.restore_state) {
if (store.loadWindow(app_info.main_window.label, &buffers.read) catch null) |saved| {
app_info.main_window.default_frame = saved.frame;
app_info.main_window.initial_placement = .restored;
}
}
return store;
-1
View File
@@ -16,7 +16,6 @@
.height = 800,
.min_width = 900,
.min_height = 560,
.restore_state = false,
.restore_policy = "center_on_primary",
.titlebar = "hidden_inset",
.views = .{
-2
View File
@@ -257,7 +257,6 @@ const shell_windows = [_]native_sdk.ShellWindow{.{
.height = window_height,
.min_width = window_min_width,
.min_height = window_min_height,
.restore_state = false,
// The Ghostty-clean chrome: hidden-inset titlebar, no status bar —
// the traffic lights float over the terminal pane's own band.
.titlebar = .hidden_inset,
@@ -294,7 +293,6 @@ pub fn main(init: std.process.Init) !void {
.window_title = "Workbench",
.bundle_id = "dev.native_sdk.workbench",
.default_frame = geometry.RectF.init(0, 0, window_width, window_height),
.restore_state = false,
.js_window_api = false,
.security = .{
.permissions = &app_permissions,
+1
View File
@@ -96,6 +96,7 @@ export function windowDescriptor(spec: WindowDescriptorSpec): WindowDescriptor {
x: spec.x ?? null,
y: spec.y ?? null,
resizable: spec.resizable ?? true,
restorePolicy: spec.restorePolicy ?? "clamp_to_visible_screen",
minWidth: spec.minWidth ?? 0,
minHeight: spec.minHeight ?? 0,
titlebar: spec.titlebar ?? "standard",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@native-sdk/core",
"version": "0.9.1",
"version": "0.9.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@native-sdk/core",
"version": "0.9.1",
"version": "0.9.3",
"dependencies": {
"scriptc": "0.0.31"
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@native-sdk/core",
"version": "0.9.1",
"version": "0.9.3",
"description": "The TypeScript authoring tier: the app-core subset, its checker and contract frontend, the exact-pinned core compiler dependency, and the SDK module cores import",
"repository": {
"type": "git",
+1
View File
@@ -388,6 +388,7 @@ export function windowDescriptor(spec: WindowDescriptorSpec): WindowDescriptor {
x: spec.x ?? null,
y: spec.y ?? null,
resizable: spec.resizable ?? true,
restorePolicy: spec.restorePolicy ?? "clamp_to_visible_screen",
minWidth: spec.minWidth ?? 0,
minHeight: spec.minHeight ?? 0,
titlebar: spec.titlebar ?? "standard",
+10
View File
@@ -1,4 +1,11 @@
export type { TextCaretDirection, TextCaretMove, TextSelection, TextInputEvent } from "./text.js";
export type ThemeStatePack = "house" | "geist";
export type ThemeStateColorScheme = "light" | "dark" | "system";
export type ThemeState = {
readonly pack?: ThemeStatePack;
readonly colorScheme?: ThemeStateColorScheme;
readonly accent?: string;
};
export type StatusItemTone = "normal" | "warning" | "critical";
export type StatusItemMenuRole = "command" | "info" | "header" | "hero" | "agent" | "context";
export interface StatusItemModifiers {
@@ -48,6 +55,7 @@ export interface StatusItemDescriptor {
}
export type WindowClosePolicy = "quit" | "hide";
export type WindowTitlebarStyle = "standard" | "hidden_inset" | "hidden_inset_tall" | "chromeless";
export type WindowRestorePolicy = "clamp_to_visible_screen" | "center_on_primary";
export interface WindowDescriptorSpec {
readonly label: Uint8Array;
readonly canvasLabel: Uint8Array;
@@ -57,6 +65,7 @@ export interface WindowDescriptorSpec {
readonly x?: number | null;
readonly y?: number | null;
readonly resizable?: boolean;
readonly restorePolicy?: WindowRestorePolicy;
readonly minWidth?: number;
readonly minHeight?: number;
readonly titlebar?: WindowTitlebarStyle;
@@ -77,6 +86,7 @@ export interface WindowDescriptor {
readonly x: number | null;
readonly y: number | null;
readonly resizable: boolean;
readonly restorePolicy: WindowRestorePolicy;
readonly minWidth: number;
readonly minHeight: number;
readonly titlebar: WindowTitlebarStyle;
+30 -5
View File
@@ -37,6 +37,8 @@
// - `AppearanceEvent`, `ChromeEvent`, `AudioEvent`: the full arm
// payload shapes, canonical and importable for helper signatures
// (an arm value is structurally assignable to its event record).
// - `ThemeState`: the model-derived built-in theme axes returned by
// `themeState(model)`; omitted fields inherit the manifest/system.
// - `StatusItemState`, `StatusItemDescriptor`, and their nested records:
// the model-derived shell returned by `statusItem(model)` or keyed
// `statusItems(model)`; the generated launcher refreshes shell,
@@ -44,6 +46,20 @@
export type { TextCaretDirection, TextCaretMove, TextSelection, TextInputEvent } from "./text.ts";
/// The stock theme axes a TypeScript core may derive from committed model
/// state. Omit `pack`/`accent` to inherit app.zon; omit `colorScheme` (or
/// return `"system"`) to follow the OS. High contrast and reduced motion
/// remain system-owned, and high contrast suppresses accent overrides.
export type ThemeStatePack = "house" | "geist";
export type ThemeStateColorScheme = "light" | "dark" | "system";
export type ThemeState = {
readonly pack?: ThemeStatePack;
readonly colorScheme?: ThemeStateColorScheme;
readonly accent?: string;
};
/// One row in a menu-bar status item's menu. Non-separator rows need a
/// unique non-zero `id`, a non-empty `label`, and (when actionable) a
/// command name accepted by `commandMsg`. Rich readout roles may carry
@@ -119,6 +135,8 @@ export type WindowClosePolicy = "quit" | "hide";
export type WindowTitlebarStyle = "standard" | "hidden_inset" | "hidden_inset_tall" | "chromeless";
export type WindowRestorePolicy = "clamp_to_visible_screen" | "center_on_primary";
/// Author-facing input to `windowDescriptor`; omitted fields receive the
/// same defaults as UiApp.WindowDescriptor.
export interface WindowDescriptorSpec {
@@ -130,6 +148,7 @@ export interface WindowDescriptorSpec {
readonly x?: number | null;
readonly y?: number | null;
readonly resizable?: boolean;
readonly restorePolicy?: WindowRestorePolicy;
readonly minWidth?: number;
readonly minHeight?: number;
readonly titlebar?: WindowTitlebarStyle;
@@ -156,6 +175,7 @@ export interface WindowDescriptor {
readonly x: number | null;
readonly y: number | null;
readonly resizable: boolean;
readonly restorePolicy: WindowRestorePolicy;
readonly minWidth: number;
readonly minHeight: number;
readonly titlebar: WindowTitlebarStyle;
@@ -247,8 +267,10 @@ export interface PinchEvent {
readonly y: number;
}
/// A file drop's optional point in view-local canvas coordinates. Desktop
/// hosts that only know the target window leave `FileDropEvent.point` null.
/// A file drop's optional point in top-left-origin local coordinates. A
/// non-empty `FileDropEvent.viewLabel` names its canvas or WebView coordinate
/// space; an empty label may carry window-content coordinates. Hosts that
/// cannot resolve local coordinates leave `FileDropEvent.point` null.
export interface FileDropPoint {
readonly x: number;
readonly y: number;
@@ -256,9 +278,12 @@ export interface FileDropPoint {
/// The app-level file-drop channel's record (`dropMsg(drop)`). `windowId`
/// and `viewLabel` identify the source; `point` is present when the host can
/// resolve view-local coordinates. Paths are byte text so arbitrary UTF-8
/// filesystem names cross without becoming runtime JS strings. Return null
/// from `dropMsg` to ignore a drop, or map it to an ordinary Msg.
/// resolve local coordinates. The macOS system host reports labeled canvas or
/// WebView coordinates and degrades unlabeled regions to window-content
/// coordinates; hosts without either leave `point` null. Paths are byte text
/// so arbitrary UTF-8 filesystem names cross without becoming runtime JS
/// strings. Return null from `dropMsg` to ignore a drop, or map it to an
/// ordinary Msg.
export interface FileDropEvent {
readonly windowId: number;
readonly viewLabel: string;
+4
View File
@@ -22,6 +22,10 @@ export type TextInputEvent = {
readonly kind: "delete_word_backward";
} | {
readonly kind: "delete_word_forward";
} | {
readonly kind: "delete_to_start";
} | {
readonly kind: "delete_to_line_start";
} | {
readonly kind: "clear";
} | {
+51
View File
@@ -56,6 +56,8 @@ export type TextInputEvent =
| { readonly kind: "delete_forward" }
| { readonly kind: "delete_word_backward" }
| { readonly kind: "delete_word_forward" }
| { readonly kind: "delete_to_start" }
| { readonly kind: "delete_to_line_start" }
| { readonly kind: "clear" }
| { readonly kind: "move_caret"; readonly move: TextCaretMove }
| { readonly kind: "set_selection"; readonly selection: TextSelection }
@@ -417,6 +419,51 @@ function deleteWordForwardTextEdit(state: TextEditState, capacity: number): Text
);
}
function textLineStartOffset(text: Uint8Array, offset: number): number {
let cursor = snapTextOffset(text, offset);
while (cursor > 0 && text[cursor - 1] !== 0x0a) cursor -= 1;
return cursor;
}
function deleteToStartTextEdit(state: TextEditState, capacity: number): TextEditState | null {
const range = activeTextReplaceRange(state);
if (!rangeIsCollapsed(range, state.text.length)) {
return replaceTextEditRange(state, range, new Uint8Array(0), capacity, null, 0);
}
const caret = snapTextCaretOffset(state.text, state.selection.focus);
if (caret === 0) {
return { text: state.text, selection: caretSelectionAt(0, 0), composition: null };
}
return replaceTextEditRange(
state,
{ start: 0, end: caret },
new Uint8Array(0),
capacity,
null,
0,
);
}
function deleteToLineStartTextEdit(state: TextEditState, capacity: number): TextEditState | null {
const range = activeTextReplaceRange(state);
if (!rangeIsCollapsed(range, state.text.length)) {
return replaceTextEditRange(state, range, new Uint8Array(0), capacity, null, 0);
}
const caret = snapTextCaretOffset(state.text, state.selection.focus);
const lineStart = textLineStartOffset(state.text, caret);
if (lineStart === caret) {
return { text: state.text, selection: caretSelectionAt(caret, caret), composition: null };
}
return replaceTextEditRange(
state,
{ start: lineStart, end: caret },
new Uint8Array(0),
capacity,
null,
0,
);
}
function moveTextCaret(state: TextEditState, move: TextCaretMove): TextEditState {
const range = selectionRange(state.selection, state.text.length);
const focus = snapTextCaretOffset(state.text, state.selection.focus);
@@ -472,6 +519,10 @@ export function applyTextInputEvent(
return deleteWordBackwardTextEdit(normalized, capacity);
case "delete_word_forward":
return deleteWordForwardTextEdit(normalized, capacity);
case "delete_to_start":
return deleteToStartTextEdit(normalized, capacity);
case "delete_to_line_start":
return deleteToLineStartTextEdit(normalized, capacity);
case "clear":
return { text: new Uint8Array(0), selection: { anchor: 0, focus: 0 }, composition: null };
case "move_caret":
+61 -3
View File
@@ -507,6 +507,7 @@ export class SubsetChecker {
this.checkModelBindingSurface();
this.checkMigrationHook();
this.checkThemePackHelper();
this.checkThemeStateHelper();
this.checkStatusItemHelper();
this.checkStatusItemsHelper();
this.checkWindowsHelper();
@@ -843,6 +844,55 @@ export class SubsetChecker {
}
}
/// `themeState(model)` subsumes themePack with one exact, projection-safe
/// record. Optional properties are intentional: omission is the manifest /
/// system inheritance signal that crosses the helper ABI as null.
private checkThemeStateHelper(): void {
const decl = this.entryExportedFunction("themeState");
if (decl === null) return;
if (this.table.modelHelperDecls().some((candidate) => candidate.name === "themePack")) {
this.report("NS1033", "Export either `themePack` or `themeState`, not both.", decl.name ?? decl);
}
const helper = this.table.modelHelperDecls().find(
(candidate) => candidate.name === "themeState" && candidate.decl === decl,
);
if (helper === undefined || decl.type === undefined) {
this.report(
"NS1033",
"`themeState` is not a single-Model-parameter helper with an explicit return type.",
decl.name ?? decl,
);
return;
}
const returns = this.table.resolveTypeNode(decl.type);
const state = returns.k === "struct" ? this.table.structs.get(returns.name) : undefined;
const names = state?.fields.map((field) => field.tsName).sort() ?? [];
const field = (name: string) => state?.fields.find((candidate) => candidate.tsName === name);
const pack = field("pack");
const colorScheme = field("colorScheme");
const accent = field("accent");
const optionalEnumMembersAre = (candidate: typeof pack, expected: readonly string[]): boolean => {
if (candidate?.type.k !== "optional" || candidate.type.inner.k !== "enum") return false;
const found = this.table.enums.get(candidate.type.inner.name)?.members.slice().sort() ?? [];
return found.join(",") === expected.slice().sort().join(",");
};
const valid =
names.join(",") === "accent,colorScheme,pack" &&
optionalEnumMembersAre(pack, ["house", "geist"]) &&
optionalEnumMembersAre(colorScheme, ["light", "dark", "system"]) &&
accent?.type.k === "optional" && accent.type.inner.k === "string";
if (!valid) {
this.report(
"NS1033",
"`themeState` must return the exact canonical `ThemeState` record (`pack?`, `colorScheme?`, `accent?`); import it from `@native-sdk/core/events`.",
decl.type,
);
}
}
/// Persistence migration is a pure entry hook, not a model helper. It
/// receives the previous canonical snapshot and monotonic schema version;
/// returning the current Model succeeds, while throwing closes as
@@ -1417,12 +1467,13 @@ export class SubsetChecker {
return candidate?.type.k === "optional" && ["number", "i64", "f64", "numAlias"].includes(candidate.type.inner.k);
};
const valid =
names.join(",") === "activateOnShow,allowsFullscreen,alwaysOnTop,canvasLabel,clickThrough,closePolicy,height,label,minHeight,minWidth,onCloseCommand,resizable,title,titlebar,transparent,width,x,y" &&
names.join(",") === "activateOnShow,allowsFullscreen,alwaysOnTop,canvasLabel,clickThrough,closePolicy,height,label,minHeight,minWidth,onCloseCommand,resizable,restorePolicy,title,titlebar,transparent,width,x,y" &&
field("label")?.type.k === "bytes" &&
field("canvasLabel")?.type.k === "bytes" &&
field("title")?.type.k === "bytes" &&
numeric("width") && numeric("height") && optionalNumeric("x") && optionalNumeric("y") &&
field("resizable")?.type.k === "bool" && numeric("minWidth") && numeric("minHeight") &&
field("resizable")?.type.k === "bool" && enumMembersAre("restorePolicy", ["clamp_to_visible_screen", "center_on_primary"]) &&
numeric("minWidth") && numeric("minHeight") &&
enumMembersAre("titlebar", ["standard", "hidden_inset", "hidden_inset_tall", "chromeless"]) &&
field("transparent")?.type.k === "bool" && field("alwaysOnTop")?.type.k === "bool" &&
field("clickThrough")?.type.k === "bool" && field("activateOnShow")?.type.k === "bool" &&
@@ -1871,7 +1922,7 @@ export class SubsetChecker {
/// entry points, but the exports themselves live in the entry module.
private static readonly entryOnlyExports = new Set([
"update", "initialModel", "subscriptions", "migrate",
"commandMsg", "keyMsg", "frameMsg", "pinchMsg", "dropMsg", "appearanceMsg", "chromeMsg", "envMsgs", "themePack", "statusItem", "statusItems", "windows",
"commandMsg", "keyMsg", "frameMsg", "pinchMsg", "dropMsg", "appearanceMsg", "chromeMsg", "envMsgs", "themePack", "themeState", "statusItem", "statusItems", "windows",
"viewUnbound", "modelUnbound", "msgUnbound",
]);
@@ -2039,6 +2090,13 @@ export class SubsetChecker {
: this.table.unions.get(typeName)?.arms.flatMap((a) => [...a.fields]) ?? [];
for (const f of fields) {
const decl = f.decl;
if (ts.isPropertySignature(decl) && decl.questionToken) {
this.report(
"NS1012",
`Optional field \`${f.tsName}?\` in the ${typeName} tree has an implicit undefined state — spell the empty explicitly as \`${f.tsName}: T | null\`.`,
decl,
);
}
if (ts.isPropertySignature(decl) && decl.type && ts.isFunctionTypeNode(decl.type)) {
this.report("NS1003", `\`${f.tsName}\` stores a function in the ${typeName} tree.`, decl);
}
+5 -4
View File
@@ -457,10 +457,11 @@ class ContractEmitter {
}
}
}
// `statusItem` is consumed by the generated launcher, not markup. It
// is still an ordinary Model helper in the core ABI, so teach native
// check that this one helper is intentionally shell-bound without
// making every app repeat it in `viewUnbound`.
// These helpers are consumed by the generated launcher, not markup.
// They remain ordinary Model helpers in the core ABI, so teach native
// check that they are intentionally shell-bound without making every
// app repeat them in `viewUnbound`.
if (helperNames.includes("themeState") && !model.includes("themeState")) model.push("themeState");
if (helperNames.includes("statusItem") && !model.includes("statusItem")) model.push("statusItem");
if (helperNames.includes("statusItems") && !model.includes("statusItems")) model.push("statusItems");
if (helperNames.includes("windows") && !model.includes("windows")) model.push("windows");
+2 -2
View File
@@ -5,8 +5,8 @@ import fs from "node:fs";
// recordings.
// EffectFileOp appended `delete`; the reflected journal layout fingerprint
// moves so older recordings refuse cleanly instead of decoding op 8 wrongly.
export const journalFormatFingerprint = 0xb3bd2e83971de44dn;
export const automationProtocolFingerprint = 0x59d66f39803fd602n;
export const journalFormatFingerprint = 0xc510d4b0292ac71an;
export const automationProtocolFingerprint = 0x51f7889bbe3305e7n;
const requestKeyBase = 0x5453525100000000n;
const textEncoder = new TextEncoder();
+2 -2
View File
@@ -125,7 +125,7 @@ export const rules = {
id: "NS1014",
title: "the core's entry points live in core.ts",
fix: "Move this export into src/core.ts (imported modules may hold the helpers it calls and the types it uses).",
why: "The build wires `update`, `initialModel`, `subscriptions`, the host-event channels, `themePack`, and `viewUnbound` from the entry module only, so an entry export in an imported file would be silently ignored.",
why: "The build wires `update`, `initialModel`, `subscriptions`, the host-event channels, `themeState` / `themePack`, and `viewUnbound` from the entry module only, so an entry export in an imported file would be silently ignored.",
class: "guarantee",
},
NS1015: {
@@ -257,7 +257,7 @@ export const rules = {
NS1033: {
id: "NS1033",
title: "wiring exports match their runtime shapes",
fix: "Declare the channel exactly: `commandMsg(name: string)` / `keyMsg(key: KeyEvent)` / `frameMsg(model: Model, frame: FrameEvent)` / `pinchMsg(pinch: PinchEvent)` / `dropMsg(drop: FileDropEvent)` returning `Msg | null`; `themePack(model: Model): ThemePack`; singular `statusItem(model: Model): StatusItemState` or collection `statusItems(model: Model): readonly StatusItemDescriptor[]`; `windows(model: Model): readonly WindowDescriptor[]`, with each entry constructed by `windowDescriptor` and a literal `label: asciiBytes(\"name\")` matching `src/windows/name.native`; `appearanceMsg` / `chromeMsg` naming an arm with that channel's record shape; `envMsgs` entries targeting one-`Uint8Array`-field arms; and persistence ok/none routes naming void arms while err names a one-`Uint8Array`-field arm. Import canonical records from the SDK modules.",
fix: "Declare the channel exactly: `commandMsg(name: string)` / `keyMsg(key: KeyEvent)` / `frameMsg(model: Model, frame: FrameEvent)` / `pinchMsg(pinch: PinchEvent)` / `dropMsg(drop: FileDropEvent)` returning `Msg | null`; `themePack(model: Model): ThemePack` or `themeState(model: Model): ThemeState` (not both); singular `statusItem(model: Model): StatusItemState` or collection `statusItems(model: Model): readonly StatusItemDescriptor[]`; `windows(model: Model): readonly WindowDescriptor[]`, with each entry constructed by `windowDescriptor` and a literal `label: asciiBytes(\"name\")` matching `src/windows/name.native`; `appearanceMsg` / `chromeMsg` naming an arm with that channel's record shape; `envMsgs` entries targeting one-`Uint8Array`-field arms; and persistence ok/none routes naming void arms while err names a one-`Uint8Array`-field arm. Import canonical records from the SDK modules.",
why: "The generated wiring builds host events, persistence restore results, model-derived theme/status/window declarations, and their typed callbacks structurally at build time; a wrong shape would otherwise surface as a Zig compile error inside generated code instead of a teaching diagnostic here.",
class: "guarantee",
},
+10
View File
@@ -258,6 +258,16 @@ class ServiceShapeTable {
if (this.listed.has(name)) return;
const info = this.table.structs.get(name);
if (!info) throw new ServiceShapeError(`The shared type table has no record named \`${name}\``, this.fallbackSite);
const optionalField = info.fields.find((field) =>
(ts.isPropertySignature(field.decl) || ts.isPropertyDeclaration(field.decl)) &&
field.decl.questionToken !== undefined
);
if (optionalField) {
throw new ServiceShapeError(
`Service boundary record \`${name}\` has optional property \`${optionalField.tsName}?\`; spell absence explicitly as \`${optionalField.tsName}: T | null\` so both service codecs encode the same state`,
optionalField.decl,
);
}
this.listed.add(name);
const record: ServiceRecordType = {
name,
+9 -7
View File
@@ -340,18 +340,20 @@ export class TypedAst {
/// Properties of an object-literal type alias body, in declaration
/// order — null unless every member is a plain record property (an
/// identifier-named, annotated, non-optional property signature), so
/// identifier-named, annotated property signature), so
/// a shape this walk cannot carry whole refuses as an unsupported
/// alias instead of registering a struct with silently missing
/// fields.
propsOfTypeLiteral(node: tsImpl.TypeLiteralNode): PropInfo[] | null {
/// alias instead of registering a struct with silently missing fields.
/// The one omission-carrying value record is the SDK-owned ThemeState;
/// its caller opts in explicitly so authored/service records keep the
/// fixed-shape rule.
propsOfTypeLiteral(node: tsImpl.TypeLiteralNode, allowOptional = false): PropInfo[] | null {
const out: PropInfo[] = [];
for (const member of node.members) {
if (!tsImpl.isPropertySignature(member) || !member.name || !tsImpl.isIdentifier(member.name)) return null;
if (member.questionToken || !member.type) return null;
if (!tsImpl.isPropertySignature(member) || !member.name || !tsImpl.isIdentifier(member.name) || !member.type) return null;
if (member.questionToken && !allowOptional) return null;
out.push({
name: member.name.text,
optional: false,
optional: member.questionToken !== undefined,
readonly: hasReadonlyModifier(member),
typeNode: member.type,
declaration: member,

Some files were not shown because too many files have changed in this diff Show More