Skip to content

feat(swift-lang): add native Swift and SwiftUI support - #1295

Open
NandishwarSingh wants to merge 50 commits into
thesysdev:mainfrom
NandishwarSingh:feat/swift-lang
Open

NandishwarSingh wants to merge 50 commits into
thesysdev:mainfrom
NandishwarSingh:feat/swift-lang

Conversation

@NandishwarSingh

@NandishwarSingh NandishwarSingh commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

What

Resolves #393. Adds native Swift support as a SwiftPM package in packages/swift-lang with two libraries:

  • OpenUILang: the parser (batch + streaming), runtime (state, bindings, actions, Query/Mutation, error reporting), component library DSL, prompt generation, OpenUI Cloud config, Cloud's message format and edit-mode merge. Plain Swift + Foundation.
  • OpenUISwiftUI: an OpenUIRenderer view plus native SwiftUI views for react-ui's two libraries, openuiChatLibrary (84 components) and openuiLibrary (Stack root, Modal), so the same prompt or OpenUI Cloud setup that drives react-ui on web renders natively on iOS, iPadOS and macOS.

Right now an Apple app has to wrap the React runtime in a WebView or write its own parser. This gives Swift apps the same flow as the JS packages: define components, generate the prompt or Cloud config, render the stream.

I saw #393 had an earlier go-ahead back in June but nothing landed since, so I went ahead and built it. Fine to change direction on anything below.

Changes

  • OpenUILang is a port of lang-core, not a reimplementation. Lexer, statement splitter/auto-close, Pratt expression parser, materialize + schema validation, streaming parser, evaluator, store, query manager, prompt generation, generateSystemPrompt({ cloud: true }) and mergeStatements follow the TS file by file, so changes there map over directly. lang-core changed while this was open (lang-core: report plain objects in component slots as type-mismatch #1287, data in component slots) and that's ported too.
  • It keeps the JS semantics lang-core relies on without saying so: UTF-16 indexing, JSON.parse string unescaping (including its fallback for broken escapes mid-stream), Number to string formatting, toFixed rounding, loose ==, truthiness, undefined vs null, and JS object key order (prop order is argument order).
  • Swift libraries serialize to the same JSON Schema Zod produces, key order included, so a Swift library's toSpec() works with generateSystemPrompt on a server, and its Cloud config is byte for byte what lang-core sends.
  • OpenUIRenderer wraps a framework-free OpenUIRuntime (a port of useOpenUIState) and has react-lang's props: onAction, onStateUpdate, onParseResult, onError (same errors and timing as useOpenUIErrors, with enrichErrors' hints), queryLoader and publishObservability. Nodes keep their identity by statement name across streamed updates, so inputs keep focus while the response re-renders (the issue that came up on the Angular port). A finished response renders on its first frame.
  • The views follow react-ui rather than restyling it. I went through react-ui component by component and ported what was missing: chart tooltips, crosshairs and legends (tap a key to hide a series), react-ui's number formats, pie slices sorted like react-ui's, the image gallery mosaic and viewer, Prism-colored code blocks with copy, sliders with value fields and ranges, carousels with snapping and edge fades, hover states for pointers, keyboard moves in the editable table, and the theme's accent, per-chart palettes and status colors.
  • Also ported: react-ui's sentinelParser and message helpers for OpenUI Cloud's message format, @openuidev/observability (the runtime emits the same react-lang:stream events), and an MCP tool provider like passing an MCP client to react-lang's Renderer.
  • ChatComponents.swift and OpenUIComponents.swift are generated from react-ui by Scripts/generate-chat-components.mjs.
  • CI: a path-filtered build-swift.yml that builds/tests/lints on macOS and builds for iOS, and a Linux job that regenerates the fixtures from freshly built lang-core + react-ui and fails if anything changed, so the port can't silently drift.

Doesn't touch any JS package, the lockfile or shared tooling. No package.json in packages/swift-lang, so pnpm and the existing CI ignore it, and no changeset is needed.

One intentional difference, and it's a lang-core bug: a reactive input inside @Each or a ternary (e.g. @Each($items, it, Input("n", "p", "text", null, $v))) gets its binding re-evaluated in evaluatePropCore's plain-object branch and nested four levels deep, so typing writes undefined into $v. The Swift port keeps the binding intact and a test pins it. I can open a separate issue/PR for the TS side. The rest are in the README's differences section (no TeX, a small highlighter instead of Prism's grammars, tooltips on tap since touch has no hover, table cells that stay text fields, the gallery's fixed proportions).

Test Plan

  • Verified locally

  • swift test: 131 tests in 36 suites pass, Swift 6 strict concurrency, no warnings, swift format lint --strict clean. CI is green on macos-15 (Xcode 16.4) and the iOS build; I'm on Xcode 26.6 locally, so both toolchains are covered.

  • Conformance fixtures generated from lang-core/react-ui: parser and streaming checkpoints for the examples of both libraries plus edge cases (fences, CRLF, emoji, broken escapes, cycles, enum/type validation, data in component slots), set() sequences, evaluation, prompts, edit-mode merges, Cloud configs, Cloud's message format, error hints, chart number formats, and both libraries' schemas, JSON Schema and prompts byte for byte.

  • Real model output: 42 prompts covering every component family sent to OpenUI Cloud and replayed chunk by chunk, 1,770 chunks. Swift and lang-core produce identical parse results at every chunk through set() and push(), and identical evaluated trees.

  • Rendered every real response I have (42 from OpenUI Cloud, 42 from the planner app) at phone width and checked each one for layout that doesn't fill its measured height, which is how a few of the fixes in here were found (text cut off next to rows with spacers, horizontal bars spilling over their axis titles, cards squeezed to a word per line).

  • Fed it numbers a model can write but a layout can't use (1e999, a slider with a billion steps, a refresh interval of 1e999) and checked nothing crashes or hangs. That found three crashes and a hang, fixed in here with a test each.

  • Bug-fix tests were checked by putting the old behavior back and watching them fail (the edit counting, a closed Modal's spacing, tool error wording, text cut off at an exact height, the refresh interval crash, a long tag running off its block).

  • Ran a SwiftUI planner app on real Thesys responses on iPhone, iPad and Mac, with XCUITest flows for the gallery, chart tooltips/legends, dragging a range slider, the table keys, photos, payments and follow-ups. It hit a main-thread hang in long chats that I traced to LazyVStack transcripts re-measuring inside animations (not the package), so the README now says to use a VStack. After that, 196 replayed runs had no stall over 0.6 s.

  • Every commit builds, passes its tests and lints on its own. Fixture regeneration from a fresh build reproduces the committed fixtures byte for byte. JS side unchanged.

Demo

The planner app from the test plan on iPhone, iPad and Mac. These are real Thesys answers, replayed so the runs repeat, and the data in them comes from tools running on the device: Open-Meteo for the forecast, Apple Maps for places, Wikipedia for sights and photos, Razorpay's test checkout for payments. Sped up, with the waits cut.

The same chart answer on a Mac, an iPad and an iPhone

Same answer on a Mac, an iPad and an iPhone.

Hourly temperature and rain charts with tooltips, legend and tabs Sights in Rome as photo cards, then the map full screen A Kathmandu trip plan with hotels, sights, a map and a food photo viewer Paying a brunch deposit through Razorpay's test checkout
Chart tooltips, a legend key hiding a city, tabs Photo cards from Wikipedia, then the map full screen Trip plan with Apple Maps hotels and a photo viewer Razorpay checkout in test mode, then the model confirms

On the Mac the chart follows the pointer:

The chart answer on the Mac, with tooltips following the pointer

The Kathmandu trip on the Mac, with the photo viewer Rome's sights on an iPad in landscape
Mac: the trip, and the photo viewer on arrow keys iPad: Rome's photo grid, and the map as a sheet
More: Mac Rome, iPad charts, Rome on all three Rome's sights on a Mac, an iPad and an iPhone
Rome's sights as a photo grid on the Mac, and the map in a sheet The chart answer on an iPad in landscape

The app lives on a branch of my fork so this PR stays package-only: examples/app-frameworks/swift-planner. It has its own README (with more clips and screenshots) and UI tests. I can open it as its own PR, like the Angular example, if you want it in the repo.

Screenshots

Top two rows are rendered offscreen on macOS at 440pt: the chat library's dashboard example in light, dark and 45% into the stream, then the trip form example, the Paris guide example and a sheet of the charts. Below that, iPhone simulator shots from the UI test runs (gallery, viewer, chart tooltips) and code blocks in dark mode.

Checklist

  • I linked a related issue, if applicable
  • I updated docs/README when needed
  • I considered backwards compatibility

Where to look

It's big, so here's the order that makes sense:

  1. Sources/OpenUILang/Parser.swift, Materialize.swift, Validation.swift, Evaluator.swift: the core of the port, side by side with their TS files.
  2. Tests/OpenUILangTests/ParserConformanceTests.swift + Scripts/generate-fixtures.mjs: how conformance is proven.
  3. Sources/OpenUILang/Runtime.swift and Sources/OpenUISwiftUI/OpenUIRenderer.swift, OpenUIContext.swift, OpenUINode.swift: the runtime and renderer.
  4. Sources/OpenUISwiftUI/Components/*: the views.

The schemas files and the JSON fixtures are generated (marked in .gitattributes, so GitHub collapses them). The commits go roughly in that order, then one per react-ui feature, if you'd rather go commit by commit. I can also split it into separate PRs (e.g. OpenUILang first, then the SwiftUI renderer).

Two things for you to decide:

  • SwiftPM only resolves packages at a repo root, so apps can't add this by git URL yet. Options are a root Package.swift pointing at packages/swift-lang, or mirroring it to a separate openui-swift repo. The README says to add it as a local package for now.
  • Whether you want the planner app (see Demo) in examples/ as a follow-up PR, or something smaller.

@vercel

vercel Bot commented Oct 5, 2026

Copy link
Copy Markdown

@NandishwarSingh is attempting to deploy a commit to the thesys-devs Team on Vercel.

A member of the Team first needs to authorize it.

@vercel

vercel Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
openui-docs Ready Ready Preview Oct 5, 2026 5:24pm UTC

Request Review

Ports lang-core's parsing pipeline to a Swift package (OpenUILang): lexer,
statement splitter and auto-close, Pratt expression parser, schema-aware
materialization and validation, the batch parser and the incremental
streaming parser.

The port keeps the JavaScript semantics the TypeScript code relies on:
UTF-16 indexing, JSON.parse string unescaping (with its fallback for
malformed escapes), Number to String formatting, loose equality,
truthiness, undefined versus null, and JavaScript object key order.

Conformance fixtures are generated from @openuidev/lang-core by
Scripts/generate-fixtures.mjs: 55 inputs (the chat library prompt examples
plus edge cases) for batch parsing, streaming checkpoints for each, and
StreamParser.set() sequences. The Swift tests compare against them.
Ports the runtime half of lang-core and react-lang's useOpenUIState to
OpenUILang:

- the expression evaluator (operators, builtins, @each, action steps,
  reactive assignments) and schema-aware prop evaluation
- the state store, state fields and form rule validators
- the query manager (caching, refresh intervals, mutations, MCP results)
  behind a ToolProvider protocol
- OpenUIRuntime, which ties the streaming parser, store, evaluation, form
  fields and action execution together for UI layers to observe

Evaluation fixtures generated from lang-core cover operators, builtins,
action plans, ternary components, bindings, query results and every chat
library example.

One intentional difference: lang-core re-evaluates the binding of a
reactive input rendered inside @each or a ternary, nesting it four levels
deep, so typing writes undefined into the bound $state. The Swift port
keeps the binding intact; RuntimeTests pins the expected behaviour.
…Swift

Adds the library side of OpenUILang:

- PropType/Prop/ComponentSchema to describe components with typed props,
  $binding props and defaults; schemas serialize to the same JSON Schema
  Zod's toJSONSchema produces, and signatures match buildSignature
- Library, which compiles the parser's parameter map, answers binding
  lookups for evaluation, and exports a LibrarySpec a server can pass to
  lang-core's generateSystemPrompt
- generatePrompt, a port of lang-core's prompt assembly

Fixtures check the prompt text byte for byte against lang-core across
option combinations (tools, bindings, edit and inline modes, groups), and
check Swift-defined chat components against the Zod schemas.
ChatComponents defines all 84 openuiChatLibrary components in the Swift
DSL, with the same names, prop order, types, defaults, $binding props,
descriptions and prompt groups. Array props gain an optional minItems,
which the card blocks use.

Tests compare every schema with the Zod output, every signature with
lang-core's, and the full chat prompt generated from the Swift library
with lang-core's, byte for byte.
Adds OpenUISwiftUI:

- OpenUIRenderer, a view that renders a (streaming) response with a
  component library, backed by an observable OpenUIContext around
  OpenUIRuntime; node identity follows statement names so inputs keep
  focus and local state while the response re-renders
- OpenUIChatLibrary, native SwiftUI views for every openuiChatLibrary
  component: content, Swift Charts (bar, line, area, horizontal bar, pie,
  radial, stacked, scatter, plus a drawn radar chart), tables and the
  editable table, forms with validation, chips and option cards, buttons,
  lists, tabs, accordions, sections, carousels and the card blocks
- lucide icon names mapped to SF Symbols with react-ui's category fallbacks
- card block clicks that merge item context into the action like react-ui's
  withItemContext

Tests cover library completeness, card actions, icon mapping, node
identity, context updates and an offscreen end-to-end render of every chat
library example.
Edit mode's mergeStatements and generateSystemPrompt({ cloud: true }) (with
the library checks Cloud applies) are now in OpenUILang, both checked
against fixtures generated from lang-core.

Library JSON Schemas now match Zod's key order exactly: top-level properties
in library order, $defs in Zod's depth-first discovery order, and minItems
before type. The chat schemas are listed in openuiChatLibrary's order, so
the full chat library schema and its Cloud config are byte-identical to
lang-core's output.
…enderer

OpenUIRuntime gains onError and onParseResult, ported from react-lang's
useOpenUIErrors: parser errors carry enrichErrors' hints, a response with
no root reports parse-failed, failed Query/Mutation calls are included, and
errors are reported once streaming ends and cleared when the next response
starts. OpenUIRenderer passes both through and takes a custom queryLoader
view, defaulting to a small spinner like the React renderer.

Hints are checked against lang-core's enrichErrors, and the parser fixtures
now include a response in OpenUI Cloud's content/end marker format.
A failed query recovers on refetch and clears its error, and a rendered
text field stays the same AppKit view while statements stream in after it
and while typing writes its $binding (the focus loss found in the Angular
port's review).
ChatComponents now includes openuiChatPromptOptions (examples and
additional rules), so a Swift app prompts exactly like a react-ui app: the
full chat prompt and the chat Cloud config are byte-identical to
react-ui's. ChatComponents.swift is generated from openuiChatLibrary by
Scripts/generate-chat-components.mjs.
Rendered every chat example and 42 real OpenUI Cloud responses with both
react-ui (in Storybook, at the same width) and the SwiftUI renderer, and
ported the differences:

- Behavior: Tabs follow the streaming tab until the user picks one,
  Accordion opens the newest item, SectionBlock opens sections as they
  stream and folds back to the first when the response ends.
- Layout: card blocks and OptionCards use react-ui's responsive grid (rows
  of up to 2 or 3, two columns at 768pt and below, one at 480pt), clickable
  cards get chevrons, snippet cards are label/value rows, visual cards put
  the text panel over the photo, and the root Card has no frame.
- Text: TextContent renders full markdown with [n] citations resolved
  against the card's sources, tables, line breaks and $$ math (as source);
  only ~~double~~ tildes strike through.
- Forms: required fields get an asterisk, DatePicker shows a placeholder
  until something is picked, Slider shows min/max labels, Select is a
  full-width field, switches lead their labels, and inputs use react-ui's
  field look.
- Data: tables fill the width with tinted alternate rows, page like
  react-ui and show a placeholder while a Query fills them; FollowUpBlock
  shows "Related Queries"; sources show as favicon cards; tags use
  react-ui's corners and padding.
- Charts: react-ui's default palette and curves (monotone by default, no
  point markers), horizontal grid lines, rounded bars, gradient area fills
  and scatter axes fitted to the data.

Theme defaults follow react-ui's spacing, radii, fills and borders.
Ports thesysdev#1287: a string, number, boolean, object or array in a slot that
only accepts components is a type-mismatch and is pruned, and  that
aren't listed as components (zod's hoisted __schema0 shapes) don't count
as components. The new lang-core cases are in the fixtures.
Covers installing the package as a local Swift package, defining
components and libraries, generating prompts, rendering streamed output,
the chat library, tool providers, the differences from lang-core, and how
to regenerate the conformance fixtures.
Runs on changes to packages/swift-lang, lang-core sources or the chat
library: builds, tests and lints the package on macOS, and regenerates the
conformance fixtures from the TypeScript packages to fail when the Swift
port and lang-core drift apart.
The renderer's content was a Group, and a Group's modifiers apply to its
children. Before the first parse there are none, so inside a stack or a
lazy list the initial onChange never ran and a response that doesn't change
afterwards (a restored chat message) stayed blank. Streaming hid it, since
each new chunk triggers an update.
- Charts use react-ui's motion: bars grow in, lines and areas draw from
  left to right, pie/radial/radar/scatter fade in, and values glide to new
  positions on the dataMorph spring (stiffness 480, damping 44) as data
  streams in. Points are keyed by series and label so they move rather
  than redraw.
- Newly streamed nodes fade in; nodes keep their identity, so nothing else
  re-animates. Tabs, accordions and sections animate when they follow the
  stream.
- Charts that have no data yet and loading images show react-ui's pulsing
  skeleton; images fade in when they load.
- Card grids are a Layout that reads the offered width, so they lay out
  correctly on the first frame instead of re-flowing.

Everything respects Reduce Motion.
The renderer parsed the response in onAppear/onChange, which run after
the first layout. A finished response (a restored message, or one
rebuilt when it scrolls back into view) laid out empty for a frame and
then jumped, and a transcript scrolled to the end landed short of it.

The context now preloads the response when it's created: it parses and
seeds the store, but reports nothing and runs no queries, so callbacks
and queries still start on the first update as before. The renderer
keeps the context in a StateObject, whose initializer is an autoclosure,
so the parse runs once per renderer and not on every init of the view.
OpenUITheme gets onAccent (react-ui's textAccentPrimary) and
chartPalette (react-ui's defaultChartPalette). Primary and icon buttons,
option rows, list actions, steps, icon badges and the editable table's
save button use the theme's accent and onAccent instead of the app's
accentColor, and the renderer tints controls with the accent. Charts
pick from chartPalette the same way they pick from the default ramp; an
empty palette falls back to the default.
The slider follows SliderBlock: the label row shows its values as number
fields (a menu of steps for discrete sliders, up to 200), typed values
get react-ui's messages ("Invalid number", "Value must be between X and
Y", "Min must be less than max"), and two values make a two-thumb range
slider that works with drag, VoiceOver and the arrow keys.
Carousel and the carousel card blocks share one scroller: items snap to
the leading edge, an edge fades out while there's more to scroll that
way (40pt, 48pt for card blocks), and Carousel gets react-ui's step
buttons. Card widths come from containerRelativeFrame instead of a
measured width, so the first frame no longer lays out at the wrong width
and jumps. The fades and buttons read the scroll geometry but never
change the layout.
Ports react-ui's sentinelParser and the message builders its
GenUIAssistantMessage uses: split a stored message into the program and
its context, wrap content and context, build the follow-up and form
state messages react-ui sends, restore form state from a message, and
read ]]>openui:artifact headers. Offsets are UTF-16, as in JavaScript.

generate-fixtures.mjs imports sentinelParser.ts directly (Node strips
the types) and writes messages.json, which the new tests check the port
against.
CodeBlock uses Prism's vscDarkPlus colors and gets react-ui's copy
button (shown on hover on the Mac, as on the web). Fenced code in
Markdown keeps its language, uses vscDarkPlus or oneLight with the color
scheme and is trimmed like react-ui's; image lines render as images.

Highlighting comes from a small tokenizer for common languages rather
than Prism's grammars.
ImageGallery lays out the first five images in react-ui's grid
templates, the first one large, with its narrow templates below 520pt
(react-ui switches on a 768px viewport, and a chat column is narrower
than its window). Past five images it shows a Show All button. Tapping
an image opens a viewer with one image per page (swipe, or the arrow
keys), react-ui's thumbnail strip, and the image's details under it.

react-ui sizes the grid from the loaded images, up to 376px. The mosaic
uses a fixed shape instead so it doesn't jump when the images arrive.
Covers the theme's accent and chart palette, the OpenUI Cloud message
helpers, the highlighter's limits, and laying out a chat transcript in a
VStack: a LazyVStack re-measures rows inside animations (an animated
scrollTo, the keyboard closing), which can block the main thread for
seconds.
The message fixtures are generated from sentinelParser.ts, so a change
there can make them stale without touching the paths the workflow
watched.
- Hovering with a pointer or tapping shows react-ui's tooltip, with each
  visible series' value in its tooltip number format, and its crosshair:
  a band behind bars, a dashed line with dots on lines and areas. A tap
  rather than a drag picks, so a chart never takes over scrolling.
  Tapping the same category closes it, and opening another closes it.
- Legends are react-ui's DefaultLegend: centered keys that fold behind
  "N more" when they don't fit on one line. Tapping a key hides that
  series, never the last one, and colors stay put.
- Value axes sit on the leading side with react-ui's tick format (1.3K,
  2.5M). Axis titles go above and below the plot, as in react-ui's
  condensed charts, instead of inside it, where an empty one still took
  room.

Fixes found on the way: pie and radial slices are sorted largest first,
as react-ui sorts them, and the single stacked bar now matches react-ui's
SegmentedBar (ramp colors in input order, a 20pt track with gaps, shares
in the legend, a value and share tooltip).

The number format fixtures come from react-ui's formatters, so the Swift
workflow also runs when those change.
OpenUILibrary.library renders react-ui's openuiLibrary, for apps that
aren't a chat. It shares the chat library's views and adds:

- Stack, the root: a flex row or column with react-ui's gap tokens,
  align, justify and wrap. Cards in a row share the width like react-ui's
  flex: 1 cards; other views keep their size and shrink in proportion.
- Card with Stack's layout props and its card, sunk and clear variants.
- Modal: a sheet its $boolean open binding shows. Closing it (the X,
  Escape, a swipe) writes false back, like react-ui's useStateField.

The schemas (OpenUIComponents) come from the same generator as the chat
ones and reuse ChatComponents for the 76 components both libraries define
the same way. The schema tests now cover both libraries against Zod,
including the full JSON Schema and both prompts byte for byte, and the
general library's examples join the parser, streaming, evaluation and
end-to-end render fixtures.
react-lang's Renderer takes an MCP client directly as its toolProvider
and unwraps each callTool result. McpToolProvider does the same for a
closure that returns the client's raw result, so error results become
McpToolErrors (reported as mcp-error) and structuredContent or JSON text
becomes the query's data. A closure keeps the package free of any
particular MCP client.
Observability.shared ports @openuidev/observability: sinks listen by
level or to everything, and emitters skip building events when nothing
listens. The runtime publishes react-lang's react-lang:stream events with
the same fields (a stable id, phase, updateIndex, the response, parser
metadata, errors and timings), so a sink written for the web, like
observability-cloud, reads them unchanged. A settled response with errors
is an error-level event.

StreamObservation ports advanceStreamingObservability, and its tests are
react-lang's, case for case. publishObservability: false on the renderer
turns the events off, like react-lang's Renderer prop.
On the Mac a focused text field writes its text back when editing ends,
so clicking or tabbing through cells stored their unchanged values as
edits and the table offered to save changes nobody made. Text equal to
the data is no longer an edit, and typing the original back undoes one.
The bookkeeping moves into TableEdits so it can be tested on its own.
react-ui's EditableTable takes spreadsheet keys. With a hardware
keyboard, Up and Down now move between rows in the same column, and
Enter keeps the edit and moves down; Tab already moved across. Cells
stay text fields rather than react-ui's select-then-edit, which suits
touch. Escape isn't ported: a focused Mac text field keeps showing its
typed text after the value is reverted, so it would look undone without
being undone.
OpenUITheme gets react-ui's per-chart palettes (barChartPalette,
lineChartPalette and the rest). Each chart uses its own, then
chartPalette, then the default ramp, as react-ui's resolvePalette does,
with an empty palette counting as none. The single stacked bar uses the
bar palette, as react-ui's does.

It also gets info, success, alert and danger colors for callouts, tags,
icon badges, trends, form and slider errors and the code block's copied
state, which used hardcoded system colors. The defaults are those same
colors.
With a pointer (on the Mac, or an iPad with a trackpad), controls now
react the way react-ui's do: clickable cards sink a little and visual
cards zoom their photo, list rows grow their marker, chips fill in,
unselected option cards darken their border, empty radios and check boxes
fill, inactive tabs brighten and sources highlight.

A HoverButtonStyle tracks the pointer and passes it to the label through
the environment, so each control draws its own hover state. Touch is
unchanged.
The section on differences covered lang-core and a few rendering limits.
It now also covers the choices made for native platforms: tooltips on a
tap without hover, editable table cells that stay text fields, and the
gallery's fixed mosaic proportions.
Query and Mutation errors interpolated the thrown error, which for a
Cocoa error prints its debug form: a location lookup that hadn't got a
fix yet reported "Error Domain=kCLErrorDomain Code=0 "(null)"" to onError
and to the user. They now use what lang-core's err.message corresponds
to: a LocalizedError's description, a Cocoa error's localized
description, and otherwise the error's own description as before.
With responsive set to false, react-ui keeps up to three cards per row
and lets each card's minimum width (196px for context cards, 158px for
overview cards, 15rem for composite cards) overflow its column. The port
had no minimum, so on a phone three context cards squeezed to a word or
two per line with hyphens. The grid now drops to as many columns as fit
at the card's minimum width, keeping react-ui's breakpoints otherwise.
Primary buttons set their label to the theme's onAccent color. The
system draws a disabled prominent button, and every prominent button in
an inactive Mac window, gray instead of in the accent, which left white
text on light gray. Those now keep the system's label color.
Offered exactly its ideal height (a fixed frame, a self-sizing table
cell), a response's VStack split that height by flexibility. A row with a
Spacer (a list item, callout, check box or key/value row) counts as very
flexible, so wrapping text above it was offered too little and cut off
mid-sentence, and the leftover height stayed empty. Each node now takes
its ideal height for its width. In a scroll view, which offers unlimited
height, nothing changes.
A horizontal bar chart needs a row per category, so it asked for at least
28pt each, but its frame was fixed at the chart height. With more than
about eight categories the bars overflowed the frame above and below,
over the axis titles and the value labels. The frame now grows to the
chart's minimum instead.
Bar, line and area charts squeezed every category label into its own
slot, so on a phone eight city names read "Mum…", "Kolk…" and a burndown's
"Sprint 1" to "Sprint 10" all read "Sprin…". react-ui's condensed axis
draws every n-th label once a category is narrower than its label
(counting at most 40px) plus an 8px gap, and gives each the room of the
ones skipped. The port now does the same: each label is centered under
its category, and the first and last are cut at the ends of the plot and
moved inside it, as react-ui's CondensedXAxis keeps them.
…hem off

react-ui's rule only thins labels until each has 40px, so longer ones
still truncate: a cohort chart's seven "Month n" labels read "Month…". The
axis now keeps thinning while labels would be cut off, as long as three
remain, and draws them in the font they're measured in, which also stops
labels that fit from being truncated. The README lists this with the
other differences from react-ui.
Numbers in a response can be infinite or NaN (1e999, 0/0) or far larger
than a view can draw, and a few of them reached conversions that trap or
loops that never end: a Query refresh interval of 1e999, a TextArea with
1e999 rows and a discrete Slider up to 1e999 crashed; a discrete Slider
with a billion steps hung, building its menu options, and on the Mac
laying out a tick mark per step. Bounds and sizes that aren't finite now
fall back to their defaults, refresh intervals stop at setInterval's
longest delay, step menus give up past 200 options without building
them, and a slider with more than 100 steps snaps its own value.
A drag's translation is measured from where it began, but each update
added it to where the thumb had already moved to, so the thumb ran ahead
of the finger and hit the other end of the range. The drag now starts
from the thumb's position when it began.
A tag wider than its container was laid out at its full width and ran off
the card. Wrapping rows now give a child wider than a row the row's width,
and tags stay on one line, cut short with an ellipsis, as react-ui's
`max-width: 100%` and `text-overflow: ellipsis` do.
Library validation built codes and paths for its issues that only ever
became one message, the generated openuiLibrary schemas aliased a rules
object none of them use, and two views kept a value they never read.
Each end of a range DatePicker could be set past the other, storing a
`to` before its `from`. Each end is now bounded by the other, as react-ui's
range calendar does.
A response can bind a card's text to values that come out empty, most
often by using an @each loop variable in a separate statement, which
can't see it. The card then drew an empty white panel over its photo.
It now shows just the photo; the README lists this with the other
differences from react-ui, which draws the empty panel.
Pretty-printed, the fixtures generated from the TypeScript packages took
about 39k lines, most of this package's diff, for data nobody reads
value by value. They're now one case per line: still JSON with the same
contents, still diffable case by case, and the CI job that regenerates
them still compares byte for byte.
Card blocks laid out as a carousel had no step buttons, as in react-ui, so
on a Mac with a mouse the cards past the edge were out of reach: the wheel
scrolls up and down, and there's no shift-scrolling or scrollbar to fall
back on like in a browser. Show the Carousel's buttons while a pointer is
over the row.

This branch was successfully deployed

1 active (outdated) deployment
Preview — 5d444cd8 Deployed Oct 5, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Native Swift Integration

1 participant