From 806233ee012142a7c55a38614b1233bb4ae2d14e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 5 Oct 2026 12:50:00 -0500 Subject: [PATCH 1/3] Add compiled typography and model theme overrides - Expose complete typed token overrides and model theme coordination. - Compile rich paragraphs and register bundled fonts before layout. - Add Typography with ownership, startup and replay parity coverage. --- apps/schema/public/app/v1.json | 13 + build.zig | 40 +++ examples/README.md | 1 + examples/typography/README.md | 5 + examples/typography/app.json | 51 ++++ .../typography/assets/GeistMono-Regular.ttf | Bin 0 -> 116248 bytes examples/typography/assets/OFL.txt | 93 ++++++ examples/typography/src/app.native | 9 + examples/typography/src/core.ts | 16 ++ examples/typography/tests/typography.test.ts | 55 ++++ packages/core/package.json | 4 + packages/core/scripts/gen_declarations.mjs | 2 +- packages/core/scripts/stage_external_core.mjs | 3 +- packages/core/sdk/theme.d.ts | 251 ++++++++++++++++ packages/core/sdk/theme.ts | 267 ++++++++++++++++++ packages/core/src/checker.ts | 18 +- packages/core/src/infer.ts | 12 +- packages/core/src/runtime_policy.ts | 4 +- packages/core/src/typed_ast.ts | 1 + packages/core/src/types.ts | 2 + packages/core/src/view_components.ts | 10 +- packages/core/src/view_frontend.ts | 68 ++++- packages/core/test/checker.test.ts | 34 +++ packages/core/test/runtime_policy.test.ts | 4 +- packages/core/test/view_frontend.test.ts | 41 +++ skill-data/native-ui/SKILL.md | 12 + skill-data/ts-core/SKILL.md | 2 + src/app_runner/ts_compiled_view.zig | 95 ++++++- src/app_runner/ts_core_main.zig | 59 +++- src/root.zig | 2 + src/runtime/ts_ui_app.zig | 52 ++++ src/runtime/ui_app.zig | 9 +- src/tooling/manifest.zig | 35 +++ src/tooling/raw_manifest.zig | 6 + tests/ts-core/surface_layout_e2e_tests.zig | 4 + tests/ts-core/typography_e2e_tests.zig | 113 ++++++++ 36 files changed, 1364 insertions(+), 29 deletions(-) create mode 100644 examples/typography/README.md create mode 100644 examples/typography/app.json create mode 100644 examples/typography/assets/GeistMono-Regular.ttf create mode 100644 examples/typography/assets/OFL.txt create mode 100644 examples/typography/src/app.native create mode 100644 examples/typography/src/core.ts create mode 100644 examples/typography/tests/typography.test.ts create mode 100644 packages/core/sdk/theme.d.ts create mode 100644 packages/core/sdk/theme.ts create mode 100644 tests/ts-core/typography_e2e_tests.zig diff --git a/apps/schema/public/app/v1.json b/apps/schema/public/app/v1.json index 9e74cf6e8..128835ba4 100644 --- a/apps/schema/public/app/v1.json +++ b/apps/schema/public/app/v1.json @@ -213,6 +213,19 @@ "type": "object", "additionalProperties": false, "properties": { + "fonts": { + "type": "array", + "maxItems": 8, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "path"], + "properties": { + "id": { "type": "integer", "minimum": 64, "maximum": 9007199254740991 }, + "path": { "type": "string", "pattern": "^assets/", "minLength": 8 } + } + } + }, "images": { "type": "array", "items": { diff --git a/build.zig b/build.zig index 870192d5e..9c9e6ce32 100644 --- a/build.zig +++ b/build.zig @@ -1143,6 +1143,26 @@ pub fn build(b: *std.Build) void { ts_core_e2e_step.dependOn(&status_driver_run.step); test_step.dependOn(&status_driver_run.step); + const typography_reference_run = managedExampleRun(b, host_cli_exe, &.{ "test", "-Dplatform=null" }); + typography_reference_run.setCwd(b.path("examples/typography")); + typography_reference_run.has_side_effects = true; + typography_reference_run.setEnvironmentVariable("NATIVE_SDK_TEST_VIEW_BACKEND", "zig"); + typography_reference_run.setEnvironmentVariable("NATIVE_SDK_TEST_VIEW_REFERENCE", b.pathFromRoot(".zig-cache/typography-view-reference")); + _ = typography_reference_run.captureStdOut(.{}); + _ = typography_reference_run.captureStdErr(.{}); + const typography_driver_run = managedExampleRun(b, host_cli_exe, &.{ "test", "-Dplatform=null", "-Dtypescript-view=true" }); + typography_driver_run.setCwd(b.path("examples/typography")); + typography_driver_run.has_side_effects = true; + typography_driver_run.setEnvironmentVariable("NATIVE_SDK_TEST_VIEW_BACKEND", "typescript"); + typography_driver_run.setEnvironmentVariable("NATIVE_SDK_TEST_VIEW_REFERENCE", b.pathFromRoot(".zig-cache/typography-view-reference")); + _ = typography_driver_run.captureStdOut(.{}); + _ = typography_driver_run.captureStdErr(.{}); + typography_driver_run.step.dependOn(&typography_reference_run.step); + b.step("test-ts-typography-driver", "Compare complete typography snapshots and replay across view backends").dependOn(&typography_driver_run.step); + native_driver_step.dependOn(&typography_driver_run.step); + ts_core_e2e_step.dependOn(&typography_driver_run.step); + test_step.dependOn(&typography_driver_run.step); + const relational_reference_run = managedExampleRun(b, host_cli_exe, &.{ "test", "-Dplatform=null" }); relational_reference_run.setCwd(b.path("examples/relational-notes")); relational_reference_run.has_side_effects = true; @@ -1290,6 +1310,10 @@ pub fn build(b: *std.Build) void { addTestStep(b, "test-ts-shell-layout-e2e", "Run compiled shell planning, ownership, and OS application parity", ts_core_artifacts.shell_layout); ts_core_e2e_step.dependOn(&shell_layout_e2e_run.step); test_step.dependOn(&shell_layout_e2e_run.step); + const typography_run = b.addRunArtifact(ts_core_artifacts.typography); + b.step("test-ts-typography-e2e", "Verify compiled token overrides and registered rich paragraphs").dependOn(&typography_run.step); + ts_core_e2e_step.dependOn(&typography_run.step); + test_step.dependOn(&typography_run.step); const surface_layout_run = b.addRunArtifact(ts_core_artifacts.surface_layout); b.step("test-ts-surface-layout-e2e", "Compare compiled floating-surface placement with native layout and ABI ownership").dependOn(&surface_layout_run.step); ts_core_e2e_step.dependOn(&surface_layout_run.step); @@ -2464,6 +2488,7 @@ pub fn build(b: *std.Build) void { addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-effect-policy", "Run named effect policy example tests", "examples/effect-policy", .managed), ui_inbox_example_step, addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-kanban", "Run ui builder kanban example tests", "examples/kanban", .managed), + addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-typography", "Run typography and bundled font startup tests", "examples/typography", .managed), addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-habits", "Run markup habits example tests", "examples/habits", .managed), addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-soundboard", "Run soundboard example tests", "examples/soundboard", .managed), addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-video-player", "Run video player example tests", "examples/video-player", .managed), @@ -3990,6 +4015,7 @@ const TsCoreE2eArtifacts = struct { feed_reader: *std.Build.Step.Compile, shell_layout: *std.Build.Step.Compile, surface_layout: *std.Build.Step.Compile, + typography: *std.Build.Step.Compile, /// The phase-1 service seam: a real compiled core plus a real plain-scriptc /// service executable driven through the out-of-process carrier. services: *std.Build.Step.Compile, @@ -4271,6 +4297,19 @@ fn tsCoreE2eArtifact( const shell_layout_mod = module(b, target, optimize, "tests/ts-core/shell_layout_e2e_tests.zig"); shell_layout_mod.addImport("native_sdk", desktop_mod); shell_layout_mod.addImport("shell_fixture_core", feed_reader_fixture.module); + const typography_fixture = externalCoreFixtureModule(b, target, optimize, node, corewire_exe, .{ + .entry = "examples/typography/src/core.ts", + .src_dir = b.path("examples/typography/src"), + .name = "typography_core", + .typescript_view = true, + }); + const typography_mod = module(b, target, optimize, "tests/ts-core/typography_e2e_tests.zig"); + typography_mod.addImport("native_sdk", desktop_mod); + typography_mod.addImport("typography_core", typography_fixture.module); + const typography_decoder_mod = module(b, target, optimize, "src/app_runner/ts_compiled_view.zig"); + typography_decoder_mod.addImport("native_sdk", desktop_mod); + typography_decoder_mod.addImport("core.zig", typography_fixture.module); + typography_mod.addImport("typography_decoder", typography_decoder_mod); const surface_layout_mod = module(b, target, optimize, "tests/ts-core/surface_layout_e2e_tests.zig"); surface_layout_mod.addImport("native_sdk", desktop_mod); surface_layout_mod.addImport("surface_fixture_core", feed_reader_fixture.module); @@ -4542,6 +4581,7 @@ fn tsCoreE2eArtifact( .ai_chat = filteredTestArtifact(b, ai_chat_mod, "ts-ai-chat-e2e-tests", &.{}), .feed_reader = filteredTestArtifact(b, feed_reader_mod, "ts-feed-reader-e2e-tests", &.{}), .shell_layout = filteredTestArtifact(b, shell_layout_mod, "ts-shell-layout-e2e-tests", &.{}), + .typography = filteredTestArtifact(b, typography_mod, "ts-typography-e2e-tests", &.{}), .surface_layout = filteredTestArtifact(b, surface_layout_mod, "ts-surface-layout-e2e-tests", &.{}), .services = filteredTestArtifact(b, services_e2e_mod, "ts-services-e2e-tests", &.{}), .services_pool = if (services_pool_mod) |pool_mod| filteredTestArtifact(b, pool_mod, "ts-services-pool-e2e-tests", &.{}) else null, diff --git a/examples/README.md b/examples/README.md index a6b1c8e08..068e42c79 100644 --- a/examples/README.md +++ b/examples/README.md @@ -24,6 +24,7 @@ TypeScript is the primary app-authoring language. A new `native init my_app` pro | `kanban` | Keyed boards, drag interactions, avatars, controlled scrolling, and file drops. | | `menu-bar` | Status-item lifecycle, native menus, window hiding and reopening, and typed commands. | | `soundboard` | Full music player: audio effects, timers, search, assets, native context menus, and adaptive markup. | +| `typography` | Model-derived design tokens, styled paragraphs, alignment, and bundled fonts. | | `system-monitor` | Subprocess effects, timers, parsing, tables, charts, controlled scroll, and confirmation flows. | Soundboard and System Monitor use the canonical unsuffixed folders. Their TypeScript cores and Native markup replace the earlier duplicate Zig implementations. New TypeScript apps need no language suffix. diff --git a/examples/typography/README.md b/examples/typography/README.md new file mode 100644 index 000000000..38b9c8c8f --- /dev/null +++ b/examples/typography/README.md @@ -0,0 +1,5 @@ +# Typography + +A TypeScript core and Native markup example of model-derived design tokens, ordered inline text styles, paragraph alignment, and a bundled TrueType font. + +Run `native dev`. Change size re-derives typography from the committed model; Change accent re-derives the accent and selected control. The manifest registers the mono face before the first layout. The runtime stamps device scale; omitted tokens inherit the resolved OS appearance and accessibility settings. diff --git a/examples/typography/app.json b/examples/typography/app.json new file mode 100644 index 000000000..32c647422 --- /dev/null +++ b/examples/typography/app.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://schema.native-sdk.dev/app/v1.json", + "id": "dev.native_sdk.typography", + "name": "typography", + "display_name": "Typography", + "description": "Model-derived typography and inline text styles.", + "version": "0.1.0", + "platforms": [ + "macos" + ], + "permissions": [ + "view", + "command" + ], + "capabilities": [ + "native_views", + "gpu_surfaces" + ], + "assets": { + "fonts": [ + { + "id": 64, + "path": "assets/GeistMono-Regular.ttf" + } + ] + }, + "shell": { + "windows": [ + { + "label": "main", + "title": "Typography", + "width": 560, + "height": 360, + "restore_policy": "center_on_primary", + "views": [ + { + "label": "typography-canvas", + "kind": "gpu_surface", + "fill": true, + "gpu_backend": "metal", + "gpu_pixel_format": "bgra8_unorm", + "gpu_present_mode": "timer", + "gpu_color_space": "srgb", + "gpu_alpha_mode": "opaque", + "gpu_vsync": true + } + ] + } + ] + } +} diff --git a/examples/typography/assets/GeistMono-Regular.ttf b/examples/typography/assets/GeistMono-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8c09dd96a8457ce53638f8e4dab2790d9d309a8d GIT binary patch literal 116248 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

+!hS5k8EEh#F4x12sl1fL+ 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 YHc_b|yQfv#^I*3oV+Zq?+gil`14 + {status} + A single paragraph with mixed styles and registered mono. + 1234567890 + + + + + diff --git a/examples/typography/src/core.ts b/examples/typography/src/core.ts new file mode 100644 index 000000000..3bb444436 --- /dev/null +++ b/examples/typography/src/core.ts @@ -0,0 +1,16 @@ +import { asciiBytes } from "@native-sdk/core"; +import type { ThemeDesignTokenOverrides, ThemeColor } from "@native-sdk/core/theme"; + +export interface Model { readonly large: boolean; readonly accent: boolean; } +export type Msg = { readonly kind: "size" } | { readonly kind: "accent" }; +export function initialModel(): Model { return { large: false, accent: false }; } +export function update(model: Model, msg: Msg): Model { + return msg.kind === "size" ? { ...model, large: !model.large } : { ...model, accent: !model.accent }; +} +export function status(model: Model): Uint8Array { return asciiBytes(model.large ? "Large type" : "Regular type"); } +export function tokenOverrides(model: Model): ThemeDesignTokenOverrides { + const accent: ThemeColor = { r: model.accent ? 0.125 : 0.3, g: 0.25, b: 0.9, a: 1 }; + return { typography: { mono_font_id: 64, display_size: model.large ? 42 : 32 }, + radius: { sm: 7, md: 10 }, colors: { accent }, + controls: { button_primary: { active_background: accent } } }; +} diff --git a/examples/typography/tests/typography.test.ts b/examples/typography/tests/typography.test.ts new file mode 100644 index 000000000..2da9a5a72 --- /dev/null +++ b/examples/typography/tests/typography.test.ts @@ -0,0 +1,55 @@ +import assert from "node:assert/strict"; +import { readFileSync, writeFileSync, mkdtempSync, mkdirSync, rmSync, truncateSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { spawnSync } from "node:child_process"; +import test from "node:test"; +import { NativeApp, findWidget, type NativeSnapshot } from "@native-sdk/core/testing"; + +test("typography retains complete snapshots, effects and replay across model themes", async () => { + const app = await NativeApp.start({ width: 560, height: 360 }); + try { + let snapshot = await app.snapshot(); + const snapshots: NativeSnapshot[] = [snapshot]; + const identities = snapshot.widgets.map(widget => widget.id); + for (let index = 0; index < 12; index++) { + const name = index % 2 === 0 ? "Change size" : "Change accent"; + snapshot = await app.click(findWidget(snapshot, { role: "button", name })); + snapshots.push(snapshot); + assert.deepEqual(snapshot.widgets.map(widget => widget.id), identities); + assert.ok(findWidget(snapshot, { role: "text", name: snapshot.model.large ? "Large type" : "Regular type" })); + assert.deepEqual(snapshot.effects, snapshots[0]!.effects); + } + assert.deepEqual(snapshot.model, { large: false, accent: false }); + const replay = await app.verifyReplay(); + assert.ok(replay.events > 0 && replay.checkpoints > 0); + assert.equal(replay.effects, 0); + assert.deepEqual(replay.snapshot.model, snapshot.model); + assert.deepEqual(replay.snapshot.widgets, snapshot.widgets); + assert.deepEqual(replay.snapshot.effects, snapshot.effects); + snapshots.push(replay.snapshot); + const reference = process.env.NATIVE_SDK_TEST_VIEW_REFERENCE; + if (reference) { + const complete = snapshots.map(({ viewBackend, ...value }) => value); + if (process.env.NATIVE_SDK_TEST_VIEW_BACKEND === "zig") writeFileSync(`${reference}.typography.json`, JSON.stringify(complete)); + else assert.deepEqual(complete, JSON.parse(readFileSync(`${reference}.typography.json`, "utf8"))); + } + } finally { await app.close(); } +}); + +test("declared fonts refuse missing, malformed and over-budget assets before startup", () => { + const executable = resolve(process.env.NATIVE_SDK_TEST_HOST!); + const directory = mkdtempSync(join(tmpdir(), "native-font-startup-")); + const font = join(directory, "assets", "GeistMono-Regular.ttf"); + try { + mkdirSync(join(directory, "assets")); + for (const failure of ["FileNotFound", "FontParseFailed", "StreamTooLong"]) { + if (failure === "FontParseFailed") writeFileSync(font, "not a font"); + if (failure === "StreamTooLong") truncateSync(font, 24 * 1024 * 1024 + 1); + const result = spawnSync(executable, [], { cwd: directory, input: JSON.stringify({ op: "start", width: 560, height: 360 }) + "\n", encoding: "utf8", timeout: 15000 }); + assert.equal(result.error, undefined); + assert.match(result.stdout + result.stderr, new RegExp(failure)); + assert.doesNotMatch(result.stdout, /"snapshot":/); + } + } finally { rmSync(directory, { recursive: true }); } +}); diff --git a/packages/core/package.json b/packages/core/package.json index 937ddfdb3..cd6b9247d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -31,6 +31,10 @@ "types": "./sdk/events.ts", "default": "./sdk/events.ts" }, + "./theme": { + "types": "./sdk/theme.ts", + "default": "./sdk/theme.ts" + }, "./testing": { "types": "./testing/index.ts", "default": "./testing/index.ts" diff --git a/packages/core/scripts/gen_declarations.mjs b/packages/core/scripts/gen_declarations.mjs index 835e495ca..dde500f14 100644 --- a/packages/core/scripts/gen_declarations.mjs +++ b/packages/core/scripts/gen_declarations.mjs @@ -23,7 +23,7 @@ const pkg = path.dirname(path.dirname(fileURLToPath(import.meta.url))); const sdkDir = path.join(pkg, "sdk"); /// The author-visible modules, in exports-map order. -export const declarationSources = ["core.ts", "text.ts", "events.ts"]; +export const declarationSources = ["core.ts", "text.ts", "events.ts", "theme.ts"]; /// Generate declaration text per output file name (relative to sdk/), /// entirely in memory. diff --git a/packages/core/scripts/stage_external_core.mjs b/packages/core/scripts/stage_external_core.mjs index 475ffb961..bc92508f6 100644 --- a/packages/core/scripts/stage_external_core.mjs +++ b/packages/core/scripts/stage_external_core.mjs @@ -75,6 +75,7 @@ function resolveSpecifiers(text, rel) { }; return text .replaceAll('"@native-sdk/core/text"', `"${toSdk("text.ts")}"`) + .replaceAll('"@native-sdk/core/theme"', `"${toSdk("theme.ts")}"`) .replaceAll('"@native-sdk/core/events"', `"${toSdk("events.ts")}"`) .replaceAll('"@native-sdk/core"', `"${toSdk("core.ts")}"`) .replaceAll('"@native-sdk/services"', `"${path.posix.relative(path.posix.dirname(rel), "services.gen.ts").startsWith(".") ? path.posix.relative(path.posix.dirname(rel), "services.gen.ts") : `./${path.posix.relative(path.posix.dirname(rel), "services.gen.ts")}`}"`) @@ -135,7 +136,7 @@ for (const rel of authorFiles) { // Transform 5: the staged SDK library modules spell their exported // records as object-literal aliases (value storage). -for (const sdkFile of ["text.ts", "events.ts"]) { +for (const sdkFile of ["text.ts", "events.ts", "theme.ts"]) { const staged = fs .readFileSync(path.join(args.sdk, sdkFile), "utf8") .replace(/^export interface ([A-Za-z0-9_]+) \{/gm, "export type $1 = {"); diff --git a/packages/core/sdk/theme.d.ts b/packages/core/sdk/theme.d.ts new file mode 100644 index 000000000..a584c8840 --- /dev/null +++ b/packages/core/sdk/theme.d.ts @@ -0,0 +1,251 @@ +export type ThemeColor = { + readonly r: number; + readonly g: number; + readonly b: number; + readonly a: number; +}; +export type ThemeFontFamily = "geist" | "geist_mono" | "system_sans" | "system_mono"; +export type ThemeSpinnerStyleToken = "arc" | "segmented"; +export type ThemeEasing = "linear" | "standard" | "emphasized" | "spring"; +export type ThemeScrollOverscroll = "none" | "rubber_band"; +export type ThemeTabsIndicatorKind = "pill" | "underline"; +export type ThemeButtonGroupKind = "segmented" | "detached"; +export type ThemeDensity = "compact" | "regular" | "spacious"; +export type ThemeColorTokenOverrides = { + readonly background?: ThemeColor; + readonly surface?: ThemeColor; + readonly surface_subtle?: ThemeColor; + readonly surface_pressed?: ThemeColor; + readonly text?: ThemeColor; + readonly text_muted?: ThemeColor; + readonly syntax_plain?: ThemeColor; + readonly syntax_comment?: ThemeColor; + readonly syntax_keyword?: ThemeColor; + readonly syntax_literal?: ThemeColor; + readonly syntax_function?: ThemeColor; + readonly syntax_property?: ThemeColor; + readonly syntax_constant?: ThemeColor; + readonly border?: ThemeColor; + readonly accent?: ThemeColor; + readonly accent_text?: ThemeColor; + readonly destructive?: ThemeColor; + readonly destructive_text?: ThemeColor; + readonly success?: ThemeColor; + readonly success_text?: ThemeColor; + readonly warning?: ThemeColor; + readonly warning_text?: ThemeColor; + readonly info?: ThemeColor; + readonly info_text?: ThemeColor; + readonly focus_ring?: ThemeColor; + readonly shadow?: ThemeColor; + readonly scrim?: ThemeColor; + readonly disabled?: ThemeColor; +}; +export type ThemeTypographyTokenOverrides = { + readonly font_id?: number; + readonly mono_font_id?: number; + readonly font_family?: ThemeFontFamily; + readonly mono_font_family?: ThemeFontFamily; + readonly body_size?: number; + readonly label_size?: number; + readonly title_size?: number; + readonly button_size?: number; + readonly button_font_id?: number; + readonly heading_size?: number; + readonly display_size?: number; +}; +export type ThemeSpacingTokenOverrides = { + readonly xs?: number; + readonly sm?: number; + readonly md?: number; + readonly lg?: number; + readonly xl?: number; +}; +export type ThemeRadiusTokenOverrides = { + readonly sm?: number; + readonly md?: number; + readonly lg?: number; + readonly xl?: number; +}; +export type ThemeStrokeTokenOverrides = { + readonly hairline?: number; + readonly regular?: number; + readonly focus?: number; + readonly focus_offset?: number; +}; +export type ThemeStateTokenOverrides = { + readonly hover_fill_alpha?: number; + readonly pressed_fill_alpha?: number; + readonly disabled_alpha?: number; + readonly selection_wash_alpha?: number; + readonly destructive_wash_alpha?: number; + readonly destructive_wash_hover_alpha?: number; + readonly destructive_wash_pressed_alpha?: number; + readonly badge_destructive_wash_alpha?: number; + readonly secondary_hover_alpha?: number; +}; +export type ThemeControlMetricTokenOverrides = { + readonly control_height_sm?: number; + readonly control_height?: number; + readonly control_height_lg?: number; + readonly button_inset_sm?: number; + readonly button_inset?: number; + readonly button_inset_lg?: number; + readonly button_label_sm_step?: number; + readonly button_label_lg_step?: number; + readonly button_icon_gap?: number; + readonly icon_text_step?: number; + readonly row_extent?: number; + readonly size_inset_step?: number; + readonly slider_track_height?: number; + readonly slider_thumb_width?: number; + readonly slider_thumb_height?: number; + readonly tabs_indicator_thickness?: number; + readonly tabs_list_inset?: number; + readonly tabs_list_full_width?: boolean; + readonly tabs_trigger_height?: number; + readonly tabs_trigger_inset?: number; + readonly tabs_label_size_step?: number; + readonly tabs_gap?: number; + readonly button_group_gap?: number; + readonly spinner_style?: ThemeSpinnerStyleToken; + readonly spinner_segment_count?: number; + readonly spinner_segment_length_ratio?: number; + readonly spinner_segment_thickness_ratio?: number; + readonly spinner_segment_radius_ratio?: number; + readonly spinner_tail_opacity?: number; + readonly spinner_period_ms?: number; + readonly tooltip_show_delay_ms?: number; + readonly tooltip_warm_window_ms?: number; +}; +export type ThemeShadowTokenOverrides = { + readonly y?: number; + readonly blur?: number; + readonly spread?: number; +}; +export type ThemeShadowTokensOverrides = { + readonly none?: ThemeShadowTokenOverrides; + readonly xs?: ThemeShadowTokenOverrides; + readonly sm?: ThemeShadowTokenOverrides; + readonly md?: ThemeShadowTokenOverrides; +}; +export type ThemeBlurTokenOverrides = { + readonly none?: number; + readonly sm?: number; + readonly md?: number; + readonly scrim?: number; +}; +export type ThemeSpringTokenOverrides = { + readonly mass?: number; + readonly stiffness?: number; + readonly damping?: number; +}; +export type ThemeMotionTokenOverrides = { + readonly fast_ms?: number; + readonly normal_ms?: number; + readonly slow_ms?: number; + readonly easing?: ThemeEasing; + readonly spring?: ThemeSpringTokenOverrides; +}; +export type ThemeScrollPhysicsOverrides = { + readonly wheel_multiplier?: number; + readonly wheel_velocity_scale?: number; + readonly deceleration_per_second?: number; + readonly stop_velocity?: number; + readonly overscroll?: ThemeScrollOverscroll; + readonly rubberband_extent_ratio?: number; + readonly rubberband_max_extent?: number; + readonly rubberband_resistance?: number; + readonly rubberband_return_per_second?: number; + readonly rubberband_velocity_decay_per_second?: number; + readonly rubberband_snap_distance?: number; +}; +export type ThemeLayerTokenOverrides = { + readonly base?: number; + readonly floating?: number; + readonly overlay?: number; + readonly modal?: number; +}; +export type ThemePixelSnapTokenOverrides = { + readonly geometry?: boolean; + readonly text?: boolean; + readonly scale?: number; +}; +export type ThemeControlVisualTokenOverrides = { + readonly background?: ThemeColor; + readonly hover_background?: ThemeColor; + readonly active_background?: ThemeColor; + readonly pressed_background?: ThemeColor; + readonly disabled_background?: ThemeColor; + readonly disabled_foreground?: ThemeColor; + readonly foreground?: ThemeColor; + readonly active_foreground?: ThemeColor; + readonly border?: ThemeColor; + readonly radius?: number; + readonly stroke_width?: number; +}; +export type ThemeControlTokenOverrides = { + readonly button_default?: ThemeControlVisualTokenOverrides; + readonly button_primary?: ThemeControlVisualTokenOverrides; + readonly button_secondary?: ThemeControlVisualTokenOverrides; + readonly button_outline?: ThemeControlVisualTokenOverrides; + readonly button_ghost?: ThemeControlVisualTokenOverrides; + readonly button_destructive?: ThemeControlVisualTokenOverrides; + readonly button_disabled_border?: ThemeColor; + readonly toggle_button?: ThemeControlVisualTokenOverrides; + readonly accordion?: ThemeControlVisualTokenOverrides; + readonly alert?: ThemeControlVisualTokenOverrides; + readonly bubble?: ThemeControlVisualTokenOverrides; + readonly card?: ThemeControlVisualTokenOverrides; + readonly dialog?: ThemeControlVisualTokenOverrides; + readonly drawer?: ThemeControlVisualTokenOverrides; + readonly sheet?: ThemeControlVisualTokenOverrides; + readonly select?: ThemeControlVisualTokenOverrides; + readonly input?: ThemeControlVisualTokenOverrides; + readonly text_field?: ThemeControlVisualTokenOverrides; + readonly search_field?: ThemeControlVisualTokenOverrides; + readonly combobox?: ThemeControlVisualTokenOverrides; + readonly textarea?: ThemeControlVisualTokenOverrides; + readonly list_item?: ThemeControlVisualTokenOverrides; + readonly menu_item?: ThemeControlVisualTokenOverrides; + readonly data_cell?: ThemeControlVisualTokenOverrides; + readonly tabs?: ThemeControlVisualTokenOverrides; + readonly segmented_control?: ThemeControlVisualTokenOverrides; + readonly tabs_indicator?: ThemeTabsIndicatorKind; + readonly button_group?: ThemeControlVisualTokenOverrides; + readonly button_group_style?: ThemeButtonGroupKind; + readonly checkbox?: ThemeControlVisualTokenOverrides; + readonly radio?: ThemeControlVisualTokenOverrides; + readonly switch_control?: ThemeControlVisualTokenOverrides; + readonly slider?: ThemeControlVisualTokenOverrides; + readonly progress?: ThemeControlVisualTokenOverrides; + readonly scrollbar?: ThemeControlVisualTokenOverrides; + readonly panel?: ThemeControlVisualTokenOverrides; + readonly resizable?: ThemeControlVisualTokenOverrides; + readonly popover?: ThemeControlVisualTokenOverrides; + readonly menu_surface?: ThemeControlVisualTokenOverrides; + readonly dropdown_menu?: ThemeControlVisualTokenOverrides; + readonly tooltip?: ThemeControlVisualTokenOverrides; + readonly avatar?: ThemeControlVisualTokenOverrides; + readonly badge?: ThemeControlVisualTokenOverrides; + readonly separator?: ThemeControlVisualTokenOverrides; + readonly skeleton?: ThemeControlVisualTokenOverrides; + readonly spinner?: ThemeControlVisualTokenOverrides; +}; +export type ThemeDesignTokenOverrides = { + readonly colors?: ThemeColorTokenOverrides; + readonly typography?: ThemeTypographyTokenOverrides; + readonly spacing?: ThemeSpacingTokenOverrides; + readonly radius?: ThemeRadiusTokenOverrides; + readonly stroke?: ThemeStrokeTokenOverrides; + readonly states?: ThemeStateTokenOverrides; + readonly metrics?: ThemeControlMetricTokenOverrides; + readonly shadow?: ThemeShadowTokensOverrides; + readonly blur?: ThemeBlurTokenOverrides; + readonly motion?: ThemeMotionTokenOverrides; + readonly scroll?: ThemeScrollPhysicsOverrides; + readonly layer?: ThemeLayerTokenOverrides; + readonly pixel_snap?: ThemePixelSnapTokenOverrides; + readonly controls?: ThemeControlTokenOverrides; + readonly density?: ThemeDensity; +}; diff --git a/packages/core/sdk/theme.ts b/packages/core/sdk/theme.ts new file mode 100644 index 000000000..d71e60eeb --- /dev/null +++ b/packages/core/sdk/theme.ts @@ -0,0 +1,267 @@ +// Typed model-derived token overrides. Names match the native token ABI. +// Omitted members inherit the selected stock register. Colors use normalized RGBA. +export type ThemeColor = { readonly r: number; readonly g: number; readonly b: number; readonly a: number; } + +export type ThemeFontFamily = "geist" | "geist_mono" | "system_sans" | "system_mono"; +export type ThemeSpinnerStyleToken = "arc" | "segmented"; +export type ThemeEasing = "linear" | "standard" | "emphasized" | "spring"; +export type ThemeScrollOverscroll = "none" | "rubber_band"; +export type ThemeTabsIndicatorKind = "pill" | "underline"; +export type ThemeButtonGroupKind = "segmented" | "detached"; +export type ThemeDensity = "compact" | "regular" | "spacious"; + +export type ThemeColorTokenOverrides = { + readonly background?: ThemeColor; + readonly surface?: ThemeColor; + readonly surface_subtle?: ThemeColor; + readonly surface_pressed?: ThemeColor; + readonly text?: ThemeColor; + readonly text_muted?: ThemeColor; + readonly syntax_plain?: ThemeColor; + readonly syntax_comment?: ThemeColor; + readonly syntax_keyword?: ThemeColor; + readonly syntax_literal?: ThemeColor; + readonly syntax_function?: ThemeColor; + readonly syntax_property?: ThemeColor; + readonly syntax_constant?: ThemeColor; + readonly border?: ThemeColor; + readonly accent?: ThemeColor; + readonly accent_text?: ThemeColor; + readonly destructive?: ThemeColor; + readonly destructive_text?: ThemeColor; + readonly success?: ThemeColor; + readonly success_text?: ThemeColor; + readonly warning?: ThemeColor; + readonly warning_text?: ThemeColor; + readonly info?: ThemeColor; + readonly info_text?: ThemeColor; + readonly focus_ring?: ThemeColor; + readonly shadow?: ThemeColor; + readonly scrim?: ThemeColor; + readonly disabled?: ThemeColor; +} + +export type ThemeTypographyTokenOverrides = { + readonly font_id?: number; + readonly mono_font_id?: number; + readonly font_family?: ThemeFontFamily; + readonly mono_font_family?: ThemeFontFamily; + readonly body_size?: number; + readonly label_size?: number; + readonly title_size?: number; + readonly button_size?: number; + readonly button_font_id?: number; + readonly heading_size?: number; + readonly display_size?: number; +} + +export type ThemeSpacingTokenOverrides = { + readonly xs?: number; + readonly sm?: number; + readonly md?: number; + readonly lg?: number; + readonly xl?: number; +} + +export type ThemeRadiusTokenOverrides = { + readonly sm?: number; + readonly md?: number; + readonly lg?: number; + readonly xl?: number; +} + +export type ThemeStrokeTokenOverrides = { + readonly hairline?: number; + readonly regular?: number; + readonly focus?: number; + readonly focus_offset?: number; +} + +export type ThemeStateTokenOverrides = { + readonly hover_fill_alpha?: number; + readonly pressed_fill_alpha?: number; + readonly disabled_alpha?: number; + readonly selection_wash_alpha?: number; + readonly destructive_wash_alpha?: number; + readonly destructive_wash_hover_alpha?: number; + readonly destructive_wash_pressed_alpha?: number; + readonly badge_destructive_wash_alpha?: number; + readonly secondary_hover_alpha?: number; +} + +export type ThemeControlMetricTokenOverrides = { + readonly control_height_sm?: number; + readonly control_height?: number; + readonly control_height_lg?: number; + readonly button_inset_sm?: number; + readonly button_inset?: number; + readonly button_inset_lg?: number; + readonly button_label_sm_step?: number; + readonly button_label_lg_step?: number; + readonly button_icon_gap?: number; + readonly icon_text_step?: number; + readonly row_extent?: number; + readonly size_inset_step?: number; + readonly slider_track_height?: number; + readonly slider_thumb_width?: number; + readonly slider_thumb_height?: number; + readonly tabs_indicator_thickness?: number; + readonly tabs_list_inset?: number; + readonly tabs_list_full_width?: boolean; + readonly tabs_trigger_height?: number; + readonly tabs_trigger_inset?: number; + readonly tabs_label_size_step?: number; + readonly tabs_gap?: number; + readonly button_group_gap?: number; + readonly spinner_style?: ThemeSpinnerStyleToken; + readonly spinner_segment_count?: number; + readonly spinner_segment_length_ratio?: number; + readonly spinner_segment_thickness_ratio?: number; + readonly spinner_segment_radius_ratio?: number; + readonly spinner_tail_opacity?: number; + readonly spinner_period_ms?: number; + readonly tooltip_show_delay_ms?: number; + readonly tooltip_warm_window_ms?: number; +} + +export type ThemeShadowTokenOverrides = { + readonly y?: number; + readonly blur?: number; + readonly spread?: number; +} + +export type ThemeShadowTokensOverrides = { + readonly none?: ThemeShadowTokenOverrides; + readonly xs?: ThemeShadowTokenOverrides; + readonly sm?: ThemeShadowTokenOverrides; + readonly md?: ThemeShadowTokenOverrides; +} + +export type ThemeBlurTokenOverrides = { + readonly none?: number; + readonly sm?: number; + readonly md?: number; + readonly scrim?: number; +} + +export type ThemeSpringTokenOverrides = { + readonly mass?: number; + readonly stiffness?: number; + readonly damping?: number; +} + +export type ThemeMotionTokenOverrides = { + readonly fast_ms?: number; + readonly normal_ms?: number; + readonly slow_ms?: number; + readonly easing?: ThemeEasing; + readonly spring?: ThemeSpringTokenOverrides; +} + +export type ThemeScrollPhysicsOverrides = { + readonly wheel_multiplier?: number; + readonly wheel_velocity_scale?: number; + readonly deceleration_per_second?: number; + readonly stop_velocity?: number; + readonly overscroll?: ThemeScrollOverscroll; + readonly rubberband_extent_ratio?: number; + readonly rubberband_max_extent?: number; + readonly rubberband_resistance?: number; + readonly rubberband_return_per_second?: number; + readonly rubberband_velocity_decay_per_second?: number; + readonly rubberband_snap_distance?: number; +} + +export type ThemeLayerTokenOverrides = { + readonly base?: number; + readonly floating?: number; + readonly overlay?: number; + readonly modal?: number; +} + +export type ThemePixelSnapTokenOverrides = { + readonly geometry?: boolean; + readonly text?: boolean; + readonly scale?: number; +} + +export type ThemeControlVisualTokenOverrides = { + readonly background?: ThemeColor; + readonly hover_background?: ThemeColor; + readonly active_background?: ThemeColor; + readonly pressed_background?: ThemeColor; + readonly disabled_background?: ThemeColor; + readonly disabled_foreground?: ThemeColor; + readonly foreground?: ThemeColor; + readonly active_foreground?: ThemeColor; + readonly border?: ThemeColor; + readonly radius?: number; + readonly stroke_width?: number; +} + +export type ThemeControlTokenOverrides = { + readonly button_default?: ThemeControlVisualTokenOverrides; + readonly button_primary?: ThemeControlVisualTokenOverrides; + readonly button_secondary?: ThemeControlVisualTokenOverrides; + readonly button_outline?: ThemeControlVisualTokenOverrides; + readonly button_ghost?: ThemeControlVisualTokenOverrides; + readonly button_destructive?: ThemeControlVisualTokenOverrides; + readonly button_disabled_border?: ThemeColor; + readonly toggle_button?: ThemeControlVisualTokenOverrides; + readonly accordion?: ThemeControlVisualTokenOverrides; + readonly alert?: ThemeControlVisualTokenOverrides; + readonly bubble?: ThemeControlVisualTokenOverrides; + readonly card?: ThemeControlVisualTokenOverrides; + readonly dialog?: ThemeControlVisualTokenOverrides; + readonly drawer?: ThemeControlVisualTokenOverrides; + readonly sheet?: ThemeControlVisualTokenOverrides; + readonly select?: ThemeControlVisualTokenOverrides; + readonly input?: ThemeControlVisualTokenOverrides; + readonly text_field?: ThemeControlVisualTokenOverrides; + readonly search_field?: ThemeControlVisualTokenOverrides; + readonly combobox?: ThemeControlVisualTokenOverrides; + readonly textarea?: ThemeControlVisualTokenOverrides; + readonly list_item?: ThemeControlVisualTokenOverrides; + readonly menu_item?: ThemeControlVisualTokenOverrides; + readonly data_cell?: ThemeControlVisualTokenOverrides; + readonly tabs?: ThemeControlVisualTokenOverrides; + readonly segmented_control?: ThemeControlVisualTokenOverrides; + readonly tabs_indicator?: ThemeTabsIndicatorKind; + readonly button_group?: ThemeControlVisualTokenOverrides; + readonly button_group_style?: ThemeButtonGroupKind; + readonly checkbox?: ThemeControlVisualTokenOverrides; + readonly radio?: ThemeControlVisualTokenOverrides; + readonly switch_control?: ThemeControlVisualTokenOverrides; + readonly slider?: ThemeControlVisualTokenOverrides; + readonly progress?: ThemeControlVisualTokenOverrides; + readonly scrollbar?: ThemeControlVisualTokenOverrides; + readonly panel?: ThemeControlVisualTokenOverrides; + readonly resizable?: ThemeControlVisualTokenOverrides; + readonly popover?: ThemeControlVisualTokenOverrides; + readonly menu_surface?: ThemeControlVisualTokenOverrides; + readonly dropdown_menu?: ThemeControlVisualTokenOverrides; + readonly tooltip?: ThemeControlVisualTokenOverrides; + readonly avatar?: ThemeControlVisualTokenOverrides; + readonly badge?: ThemeControlVisualTokenOverrides; + readonly separator?: ThemeControlVisualTokenOverrides; + readonly skeleton?: ThemeControlVisualTokenOverrides; + readonly spinner?: ThemeControlVisualTokenOverrides; +} + +export type ThemeDesignTokenOverrides = { + readonly colors?: ThemeColorTokenOverrides; + readonly typography?: ThemeTypographyTokenOverrides; + readonly spacing?: ThemeSpacingTokenOverrides; + readonly radius?: ThemeRadiusTokenOverrides; + readonly stroke?: ThemeStrokeTokenOverrides; + readonly states?: ThemeStateTokenOverrides; + readonly metrics?: ThemeControlMetricTokenOverrides; + readonly shadow?: ThemeShadowTokensOverrides; + readonly blur?: ThemeBlurTokenOverrides; + readonly motion?: ThemeMotionTokenOverrides; + readonly scroll?: ThemeScrollPhysicsOverrides; + readonly layer?: ThemeLayerTokenOverrides; + readonly pixel_snap?: ThemePixelSnapTokenOverrides; + readonly controls?: ThemeControlTokenOverrides; + readonly density?: ThemeDensity; +} diff --git a/packages/core/src/checker.ts b/packages/core/src/checker.ts index c68199308..2eb4c4f55 100644 --- a/packages/core/src/checker.ts +++ b/packages/core/src/checker.ts @@ -508,6 +508,7 @@ export class SubsetChecker { this.checkMigrationHook(); this.checkThemePackHelper(); this.checkThemeStateHelper(); + this.checkTokenOverridesHelper(); this.checkStatusItemHelper(); this.checkStatusItemsHelper(); this.checkWindowsHelper(); @@ -893,6 +894,21 @@ export class SubsetChecker { } } + private checkTokenOverridesHelper(): void { + const decl = this.entryExportedFunction("tokenOverrides"); + if (decl === null) return; + const helper = this.table.modelHelperDecls().find(candidate => candidate.name === "tokenOverrides" && candidate.decl === decl); + const returns = decl.type ? this.table.resolveTypeNode(decl.type) : undefined; + // The canonical declaration is complete: importing it gives every + // consumer one optional field for every register member, even when an + // app returns a small override. Partial hand-authored shapes cannot + // silently drop a token from generated projection or native validation. + if (!helper || returns?.k !== "struct" || returns.name !== "ThemeDesignTokenOverrides" || + this.table.structs.get(returns.name)?.decl.getSourceFile().fileName !== path.join(path.dirname(sdkCoreModulePath), "theme.ts")) { + this.report("NS1033", "`tokenOverrides` must be a single-Model helper returning `ThemeDesignTokenOverrides` imported from `@native-sdk/core/theme`.", decl.type ?? decl); + } + } + /// 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 @@ -2017,7 +2033,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", "themeState", "statusItem", "statusItems", "windows", + "commandMsg", "keyMsg", "frameMsg", "pinchMsg", "dropMsg", "appearanceMsg", "chromeMsg", "envMsgs", "themePack", "themeState", "tokenOverrides", "statusItem", "statusItems", "windows", "viewUnbound", "modelUnbound", "msgUnbound", ]); diff --git a/packages/core/src/infer.ts b/packages/core/src/infer.ts index 2e9969f79..82081f570 100644 --- a/packages/core/src/infer.ts +++ b/packages/core/src/infer.ts @@ -23,7 +23,8 @@ // into a slot some use forces to be an integer — that is a conflict reported // as a teaching error (NS1016), never a downstream Zig type error. -import { ts, TypedAst, exportListBindings } from "./typed_ast.ts"; +import { ts, TypedAst, exportListBindings, sdkLibraryModules } from "./typed_ast.ts"; +import path from "node:path"; import { constFunctionValue } from "./ownership.ts"; import { thrownShapeOf } from "./checker.ts"; import type { TypeTable } from "./types.ts"; @@ -275,7 +276,14 @@ export class IntInference { this.addSlot(node, node.name.text); } } else if (ts.isPropertySignature(node) && node.name && ts.isIdentifier(node.name)) { - if (this.numberish(node.type)) this.addSlot(node, node.name.text); + if (this.numberish(node.type)) { + // Canonical token numbers cross a fixed native register. Their ABI + // remains f64 across apps; the native capability validates integer + // ids/ranges and performs the explicit f32 conversion. + const theme = sdkLibraryModules.get("@native-sdk/core/theme"); + if (theme !== undefined && path.resolve(node.getSourceFile().fileName) === path.resolve(theme)) this.addFloatSlot(node, node.name.text); + else this.addSlot(node, node.name.text); + } } else if (ts.isPropertyDeclaration(node) && node.name && ts.isIdentifier(node.name)) { // R19: a class field is a slot like a record field, fed by its // initializer and by `this.x = ...` writes (wired in collectFlows). diff --git a/packages/core/src/runtime_policy.ts b/packages/core/src/runtime_policy.ts index a1a15f513..84cb7dac8 100644 --- a/packages/core/src/runtime_policy.ts +++ b/packages/core/src/runtime_policy.ts @@ -82,11 +82,11 @@ export function native_theme_policy(request: Uint8Array): Uint8Array { return result; } if (request[0] === 2) { - if (request.length !== 5 || request[4]! > 2) throw new Error("invalid theme control request"); + if ((request.length !== 5 && request.length !== 6) || request[4]! > 2 || (request.length === 6 && request[5]! > 1)) throw new Error("invalid theme control request"); for (let i = 1; i < 4; i++) if (request[i]! > 1) throw new Error("invalid theme control flag"); result[0] = request[1] === 1 ? 1 : request[2] === 1 ? 2 : 0; result[1] = result[0] === 0 && (request[3] === 0 || request[4] === 0) ? 1 : 0; - result[2] = request[1] === 1 || request[3] === 1 || result[1] === 1 ? 1 : 0; + result[2] = request[1] === 1 || request[3] === 1 || result[1] === 1 || request[5] === 1 ? 1 : 0; return result; } throw new Error("unknown theme policy operation"); diff --git a/packages/core/src/typed_ast.ts b/packages/core/src/typed_ast.ts index ce1080a09..89fcc37d2 100644 --- a/packages/core/src/typed_ast.ts +++ b/packages/core/src/typed_ast.ts @@ -50,6 +50,7 @@ export const sdkCoreModulePath = path.join(sdkModuleDir, "core.ts"); export const sdkLibraryModules: ReadonlyMap = new Map([ ["@native-sdk/core/text", path.join(sdkModuleDir, "text.ts")], ["@native-sdk/core/events", path.join(sdkModuleDir, "events.ts")], + ["@native-sdk/core/theme", path.join(sdkModuleDir, "theme.ts")], ]); /// The ambient byte-text method surface (declaration merging into diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index e908978f3..15b6789d2 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -332,6 +332,8 @@ export class TypeTable { } private isCanonicalOptionalSdkRecord(decl: ts.TypeAliasDeclaration): boolean { + const theme = sdkLibraryModules.get("@native-sdk/core/theme"); + if (theme !== undefined && path.resolve(decl.getSourceFile().fileName) === path.resolve(theme)) return true; const events = sdkLibraryModules.get("@native-sdk/core/events"); return (decl.name.text === "ThemeState" || decl.name.text === "StatusItemPresentation" || decl.name.text === "StatusItemMenuItem") && events !== undefined && path.resolve(decl.getSourceFile().fileName) === path.resolve(events); diff --git a/packages/core/src/view_components.ts b/packages/core/src/view_components.ts index 36394885b..74ce75f30 100644 --- a/packages/core/src/view_components.ts +++ b/packages/core/src/view_components.ts @@ -4,6 +4,8 @@ */ import { textWordSelectionAtOffset as nscvWordSelection, textLineSelectionAtOffset as nscvLineSelection, caretSelectionAt as nscvSelection, applyTextInputEvent as nscvApplyTextEdit, sanitizedSingleLineTextInputEvent as nscvSanitizeTextInput, codeIndentationInsertion as nscvIndentation, parseCodeLineNumberSpec as nscvCodeLines, textClipboardRange as nscvClipboardRange, type TextInputEvent as NscvTextInputEvent } from "@native-sdk/core/text"; +type NscViewSpan = { text: string; weight?: string; color?: string; scale?: number; monospace?: boolean; italic?: boolean; underline?: boolean }; + type NscViewNode = { end: number; kind: string; text: string; placeholder?: string; command?: string; wrap?: boolean; submitOnEnter?: boolean; key?: string; keyInt?: number; keySlot?: number; globalKey?: string; globalKeyInt?: number; @@ -12,11 +14,12 @@ type NscViewNode = { resizeDuration?: number; resizeEasing?: string; resizeOrigin?: number; value?: number; valueX?: number; axis?: string; overscroll?: string; image?: number; icon?: string; label?: string; role?: string; - background?: string; foreground?: string; radius?: string; windowDrag?: boolean; + background?: string; foreground?: string; borderColor?: string; radius?: string; windowDrag?: boolean; main?: string; cross?: string; size?: string; variant?: string; checked?: boolean; disabled?: boolean; selected?: boolean; focusable?: boolean; expanded?: boolean; treeLevel?: number; listItemIndex?: number; listItemCount?: number; + spans?: readonly NscViewSpan[]; textAlignment?: string; spanWeight?: string; spanColor?: string; spanScale?: number; codeLanguage?: string; codeLineDigits?: number; codeAddedLines?: readonly number[]; codeRemovedLines?: readonly number[]; @@ -26,6 +29,11 @@ type NscViewNode = { anchor?: string; anchorAlignment?: string; anchorOffset?: number; tooltipDelay?: number; }; +function nscvSpanScale(value: number): number { + if (!Number.isFinite(value) || value <= 0 || value > 3.4028234663852886e38) throw new Error("span scale requires a positive finite f32"); + return value; +} + function nscvPush(nodes: NscViewNode[], node: NscViewNode): void { if (nodes.length >= 1024) throw new Error("compiled view exceeds 1024 nodes"); nodes.push(node); diff --git a/packages/core/src/view_frontend.ts b/packages/core/src/view_frontend.ts index 385292dce..a21d9fafa 100644 --- a/packages/core/src/view_frontend.ts +++ b/packages/core/src/view_frontend.ts @@ -11,7 +11,7 @@ export interface ViewContract { model_helpers: { name: string; params: Ref[]; returns: Ref }[]; msg: { name?: string; arms: Arm[] }; } -interface Element { name: string; attrs: Map; children: Element[]; text: string; at: number; file: string } +interface Element { name: string; attrs: Map; children: Element[]; content: (Element | { text: string; at: number; end: number })[]; text: string; at: number; end: number; file: string } interface Expr { code: string; type: Ref } type Scope = ReadonlyMap; interface Slot { children: Element[]; scope: Scope; slot?: Slot } @@ -53,7 +53,7 @@ function compileViewFunction(source: string, contract: ViewContract, options: Vi throw new Error(`${node.file}:${before.length}:${before.at(-1)!.length + 1}: compiled TypeScript view: ${message}`); }; const origin = { file: entry, at: 0 }; - const reserved = ["NscViewNode", "NscTimelineItem", "NscSurfaceRect", "native_view", "native_window_view", "native_radio_policy", "native_tabs_policy", "native_tree_policy", "native_list_policy", "native_menu_policy", "native_toggle_policy", "native_accordion_policy", "native_slider_policy", "native_split_policy", "native_scroll_policy", "native_resizable_policy", "native_text_policy", "native_timer_policy", "native_db_policy", "native_effect_policy", "native_stream_policy", "native_window_policy", "native_theme_policy", "native_status_policy", "JSON", "TextEncoder", "TextDecoder", "DataView", "String", "Number", "Array"]; + const reserved = ["NscViewSpan", "NscViewNode", "NscTimelineItem", "NscSurfaceRect", "native_view", "native_window_view", "native_radio_policy", "native_tabs_policy", "native_tree_policy", "native_list_policy", "native_menu_policy", "native_toggle_policy", "native_accordion_policy", "native_slider_policy", "native_split_policy", "native_scroll_policy", "native_resizable_policy", "native_text_policy", "native_timer_policy", "native_db_policy", "native_effect_policy", "native_stream_policy", "native_window_policy", "native_theme_policy", "native_status_policy", "JSON", "TextEncoder", "TextDecoder", "DataView", "String", "Number", "Array"]; const names = [...contract.types.structs, ...contract.types.enums ?? [], ...contract.types.unions ?? [], ...contract.model_helpers]; if (names.some(item => reserved.includes(item.name) || item.name.startsWith("nscv")) || reserved.includes(contract.msg.name ?? "")) { fail(origin, "core name collides with compiled view wiring"); @@ -86,10 +86,10 @@ function compileViewFunction(source: string, contract: ViewContract, options: Vi const start = /^<([a-z][a-z0-9-]*)\b/.exec(src.slice(pos)); if (!start) error("expected an element"); pos += start[0].length; - const node: Element = { name: start[1]!, attrs: new Map(), children: [], text: "", at, file }; + const node: Element = { name: start[1]!, attrs: new Map(), children: [], content: [], text: "", at, end: at, file }; for (;;) { while (/\s/.test(src[pos] ?? "") && pos < src.length) pos++; - if (src.startsWith("/>", pos)) { pos += 2; return node; } + if (src.startsWith("/>", pos)) { pos += 2; node.end = pos; return node; } if (src[pos] === ">") { pos++; break; } const attr = /^([a-z][a-z0-9-]*)\s*=\s*"([^"]*)"/.exec(src.slice(pos)); if (!attr) error("expected a quoted attribute or >"); @@ -106,12 +106,12 @@ function compileViewFunction(source: string, contract: ViewContract, options: Vi if (src.startsWith("/.exec(src.slice(pos)); if (!close || close[1] !== node.name) error(`expected `); - pos += close[0].length; return node; + pos += close[0].length; node.end = pos; return node; } - if (src[pos] === "<") node.children.push(element(depth + 1)); + if (src[pos] === "<") { const child = element(depth + 1); node.children.push(child); node.content.push(child); } else { const end = src.indexOf("<", pos), stop = end < 0 ? src.length : end; - node.text += decode(src.slice(pos, stop)); pos = stop; + const value = decode(src.slice(pos, stop)); node.text += value; node.content.push({ text: value, at: pos, end: stop }); pos = stop; } } }; @@ -414,8 +414,48 @@ function compileViewFunction(source: string, contract: ViewContract, options: Vi const container = ["column", "row", "stack", "grid", "card", "alert", "dialog", "drawer", "sheet", "scroll", "panel", "radio-group", "button-group", "breadcrumb", "pagination", "toggle-group", "accordion", "tabs", "tree", "list", "list-item", "dropdown-menu", "split", "resizable"].includes(node.name); if (node.attrs.get("role") === "treeitem" && !["column", "row", "panel"].includes(node.name)) fail(node, "compiled treeitem requires column, row or panel"); if (node.attrs.get("role") === "tree" && !["column", "row", "panel", "scroll", "tree"].includes(node.name)) fail(node, "tree role requires a generic container"); - if (["list-item", "card", "alert"].includes(node.name) ? node.text.trim() !== "" && node.children.length !== 0 : container ? node.text.trim() !== "" : node.children.length !== 0) fail(node, "mixed content is unsupported"); + const paragraph = node.name === "text" && node.children.length > 0; + if (!paragraph && (["list-item", "card", "alert"].includes(node.name) ? node.text.trim() !== "" && node.children.length !== 0 : container ? node.text.trim() !== "" : node.children.length !== 0)) fail(node, "mixed content is unsupported"); const props: string[] = [`kind: ${JSON.stringify(kinds[node.name])}`, `text: ${text(node.text.trim(), node, scope)}`]; + if (paragraph) { + if (node.attrs.has("wrap") || node.attrs.has("overflow")) fail(node, "span paragraphs always word-wrap; drop wrap and overflow"); + const runs: string[] = []; + let separator = false; + for (const part of node.content) { + const isSpan = "name" in part; + if (isSpan && (part.name !== "span" || part.children.length || part.content.length !== 1 || !part.text.trim())) fail(part, "text takes nonempty span leaves without nesting"); + const raw = part.text; + const visible = raw.replace(/^[ \t\r\n]+|[ \t\r\n]+$/g, ""); + if (!isSpan && /^[ \t\r\n]/.test(raw)) separator = true; + if (!visible) { if (/[ \t\r\n]/.test(raw)) separator = true; continue; } + if (runs.length && separator) runs.push('{ text: " " }'); + const fields = [`text: ${text(visible, node, scope)}`]; + if (isSpan) for (const [name, value] of part.attrs) { + if (["mono", "italic", "underline"].includes(name)) fields.push(`${name === "mono" ? "monospace" : name}: ${bound(value, "boolean", part, scope)}`); + else if (name === "scale") { + if (!value.startsWith("{") && (!Number.isFinite(Number(value)) || Number(value) <= 0)) fail(part, "span scale requires a positive finite number"); + fields.push(`scale: nscvSpanScale(${bound(value, "number", part, scope)})`); + } else if (name === "weight") { + if (value.startsWith("{")) { + const expr = binding(value, part, scope); + const members = contract.types.enums?.find(item => item.name === expr.type.name)?.members; + if (expr.type.kind !== "enum" || !members?.every(member => ["regular", "medium", "bold"].includes(member))) fail(part, "span weight requires regular, medium or bold"); + fields.push(`weight: ${expr.code}`); + } else { + if (!["regular", "medium", "bold"].includes(value)) fail(part, "unsupported span weight"); + fields.push(`weight: ${JSON.stringify(value)}`); + } + } else if (name === "foreground") { + if (!["background", "surface", "surface_subtle", "surface_pressed", "text", "text_muted", "syntax_plain", "syntax_comment", "syntax_keyword", "syntax_literal", "syntax_function", "syntax_property", "syntax_constant", "border", "accent", "accent_text", "destructive", "destructive_text", "success", "success_text", "warning", "warning_text", "info", "info_text", "focus_ring", "shadow", "scrim", "disabled"].includes(value)) fail(part, "unsupported span foreground token"); + fields.push(`color: ${JSON.stringify(value)}`); + } else fail(part, `unsupported span attribute ${name}; layout, identity and events belong on text`); + } + runs.push(`{ ${fields.join(", ")} }`); + separator = !isSpan && /[ \t\r\n]$/.test(raw); + } + props[1] = 'text: ""'; + props.push(`spans: [${runs.join(", ")}]`); + } if (node.name === "code") { if (node.attrs.get("editable") !== "true" || node.attrs.get("wrap") !== "false") fail(node, 'compiled code requires editable="true" and wrap="false"'); @@ -457,6 +497,10 @@ function compileViewFunction(source: string, contract: ViewContract, options: Vi props.push(`${name}: ${JSON.stringify(value)}`); } } + else if (name === "text-alignment") { + if (!["start", "center", "end"].includes(value)) fail(node, "unsupported text-alignment"); + props.push(`textAlignment: ${JSON.stringify(value)}`); + } else if (name === "min-width" || name === "max-width") props.push(`${name === "min-width" ? "minWidth" : "maxWidth"}: ${bound(value, "number", node, scope)}`); else if (["resize-duration", "resize-easing", "resize-origin"].includes(name)) { if (node.name !== "split") fail(node, `${name} requires split`); @@ -513,11 +557,11 @@ function compileViewFunction(source: string, contract: ViewContract, options: Vi props.push(`${name === "anchor" ? "anchor" : "anchorAlignment"}: ${JSON.stringify(value)}`); } } - else if (["main", "cross", "size", "variant", "role", "background", "foreground", "radius"].includes(name)) { - const vocab: Record = { main: ["start", "center", "end", "space_between"], cross: ["start", "center", "end", "stretch"], size: ["default", "sm", "lg", "icon", "heading", "display"], variant: ["default", "primary", "secondary", "ghost", "destructive", "outline"], role: ["listitem", "tree", "treeitem"], background: ["background", "surface"], foreground: ["text", "text_muted", "destructive"], radius: ["sm", "md", "lg"] }; + else if (["main", "cross", "size", "variant", "role", "background", "foreground", "border-color", "radius"].includes(name)) { + const vocab: Record = { main: ["start", "center", "end", "space_between"], cross: ["start", "center", "end", "stretch"], size: ["default", "sm", "lg", "icon", "heading", "display"], variant: ["default", "primary", "secondary", "ghost", "destructive", "outline"], role: ["listitem", "tree", "treeitem"], background: ["background", "surface"], foreground: ["text", "text_muted", "destructive"], "border-color": ["background", "surface", "border", "accent", "focus_ring", "destructive"], radius: ["sm", "md", "lg"] }; if (!vocab[name]!.includes(value)) fail(node, `unsupported ${name}=${value}`); if (name === "size" && (value === "heading" || value === "display") && node.name !== "text") fail(node, `${value} size requires text`); - props.push(`${name}: ${JSON.stringify(value)}`); + props.push(`${name === "border-color" ? "borderColor" : name}: ${JSON.stringify(value)}`); } else if (["on-press", "on-hold", "on-toggle", "on-change", "on-drag", "on-scroll", "on-input", "on-submit", "on-dismiss", "on-resize", "on-hover-enter", "on-hover-leave"].includes(name)) { const channel = name.slice(3); const treeRow = container && node.attrs.get("role") === "treeitem"; @@ -532,7 +576,7 @@ function compileViewFunction(source: string, contract: ViewContract, options: Vi const id = next++; if (node.name === "tabs" || node.name === "split") output.push(`const nscvStart${id} = nscvNodes.length;`); output.push(`const nscvNode${id}: NscViewNode = { end: 0, ${props.join(", ")} };`, `nscvNodes.push(nscvNode${id});`, 'if (nscvNodes.length > 1024) throw new Error("compiled view exceeds 1024 nodes");'); - emitChildren(node.children, scope, slot, stack, depth + 1); output.push(`nscvNode${id}.end = nscvNodes.length;`); + if (!paragraph) emitChildren(node.children, scope, slot, stack, depth + 1); output.push(`nscvNode${id}.end = nscvNodes.length;`); if (node.name === "code") { const spec = (name: string): string => { const raw = node.attrs.get(name); diff --git a/packages/core/test/checker.test.ts b/packages/core/test/checker.test.ts index 363fd31c2..df9dab5bf 100644 --- a/packages/core/test/checker.test.ts +++ b/packages/core/test/checker.test.ts @@ -1867,3 +1867,37 @@ export function update(model: Model, msg: Msg): Model { `); assert.ok(ruleIds(asserted).includes("NS1061"), `got ${ruleIds(asserted)}`); }); + +test("model token overrides require the complete SDK register and remain entry-owned", () => { + const result = check(` +import type { ThemeDesignTokenOverrides } from "@native-sdk/core/theme"; +export interface Model { readonly dark: boolean; } +export type Msg = { readonly kind: "toggle" }; +export function initialModel(): Model { return { dark: false }; } +export function update(model: Model, msg: Msg): Model { return { dark: !model.dark }; } +export function tokenOverrides(model: Model): ThemeDesignTokenOverrides { + return { typography: { display_size: 36, mono_font_id: 64 }, radius: { sm: 7 }, + colors: { accent: { r: 0.1, g: 0.2, b: model.dark ? 0.9 : 0.5, a: 1 } }, + controls: { button_primary: { radius: 10, stroke_width: 1 } }, pixel_snap: { geometry: true, text: true } }; +} +`, { contractEntry: "core.ts" }); + assert.equal(result.ok, true, result.diagnostics.map(d => d.message).join("\n")); + const contract = JSON.parse(result.contract!); + const register = contract.types.structs.find((record: { name: string }) => record.name === "ThemeDesignTokenOverrides"); + assert.equal(register.fields.length, 15); + assert.ok(register.fields.every((field: { type: { kind: string } }) => field.type.kind === "optional")); + const colors = contract.types.structs.find((record: { name: string }) => record.name === "ThemeColorTokenOverrides"); + const typography = contract.types.structs.find((record: { name: string }) => record.name === "ThemeTypographyTokenOverrides"); + assert.ok(typography.fields.every((field: { type: { inner: { kind: string } } }) => field.type.inner.kind !== "i64")); + assert.equal(colors.fields.length, 28); + assert.ok(colors.fields.every((field: { type: { kind: string } }) => field.type.kind === "optional")); + const wrong = checkOnly(` +export interface Model { readonly value: number; } +export type Msg = { readonly kind: "tick" }; +export function initialModel(): Model { return { value: 0 }; } +export function update(model: Model, msg: Msg): Model { return model; } +export type PartialTokens = { readonly radius?: number }; +export function tokenOverrides(model: Model): PartialTokens { return {}; } +`); + assert.ok(ruleIds(wrong).includes("NS1033")); +}); diff --git a/packages/core/test/runtime_policy.test.ts b/packages/core/test/runtime_policy.test.ts index eb9364a58..e69aee952 100644 --- a/packages/core/test/runtime_policy.test.ts +++ b/packages/core/test/runtime_policy.test.ts @@ -342,8 +342,10 @@ test("theme policy preserves complete-token precedence and rebuild coordination" const follows = !fn && !fixed && (!helper || scheme === 0); assert.deepEqual([...native_theme_policy(new Uint8Array([2, fn, fixed, helper, scheme]))], [fn ? 1 : fixed ? 2 : 0, +follows, +(!!fn || !!helper || follows), 0]); + for (const overrides of [0, 1]) assert.deepEqual([...native_theme_policy(new Uint8Array([2, fn, fixed, helper, scheme, overrides]))], + [fn ? 1 : fixed ? 2 : 0, +follows, +(!!fn || !!helper || !!overrides || follows), 0]); } - for (const bytes of [[], [3], [1], [2], [1, 3, 1, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 1, 0, 2, 0, 0, 0], [2, 2, 0, 0, 0], [2, 0, 0, 0, 3]]) { + for (const bytes of [[], [3], [1], [2], [1, 3, 1, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 1, 0, 2, 0, 0, 0], [2, 2, 0, 0, 0], [2, 0, 0, 0, 3], [2, 0, 0, 0, 0, 2], [2, 0, 0, 0, 0, 0, 0]]) { assert.throws(() => native_theme_policy(new Uint8Array(bytes)), /theme policy|theme .*request|theme .*flag/); } }); diff --git a/packages/core/test/view_frontend.test.ts b/packages/core/test/view_frontend.test.ts index 6ceb1d328..3e131119c 100644 --- a/packages/core/test/view_frontend.test.ts +++ b/packages/core/test/view_frontend.test.ts @@ -1765,3 +1765,44 @@ test("unkeyed enum filters preserve structural identities and dispatch typed enu assert.throws(() => compileView(markup.replace("set_filter:{f}", "set_filter:{other}"), c), /matching scalar/); assert.throws(() => compileView(markup.replace('as="f"', 'as="f" key=""'), c), /for requires/); }); + +test("inline paragraphs preserve ordered runs, native whitespace, identity and styles", () => { + const { view } = evaluate(' Value {status}. done '); + const tree = view(); + assert.equal(tree.nodes.length, 1); + const node = tree.nodes[0]; + assert.equal(node.textAlignment, "end"); + assert.equal(node.key, "readout"); + assert.deepEqual(node.spans, [ + { text: "Value" }, { text: " " }, + { text: "Café\nnotes", monospace: true, weight: "medium", italic: true, underline: true, scale: 1.5, color: "accent" }, + { text: "." }, { text: " " }, { text: "done", weight: "bold" }, + ]); + assert.equal(node.end, 1); + assert.equal(node.text, ""); + assert.deepEqual(evaluate('ab').view().nodes[0].spans, [{ text: "a" }, { text: "b" }]); + assert.deepEqual(evaluate('a \n b').view().nodes[0].spans, [{ text: "a" }, { text: " " }, { text: "b" }]); +}); + +test("inline paragraph misuse is rejected at its authoring boundary", () => { + for (const markup of [ + 'outside', '', + 'nested', 'ab', '', + 'x', 'x', + 'x', 'x', + 'x', 'x', + 'x', 'x', + 'x', 'x', + 'x', + ]) assert.throws(() => compileView(markup, contract), undefined, markup); + const { model, view } = evaluate('x'); + assert.equal(view().nodes[0].spans[0].scale, 7); + model.count = 0; + assert.throws(view, /positive finite/); +}); + +test("text entry border tokens preserve the authored surface blend", () => { + const node = evaluate('').view().nodes[0]; + assert.equal(node.borderColor, "background"); + assert.throws(() => compileView('', contract), /unsupported border-color/); +}); diff --git a/skill-data/native-ui/SKILL.md b/skill-data/native-ui/SKILL.md index d53cb135a..f2f61bcf0 100644 --- a/skill-data/native-ui/SKILL.md +++ b/skill-data/native-ui/SKILL.md @@ -1286,3 +1286,15 @@ For scripted checks (and the CI workflow `native init --full` scaffolds), replac native automate assert 'gpu_nonblank=true' 'role=button name="Reset"' 'count: 0' native automate assert --absent 'error event=' ``` + +## Compiled inline typography + +A text paragraph can contain ordered `span` leaves. Each leaf supports `weight="regular|medium|bold"`, `mono`, `italic`, `underline`, a positive `scale`, and a foreground color token. Use `text-alignment="start|center|end"` on the paragraph. Layout, keys, semantics, and events belong on the enclosing text element. The compiler preserves one separating space between whitespace-separated runs and keeps punctuation adjacent. Spans cannot contain nested elements or several comment-separated text runs. + +```xml + + {readout} + +``` + +Text-entry controls also accept `border-color` tokens, including `background` for a field that blends into the surrounding surface. diff --git a/skill-data/ts-core/SKILL.md b/skill-data/ts-core/SKILL.md index f6fd0fa4a..bf32e486d 100644 --- a/skill-data/ts-core/SKILL.md +++ b/skill-data/ts-core/SKILL.md @@ -401,6 +401,8 @@ The generated wiring detects each channel from an export (export exists → wire - **`commandMsg(name: string): Msg | null`** — menus, shortcuts, and chrome tabs, by command id (string equality works on `string` values). - **`statusItem(model: Model): StatusItemState` / `statusItems(model: Model): readonly StatusItemDescriptor[]`** — one complete menu-bar item, or a stable-id collection. Presentation includes generic title typography. Dropdown rows include a typed `metric` block, typed segmented options, and bounded chart data; never encode these as text conventions. The generated launcher reconciles after committed updates. +- **`tokenOverrides(model: Model): ThemeDesignTokenOverrides`** — model-derived colors, typography, spacing, geometry, controls, and motion over the selected stock theme. Import the complete optional record from `@native-sdk/core/theme`; omitted members inherit the resolved stock register. Colors use normalized RGBA. Numbers cross the helper ABI as f64; native registration validates font ids and ranges and explicitly converts GPU values to f32. The runtime stamps device scale and installs its layout policies after applying the overrides. Full native `tokens`/`tokens_fn` configurations retain precedence. +- **`assets.fonts` in the manifest** — declare bundled TrueType faces as `{ "id": 64, "path": "assets/mono.ttf" }`. Ids start at 64 and remain permanent; at most eight faces register before the first layout. Reference their ids through `tokenOverrides` typography. Missing, oversized, or invalid files fail startup. The same asset tree is included in packaging. - **`themeState(model: Model): ThemeState`** — the stock theme's model-owned pack, color scheme, and accent. Import `ThemeState` from `@native-sdk/core/events`; each field is optional (`pack`, `colorScheme`, `accent`). Omit a field to inherit app.zon, use `colorScheme: "system"` (or omit it) to follow the OS, and spell accents exactly `#rrggbb`. High contrast/reduced motion remain system-owned and high contrast suppresses accents. `themePack(model)` remains the pack-only compatibility helper; export one or the other, never both. Complete `tokens_fn`/`tokens` wiring takes precedence. Forced scheme applies to canvas tokens in v1; native chrome and WebViews remain OS-themed. - **`windows(model: Model): readonly WindowDescriptor[]`** — the model-declared secondary-window set. The launcher compiles `src/windows/