From c65f40992d13e477dd6e640793f21acc5a442c85 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 17 Aug 2026 09:58:35 -0500 Subject: [PATCH 1/6] feat(ts): support root markup component imports - Discover and embed root markup component sources for desktop and mobile builds. - Cover hot reload, automation, replay, path budgets, and empty source sets. - Move Kanban composition into a real component file and update authoring docs. --- build.zig | 38 +++++- build/app.zig | 122 +++++++++++++++++- .../src/app/docs/building-components/page.mdx | 61 ++------- docs/src/app/docs/native-ui/page.mdx | 12 +- docs/src/app/docs/typescript/page.mdx | 2 + docs/src/app/docs/windows/page.mdx | 2 +- examples/kanban/README.md | 2 +- examples/kanban/src/app.native | 77 ++--------- .../kanban/src/components/board-column.native | 26 ++++ skill-data/native-ui/SKILL.md | 2 +- skill-data/ts-core/SKILL.md | 2 +- src/app_runner/ts_core_main.zig | 3 +- src/app_runner/ts_core_mobile.zig | 3 +- src/runtime/ui_app_tests.zig | 27 ++-- tests/ts-core/components/actions.native | 10 ++ tests/ts-core/kanban_e2e_tests.zig | 35 ++++- tests/ts-core/markup_e2e_tests.zig | 12 +- tests/ts-core/markup_view.native | 10 +- 18 files changed, 282 insertions(+), 164 deletions(-) create mode 100644 examples/kanban/src/components/board-column.native create mode 100644 tests/ts-core/components/actions.native diff --git a/build.zig b/build.zig index fe06de11f..6b270b3db 100644 --- a/build.zig +++ b/build.zig @@ -97,6 +97,20 @@ test "service archive support matches ScriptC localized object formats" { try std.testing.expect(app_build.serviceArchiveSupported(windows_host, native_windows_msvc)); } +test "root TypeScript markup discovery classification and resolver budgets" { + const app_build = @import("build/app.zig"); + try std.testing.expect(app_build.isRootMarkupSourcePath("components/card.native")); + try std.testing.expect(app_build.isRootMarkupSourcePath("feature/nested/panel.native")); + try std.testing.expect(!app_build.isRootMarkupSourcePath("app.native")); + try std.testing.expect(!app_build.isRootMarkupSourcePath("windows/settings.native")); + try std.testing.expect(!app_build.isRootMarkupSourcePath("components/card.ts")); + + try std.testing.expect(app_build.markupSourcePathWithinBudget("a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a")); + try std.testing.expect(!app_build.markupSourcePathWithinBudget("a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a")); + try std.testing.expect(app_build.markupSourcePathWithinBudget("a" ** 200)); + try std.testing.expect(!app_build.markupSourcePathWithinBudget("a" ** 201)); +} + pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const host_target = b.graph.host; @@ -658,6 +672,9 @@ pub fn build(b: *std.Build) void { const ai_chat_e2e_run = b.addRunArtifact(ts_core_artifacts.ai_chat); const feed_reader_e2e_run = b.addRunArtifact(ts_core_artifacts.feed_reader); const services_e2e_run = b.addRunArtifact(ts_core_artifacts.services); + const markup_components_e2e_step = b.step("test-ts-markup-components-e2e", "Run root component-file compiled, interpreter, automation, and replay coverage"); + markup_components_e2e_step.dependOn(&markup_e2e_run.step); + markup_components_e2e_step.dependOn(&kanban_e2e_run.step); ts_services_e2e_step.dependOn(&feed_reader_e2e_run.step); ts_services_e2e_step.dependOn(&services_e2e_run.step); // The same fixture through the in-process carrier (ServicePool over @@ -2348,12 +2365,12 @@ pub fn build(b: *std.Build) void { \\provenance="$("$cli" automate provenance kanban-canvas "$button_id" 2>/dev/null)" \\case "$provenance" in *"authored=markup"*"root=src/app.native"*) ;; *) echo "writeback smoke: button provenance was not markup-authored: $provenance" >&2; exit 1 ;; esac \\case "$provenance" in *"node file=src/app.native"*) ;; *) echo "writeback smoke: button provenance named the wrong file: $provenance" >&2; exit 1 ;; esac - \\# 2. Loop provenance: the boot view is deliberately self-contained, - \\# so a card title reports its node in app.native plus its iteration key. + \\# 2. Imported-loop provenance: a card title reports its authored + \\# component file plus its iteration key. \\card_id="$(printf '%s\n' "$snapshot" | sed -n 's/.*widget @w1\/kanban-canvas#\([0-9][0-9]*\) role=text name="Retry failed agent runs".*/\1/p' | head -n 1)" \\case "$card_id" in ''|*[!0-9]*) echo "writeback smoke: card text id was missing from the snapshot" >&2; exit 1 ;; esac \\card_provenance="$("$cli" automate provenance kanban-canvas "$card_id" 2>/dev/null)" - \\case "$card_provenance" in *"node file=src/app.native"*) ;; *) echo "writeback smoke: card provenance named the wrong file: $card_provenance" >&2; exit 1 ;; esac + \\case "$card_provenance" in *"node file=src/components/board-column.native"*) ;; *) echo "writeback smoke: card provenance named the wrong file: $card_provenance" >&2; exit 1 ;; esac \\case "$card_provenance" in *"keys="*) ;; *) echo "writeback smoke: card provenance missed the iteration key: $card_provenance" >&2; exit 1 ;; esac \\# 3. Write-back: flip the Todo heading through the verb; the app's own \\# hot-reload watch picks the file change up and repaints. @@ -3308,9 +3325,17 @@ fn tsCoreE2eArtifact( e2e_mod.addImport("native_sdk", desktop_mod); e2e_mod.addImport("ts_core_fixture", fixture_mod); - // The markup battery: the .native view + automation + record/replay - // guarantees over the markup fixture's compiled core. - const markup_e2e_mod = module(b, target, optimize, "tests/ts-core/markup_e2e_tests.zig"); + // The markup battery: the imported .native view + automation + + // record/replay guarantees over the markup fixture's compiled core. + const markup_view_stage = b.addWriteFiles(); + const markup_view_root = markup_view_stage.addCopyFile(b.path("tests/ts-core/markup_e2e_tests.zig"), "markup_e2e_tests.zig"); + _ = markup_view_stage.addCopyFile(b.path("tests/ts-core/markup_view.native"), "markup_view.native"); + _ = markup_view_stage.addCopyFile(b.path("tests/ts-core/components/actions.native"), "components/actions.native"); + const markup_e2e_mod = b.createModule(.{ + .root_source_file = markup_view_root, + .target = target, + .optimize = optimize, + }); markup_e2e_mod.addImport("native_sdk", desktop_mod); markup_e2e_mod.addImport("ts_markup_fixture", markup_fixture_mod); @@ -3326,6 +3351,7 @@ fn tsCoreE2eArtifact( const kanban_stage = b.addWriteFiles(); const kanban_root = kanban_stage.addCopyFile(b.path("tests/ts-core/kanban_e2e_tests.zig"), "kanban_e2e_tests.zig"); _ = kanban_stage.addCopyFile(b.path("examples/kanban/src/app.native"), "app.native"); + _ = kanban_stage.addCopyFile(b.path("examples/kanban/src/components/board-column.native"), "components/board-column.native"); const kanban_mod = b.createModule(.{ .root_source_file = kanban_root, .target = target, diff --git a/build/app.zig b/build/app.zig index 03678f94c..396de2ed7 100644 --- a/build/app.zig +++ b/build/app.zig @@ -81,7 +81,7 @@ const TsWindowView = struct { staged_path: []const u8, }; -const TsWindowSource = struct { +const TsMarkupSource = struct { set_path: []const u8, source_path: []const u8, staged_path: []const u8, @@ -89,9 +89,45 @@ const TsWindowSource = struct { const TsWindowViews = struct { views: []const TsWindowView, - sources: []const TsWindowSource, + sources: []const TsMarkupSource, }; +const TsAppMarkupSources = struct { + /// Non-root, non-window files discovered under src/. Window files are + /// staged by the existing secondary-window registry and must not be + /// copied a second time. + files: []const TsMarkupSource, + /// The root view's embedded resolver set. Its path keys are relative to + /// src/, matching the root disk watcher; existing window sources are + /// projected under their staged `windows/...` names. + sources: []const TsMarkupSource, +}; + +const max_markup_source_path_len = 200; +const max_markup_source_path_segments = 24; + +pub fn markupSourcePathWithinBudget(path: []const u8) bool { + var segments: usize = 0; + var it = std.mem.tokenizeScalar(u8, path, '/'); + while (it.next() != null) segments += 1; + return path.len <= max_markup_source_path_len and segments <= max_markup_source_path_segments; +} + +fn validateMarkupSourcePath(path: []const u8) void { + if (!markupSourcePathWithinBudget(path)) { + std.debug.panic( + "\nTypeScript markup source path `{s}` exceeds the import resolver budget: keep every src/-relative .native path at most {d} bytes and {d} segments\n", + .{ path, max_markup_source_path_len, max_markup_source_path_segments }, + ); + } +} + +pub fn isRootMarkupSourcePath(path: []const u8) bool { + return std.mem.endsWith(u8, path, ".native") and + !std.mem.eql(u8, path, "app.native") and + !std.mem.startsWith(u8, path, "windows/"); +} + /// Default TypeScript secondary-window views are statically discovered under /// `src/windows/`: `settings.native` serves descriptor label `settings`. /// Direct files form the generated launcher's closed, comptime-compiled view @@ -104,7 +140,7 @@ fn collectTsWindowViews(b: *std.Build, app_root: []const u8) TsWindowViews { var walker = dir.walk(b.allocator) catch return .{ .views = &.{}, .sources = &.{} }; defer walker.deinit(); var views: std.ArrayList(TsWindowView) = .empty; - var sources: std.ArrayList(TsWindowSource) = .empty; + var sources: std.ArrayList(TsMarkupSource) = .empty; while (walker.next(b.graph.io) catch null) |entry| { if (entry.kind != .file or !std.mem.endsWith(u8, entry.path, ".native")) continue; const normalized_path = b.dupe(entry.path); @@ -141,14 +177,85 @@ fn collectTsWindowViews(b: *std.Build, app_root: []const u8) TsWindowViews { }.than; std.mem.sort(TsWindowView, views.items, {}, less); const source_less = struct { - fn than(_: void, a: TsWindowSource, z: TsWindowSource) bool { + fn than(_: void, a: TsMarkupSource, z: TsMarkupSource) bool { return std.mem.order(u8, a.set_path, z.set_path) == .lt; } }.than; - std.mem.sort(TsWindowSource, sources.items, {}, source_less); + std.mem.sort(TsMarkupSource, sources.items, {}, source_less); return .{ .views = views.items, .sources = sources.items }; } +/// The root TypeScript view resolves imports from every `.native` file under +/// `src/` except itself. Window files already come from collectTsWindowViews; +/// append those under `windows/...` keys so this set matches the root disk +/// resolver without changing the separate window resolver root. +fn collectAppMarkupSources(b: *std.Build, app_root: []const u8, window_views: TsWindowViews) TsAppMarkupSources { + const src_path = appPath(b, app_root, "src"); + var dir = b.build_root.handle.openDir(b.graph.io, src_path, .{ .iterate = true }) catch + return .{ .files = &.{}, .sources = &.{} }; + defer dir.close(b.graph.io); + var walker = dir.walk(b.allocator) catch return .{ .files = &.{}, .sources = &.{} }; + defer walker.deinit(); + + var files: std.ArrayList(TsMarkupSource) = .empty; + while (walker.next(b.graph.io) catch null) |entry| { + if (entry.kind != .file or !std.mem.endsWith(u8, entry.path, ".native")) continue; + const normalized_path = b.dupe(entry.path); + for (normalized_path) |*char| { + if (char.* == '\\') char.* = '/'; + } + if (!isRootMarkupSourcePath(normalized_path)) continue; + validateMarkupSourcePath(normalized_path); + files.append(b.allocator, .{ + .set_path = normalized_path, + .source_path = b.fmt("src/{s}", .{normalized_path}), + .staged_path = normalized_path, + }) catch @panic("OOM"); + } + + const less = struct { + fn than(_: void, a: TsMarkupSource, z: TsMarkupSource) bool { + return std.mem.order(u8, a.set_path, z.set_path) == .lt; + } + }.than; + std.mem.sort(TsMarkupSource, files.items, {}, less); + + var sources: std.ArrayList(TsMarkupSource) = .empty; + sources.ensureTotalCapacity(b.allocator, files.items.len + window_views.sources.len) catch @panic("OOM"); + sources.appendSliceAssumeCapacity(files.items); + for (window_views.sources) |source| { + validateMarkupSourcePath(source.staged_path); + sources.appendAssumeCapacity(.{ + .set_path = source.staged_path, + .source_path = source.source_path, + .staged_path = source.staged_path, + }); + } + std.mem.sort(TsMarkupSource, sources.items, {}, less); + return .{ .files = files.items, .sources = sources.items }; +} + +fn tsAppMarkupSourcesSource(b: *std.Build, registry: TsAppMarkupSources) []const u8 { + var out: std.ArrayList(u8) = .empty; + out.appendSlice(b.allocator, + \\//! Generated by build/app.zig from src/**/*.native. + \\const native_sdk = @import("native_sdk"); + \\const canvas = native_sdk.canvas; + \\pub const sources = [_]canvas.ui_markup.SourceFile{ + \\ + ) catch @panic("OOM"); + for (registry.sources) |source| { + const line = std.fmt.allocPrint( + b.allocator, + " .{{ .path = \"{f}\", .source = @embedFile(\"{f}\") }},\n", + .{ std.zig.fmtString(source.set_path), std.zig.fmtString(source.staged_path) }, + ) catch @panic("OOM"); + out.appendSlice(b.allocator, line) catch @panic("OOM"); + } + out.appendSlice(b.allocator, "};\n") catch @panic("OOM"); + return out.items; +} + fn tsWindowRegistrySource(b: *std.Build, registry: TsWindowViews) []const u8 { const views = registry.views; var out: std.ArrayList(u8) = .empty; @@ -757,6 +864,7 @@ fn tsCoreStage( ) TsCoreStage { const node = tsCorePreflight(b, dep, app_root); const window_views = collectTsWindowViews(b, app_root); + const app_markup_sources = collectAppMarkupSources(b, app_root, window_views); const has_services = appHasServiceFiles(b, app_root); if (!scriptcCompileSupported(b.graph.host.result, target)) { panicUnsupportedScriptcTarget(b, b.graph.host.result, target); @@ -1052,9 +1160,13 @@ fn tsCoreStage( , .{ service_carrier, service_pool_workers })); _ = staged.addCopyFile(migrations_zig, "migrations.zig"); _ = staged.addCopyFile(b.path(appPath(b, app_root, "src/app.native")), "app.native"); + for (app_markup_sources.files) |source| { + _ = staged.addCopyFile(b.path(appPath(b, app_root, source.source_path)), source.staged_path); + } for (window_views.sources) |source| { _ = staged.addCopyFile(b.path(appPath(b, app_root, source.source_path)), source.staged_path); } + _ = staged.add("app_sources.zig", tsAppMarkupSourcesSource(b, app_markup_sources)); _ = staged.add("window_views.zig", tsWindowRegistrySource(b, window_views)); const main_root = staged.addCopyFile(dep.path("src/app_runner/ts_core_main.zig"), "main.zig"); // The mobile wiring stages beside the desktop entry: same mirror, same diff --git a/docs/src/app/docs/building-components/page.mdx b/docs/src/app/docs/building-components/page.mdx index e6369b7ba..0e4425840 100644 --- a/docs/src/app/docs/building-components/page.mdx +++ b/docs/src/app/docs/building-components/page.mdx @@ -1,6 +1,6 @@ # Building Components -The library's built-ins cover the common register, and [theming](/docs/theming) restyles all of them at once. This page is about the pieces the library does not hand you: how to build a component of your own — first as a markup template, then as a Zig view function when the shape needs one — how it themes, and how component files spread across an app. Component code is toolkit-extension territory, so the Zig here applies whatever language the app core is written in: a TypeScript app that needs one custom widget writes that widget in Zig and keeps its core in TypeScript. The mechanics (template grammar, import rules, slots) are specified in [Native UI](/docs/native-ui#templates); this page builds one real component end to end. +The library's built-ins cover the common register, and [theming](/docs/theming) restyles all of them at once. App components are Native markup templates: they compose built-ins, accept values through template args, accept content through slots, and live in `.native` component files. Component logic stays in the TypeScript core's ordinary `update` and derived helpers. The mechanics (template grammar, import rules, slots) are specified in [Native UI](/docs/native-ui#templates); this page builds one real component end to end. The ownership model in one line: **use and theme the built-ins by default; eject a library composite when you need to own its shape; build new composites from primitives when the library has no shape for it.** The last two are this page. @@ -91,37 +91,11 @@ Args carry values; a `` carries markup. A template body may mark one inse This is the container-component pattern: the template owns the frame, the caller owns the content. The full rules (one slot per body, children without a slot are an error, ids hash as if inlined) are in [Native UI § Components](/docs/native-ui#components). -## When a component needs Zig - -The markup grammar is deliberately closed, and a few shapes sit outside it — the honest list is in [Native UI § Elements](/docs/native-ui#elements): components that carry **image ids** (pixels registered at runtime; the avatar's `image="{binding}"` is the one declarative exception), **per-cell templates** (a data grid's arbitrary render-per-column callbacks), and **Zig-side floating surfaces** (`popover`, `menu_surface`; the anchored `dropdown-menu` covers the declarative case). Beyond those, anything needing per-state styling past tokens (`ElementOptions.style`) or logic past the expression language belongs in Zig. - -A Zig component is just a function that takes the view builder and returns a node — the same primitives markup lowers to, with the same structural identity rules: - -```zig -const canvas = native_sdk.canvas; -const Ui = canvas.Ui(Msg); - -/// A contact row with an avatar image. Image pixels are registered at -/// runtime and referenced by ImageId — a runtime value markup attributes -/// cannot carry — so this component is a Zig view function. -fn contactRow(ui: *Ui, name: []const u8, initials: []const u8, image: canvas.ImageId) Ui.Node { - return ui.el(.row, .{ - .gap = 10, - .padding = 8, - .cross = .center, - .style_tokens = .{ .background = .surface, .radius = .md }, - .semantics = .{ .role = .listitem, .label = name }, - }, .{ - // A zero id keeps the initials fallback — write the id into the - // model only on successful registration, and loading states cost - // no extra branch here. - ui.avatar(.{ .image = image, .size = .sm }, initials), - ui.text(.{ .grow = 1 }, name), - }); -} -``` +## Put component logic in the core + +Templates own structure, not state. Keep state transitions in `update`, expose reusable derived values as exported TypeScript helpers, and bind those values into the component. A component never runs an app callback while the view builds. -Call it from any Zig view (`contactRow(ui, contact.name, contact.initials, contact.avatar_image)`), key it inside `ui.each` loops like any node, and compose it with everything else the builder makes. Markup views and Zig views are not either/or per app — a markup root can be paired with Zig-built windows, and a Zig root can embed compiled markup fragments — but one component is one form: pick markup when the grammar covers it (hot reload and `native check` come free), Zig when it does not. +When the closed markup vocabulary does not expose a lower-level engine capability yet, there is no app-side escape element or required Zig file. Use the nearest built-in composition and track the missing grammar admission. Toolkit extensions remain an SDK implementation concern. ## Theming your component @@ -129,31 +103,18 @@ Your component themes the same way the built-ins do: through the token system, n In markup, the style attributes (`background`, `foreground`, `border-color`, `radius`, ...) are token **references** — the stat card above says `background="surface"`, not a hex value. References resolve against the app's live tokens on every rebuild, so the card follows dark mode, a theme-pack switch, and every override with zero component code. Unknown token names are check/compile errors. -In a Zig view, `ElementOptions.style_tokens` is the same channel — `.style_tokens = .{ .background = .surface, .radius = .md }` records the reference, and the app loop resolves it against the current `DesignTokens` when the tree finalizes (`finalizeWithTokens`), re-resolving on every retheme. Explicit values through `ElementOptions.style` always win over a token reference; use them only for the values that are genuinely not design tokens (a user-picked highlight color, a data-derived fill). - -State washes follow one rule: hover feedback belongs to acting controls. List rows, menu items, buttons, and tab triggers wash on hover because the fill is the affordance — it names the thing you are about to act on. An image-forward content tile is the opposite case — a cover-art grid, a photo card — where the pointer rests on content, not a control register, and a wash over the artwork reads as a smudge. Those surfaces go quiet with the quiet-surface knob, `.style = .{ .quiet_hover = true }`, which silences only the hover fill: the pressed wash still marks the moment of commitment, and the focus ring, cursor intent, and hit testing keep their own channels. Like everything per-state beyond tokens, it is a Zig-side style decision; markup stays in the token vocabulary. +State washes follow the built-in controls' behavior. Markup components customize their token references and compose the appropriate control kinds; app components do not add a second per-state styling channel. -And the parts of your component that are built-in controls stay themed for free: the ` - + Todo - - - - - {c.title} - - NAT-{c.ticketNumber} - - - OA - - - CL - - - - - - + - - + + Doing - - - - - {c.title} - - NAT-{c.ticketNumber} - - - OA - - - CL - - - - - - + - - + + Done - - - - - {c.title} - - NAT-{c.ticketNumber} - - - OA - - - CL - - - - - - + - + diff --git a/examples/kanban/src/components/board-column.native b/examples/kanban/src/components/board-column.native new file mode 100644 index 000000000..c7aad2331 --- /dev/null +++ b/examples/kanban/src/components/board-column.native @@ -0,0 +1,26 @@ + + diff --git a/skill-data/native-ui/SKILL.md b/skill-data/native-ui/SKILL.md index 4995de293..a40c24784 100644 --- a/skill-data/native-ui/SKILL.md +++ b/skill-data/native-ui/SKILL.md @@ -1058,7 +1058,7 @@ Rules and semantics: - SLOTS: a template body may contain one `` (attribute-less, childless; named slots do not exist). The `` site's children build IN THE CONSUMER'S SCOPE — they see the model paths and loop variables where the use is written — and land at the slot's position; ids hash as if inlined. A use with no children renders the slot empty; children on a slotless template are an error; a `` inside use-site children (forwarding) is an error. - IMPORTS: `` lines go at the very top of a file, before its templates. Paths are relative to the importing file (subdirectories and transitive imports fine, always under the root view file's directory — absolute paths and escapes are errors). An imported file defines templates ONLY (a component file; a view root inside one is an error, and a component file checks standalone). Importing splices the file's templates (transitively) BEFORE yours, in import order — as if pasted at the import site — so define-before-use stays the only ordering rule. Cycles are reported with the cycle path; duplicate template names are an error naming both definition sites. -Both engines implement templates, defaults, slots, and imports: the interpreter expands at build time (hot reload re-resolves imports from disk, so edits to imported files reload), and the compiled engine inlines at comptime with the identical result. A document with imports compiles through `canvas.CompiledMarkupImports(Model, Msg, "root.native", &sources)` where `sources` is a `canvas.ui_markup.SourceFile` set (`.{ .path = "components/cards.native", .source = @embedFile("components/cards.native") }`, paths relative to the root file's directory); pass the same set on `MarkupOptions.sources` for the runtime engine. See `examples/kanban/src/board.native` + `examples/kanban/src/components/board-column.native`. +Both engines implement templates, defaults, slots, and imports: the interpreter expands at build time (hot reload re-resolves imports from disk, so edits to imported files reload), and the compiled engine inlines at comptime with the identical result. A default TypeScript app's generated launcher discovers every component `.native` file under `src/`, embeds the same source set for both engines, and wires it on desktop and mobile. The main view can import anywhere under `src/`; secondary-window roots remain under `src/windows/`. Lower-level wiring compiles an imported document through `canvas.CompiledMarkupImports(Model, Msg, "root.native", &sources)` and passes the same `canvas.ui_markup.SourceFile` set on `MarkupOptions.sources`. See `examples/kanban/src/app.native` + `examples/kanban/src/components/board-column.native`. ## Code in markup: `` diff --git a/skill-data/ts-core/SKILL.md b/skill-data/ts-core/SKILL.md index 5add87121..680981f7a 100644 --- a/skill-data/ts-core/SKILL.md +++ b/skill-data/ts-core/SKILL.md @@ -217,7 +217,7 @@ Commands are constructed inline in the return path and nowhere else (NS1017): ne Export `windows(model): readonly WindowDescriptor[]` to derive the secondary windows that should exist from committed model state. Import `WindowDescriptor` from `@native-sdk/core/events`, and construct entries with `windowDescriptor` from `@native-sdk/core` so omitted fields receive the canonical defaults. Presence is liveness: adding a descriptor creates the window, removing it closes the window and releases its retained view. -Each possible label has a statically compiled Native markup view at `src/windows/