@exponential_app/eui-solid

中文文档
0.21.0发布于 2026/11/10Apache-2.0TypeScriptES Module构建来源已验证近 30 天下载 5553

genUI for SolidJS: the core A2UI catalog painted on plain elements (floating-ui, aria-hidden) with the same DOM, the same compiled stylesheet and the same runtime themes as the React renderer, run through the conformance suite and the web-parity check.

原始使用文档
文档目录7 节
  1. The invariant
  2. The QrScanner camera ( qrScanner host option)
  3. Layout
  4. Primitives
  5. State attributes: the porting rule
  6. Commands
  7. Hosted popups (VAPP-281)

@exponential_app/eui-solid: the genUI SolidJS renderer

The SolidJS renderer of genUI (FEED-128): an A2UI surface in the core catalog (@exponential_app/eui) painted with the identical DOM of the React renderer (@exponential_app/eui-react), with the same compiled stylesheet and runtime themes. Overlays, menus, selects, tabs, accordions and the form controls paint Radix's DOM on plain elements, on a headless layer (src/behaviors/) that reproduces Radix's machinery: positioning on @floating-ui/dom, modality on aria-hidden. It depends on @exponential_app/eui (the shared web layer @exponential_app/eui/web), solid-js, @floating-ui/dom, aria-hidden and lucide-solid. It imports nothing from the React renderer and nothing from the Exponential app.

The invariant

For every surface the Solid painter produces exactly what React paints:

  • the same elements and attribute set, with the same values: the class strings in the same order, data-xui-id, data-xui-part, data-xui-c, data-r-*/data-m-*, data-xs, aria-*;
  • the same <style data-xui-style="theme"> (BASE_CSS + the compiled theme) and <style data-xui-style="nodes">;
  • the same root (.xui-surface <scope> xui-s-<id>, data-xui-mode, dir, data-xq) and the same overlay, toast and live-region layers.

src/test/parity.ts (expectDomParity) renders one tree in both renderers in jsdom and compares the serialised DOM (attributes sorted, generated ids renamed). Every native port adds its cases to src/parity.test.ts. A browser-level web-parity suite also compares computed styles, frames and pixels at zero tolerance.

import { ExponentialSurface, createSurface } from "@exponential_app/eui-solid"

function Chat(props: { messages: A2uiMessage[] }) {
  const surface = createSurface({ surfaceId: "main" })
  createEffect(() => props.messages.forEach(surface.apply))   // A2UI v0.9 server messages
  return (
    <ExponentialSurface
      surface={surface}
      theme="exponential"                     // or "neutral" | "playful" | "orange" | a theme JSON | a ResolvedTheme
      mode="system"                           // light | dark | system
      locale="de-AT" timeZone="Europe/Vienna"
      host={{ icons, onAction, onInput }}     // the host plugin (lucide-solid icons)
      handleRef={(h) => (handle = h)}         // handle.run({ focus: { id } } | { announce } | { scrollIntoView } | { scrollToIndex })
    />
  )
}

The props are the same as React's ExponentialSurfaceProps, with these differences:

  • class/style take Solid's form (kebab-case style keys).
  • handleRef is a callback that receives the SurfaceHandle.
  • The React hooks have Solid names: useSurface is createSurface, useHostOwnedValue is createHostOwnedValue, useParts is parts, useField is createField, useMediaSource is createMediaSource, useHostSurface is createHostSurface, and defineReactExtension is defineSolidExtension.
  • React's useWriteEpoch(ctx) is the live getter ctx.writeEpoch: hand createHostOwnedValue epoch: () => ctx.writeEpoch so a programmatic write (an action set, ctx.assignData) wins over a focused field.

The QrScanner camera (qrScanner host option)

The camera is the host's (VAPP-162, docs/round-5-contract.md §7), the same hook as React's:

host={{
  qrScanner: {
    // Render the live feed into `feed` (an empty element filling the
    // viewfinder; append a <video>), decode codes of `formats` (default
    // ["qr"]) and call onValue per decoded string. Return the stop.
    start(feed: HTMLElement, formats: readonly string[], onValue: (value: string) => void): (() => void) | false | void
  },
}}

onValue dispatches scanned {value} once per distinct value (a code held in view reports once; a remount or re-enable reports it again) and announces it politely. The stop runs on unmount, on disabled and when formats change. No qrScanner, start returning false or throwing = the placeholder (ui-scan + Camera unavailable), no scan line.

Layout

pathwhat
src/surface.tsx<ExponentialSurface>: theme resolution, density/contrast, the measured box + breakpoints, the two <style>s, the container, layers, live regions, the handle
src/create-surface.tscreateSurface: the reducer as a Solid store (tree and data in two signals)
src/node-view.tsxthe bind pass as per-node memos, painter lookup through <Dynamic>, NodeList (children keyed by id) and TemplateItems (keyed by template.key), runNodeAction, the paint boundary
src/context.tsthe surface context (an object of getters; deriveContext re-points fields), scope + instance accessors
src/natives/shared.tsx (parts(), the porting rules), bound.ts, the painters (overlays.tsx, select.tsx, dates.tsx, nav.tsx, inputs.tsx, qr.tsx…), index.ts (the 51 keys)
src/behaviors/the headless Radix machinery the natives and src/primitives/ share, one concern per module: portal, presence, dismissable-layer, focus-scope, focus-guards, scroll-lock, modal, popper, menu, roving-focus, listbox, typeahead, collapsible, picker-popover, bubble-input, slider, grace-area, visually-hidden, drag-to-dismiss, style-writer, element-size, image-loading-status, search-match (index.ts names the Radix primitive each reproduces). Nothing in it reads the surface context; package-internal
src/primitives/the React primitive set on Solid (FEED-131), one module per React module but the six app-specific ones, on the shared recipes of @exponential_app/eui/web/primitives and on src/behaviors/; index.ts = ./primitives; bun run build:css → dist/exponential-ui-solid.css
src/style.tscss(): React's inline-style semantics (camelCase, px for numbers, unitless list)
src/icons.tsxthe fallback glyphs on lucide-solid, painted with lucide-react's exact <svg> attributes
src/form.tsx, list.tsx, markdown.tsx, media.ts, inputs.ts, platform.ts, extensions.ts, host*.ts*the React files' Solid twins
harness/the same page protocol as React's harness (dev:harness serves on :4182, built with Vite + vite-plugin-solid)

Primitives

@exponential_app/eui-solid/primitives is the Solid twin of @exponential_app/eui-react/primitives (FEED-131): button, dialog, select, tabs, table, command, toast, calendar and the rest, with the same DOM and classes. The class strings come from ONE source, the recipes of @exponential_app/eui/web/primitives; neither set holds a class literal.

import "@exponential_app/eui-solid/styles.css"          // the compiled primitives sheet
import { globalsCss } from "@exponential_app/eui/web"
import { Button, Dialog, DialogContent, DialogTitle, DialogTrigger } from "@exponential_app/eui-solid/primitives"
  • styles.css is dist/exponential-ui-solid.css, compiled by bun run build:css from the React package's tailwind.css: byte-identical to @exponential_app/eui-react/styles.css (src/primitives/styles.test.ts).

  • Inside a surface the primitives wear its theme. Outside one, globalsCss(theme) writes the theme as shadcn variables (:root + .dark).

  • The names follow Solid: class for className, as for asChild (the element or component the part paints as), create* for the use* hooks.

  • React's SegmentedControl has no embedded arm either: the glass-group row is the Exponential app's (@exp/ui), like its other own components.

  • The web-parity gate compares every specimen of @exponential_app/eui/fixtures/primitive-specimens.json with React's over the whole page body: bun run --filter @exponential_app/eui-solid test:parity:primitives.

  • Round 6 (EXP-1495) adds Outline, Mosaic, CarouselCounter and CarouselThumbnails (React's API with class), specimens plain/outline, plain/mosaic, plain/carousel-extras; the natives' Carousel counter + thumbnails are React's markup (src/round6.test.tsx, media.parity).

State attributes: the porting rule

The theme CSS (STATE_SELECTORS) targets Radix's attribute names, so the Solid painters write the Radix-named attributes on the parts themselves. The package was first painted on Kobalte, which names the same states differently; the dependency is gone, and the Kobalte column stays as the map for porting code written against Kobalte (or any non-Radix library). The full rules are in src/natives/shared.tsx.

stateRadix (React, the theme CSS)Kobalte (history)the Solid painter writes
checked / uncheckeddata-state="checked|unchecked"data-checkeddata-state
open / closeddata-state="open|closed"data-expanded, data-closeddata-state
selected tabdata-state="active|inactive"data-selecteddata-state
pressed toggledata-state="on|off"data-presseddata-state
placementdata-side, data-align(none, style only)data-side, data-align
highlighted itemdata-highlighted=""data-highlighteddata-highlighted=""
disableddata-disabled=""data-disableddata-disabled=""

Commands

bun run --filter @exponential_app/eui-solid typecheck
bun run --filter @exponential_app/eui-solid test          # jsdom: surface, natives × catalog, DOM parity with React
bun run --filter @exponential_app/eui-solid dev:harness   # http://localhost:4182/?view=kitchen-sink&theme=orange
bun run --filter @exponential_app/eui-solid test:conformance  # the shared web runner → .conformance/exponential-ui-solid.json
bun run --filter @exponential_app/eui-solid test:parity       # web parity vs React → .conformance/exponential-ui-parity-react-solid.json
bun run --filter @exponential_app/eui-solid test:parity:primitives  # the primitive specimens only (seconds)
bun run --filter @exponential_app/eui-solid test:browser      # every headless-Chromium suite (overlays, inputs, tables…)
bun run --filter @exponential_app/eui-solid build:css         # dist/exponential-ui-solid.css (after ui-react's build:css)
bun run --filter @exponential_app/eui-solid shots             # the kitchen sink → /tmp/feed128/solid-shots/ (--theme, --mode, --out)

Hosted popups (VAPP-281)

As React: host.popupHost = { mount(popup), unmount?(popup) } hosts anchored Popovers and Menus outside the surface (docs/round-7-contract.md).

版本与发布记录

@exponential_app/eui-solid 共发布 29 个版本,首次发布于 2026/10/30;过去 12 个月发布了 29 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.21.0latest2026/11/103 天前
0.20.02026/11/094 天前
0.19.02026/11/086 天前
0.18.02026/11/077 天前
0.17.02026/11/067 天前
0.16.22026/11/068 天前

关于 @exponential_app/eui-solid 的常见问题

@exponential_app/eui-solid 是什么?

genUI for SolidJS: the core A2UI catalog painted on plain elements (floating-ui, aria-hidden) with the same DOM, the same compiled stylesheet and the same runtime themes as the React renderer, run through the conformance suite and the web-parity check.

如何安装 @exponential_app/eui-solid?

在项目目录运行 npm install @exponential_app/eui-solid,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:solid-js(^1.9.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@exponential_app/eui-solid 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@exponential_app/eui-solid 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@exponential_app/eui-solid 可以商用吗?

@exponential_app/eui-solid 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@exponential_app/eui-solid 的下载量有多少?

根据 npm 官方下载统计,@exponential_app/eui-solid 在 2026/10/14 – 2026/11/12 这 30 天内被下载 5,553 次,日均约 185 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@exponential_app/eui-solid 还在维护吗?

最新版本 0.21.0 发布于 2026/11/10(3 天前);至今共发布 29 个版本,其中过去 12 个月发布了 29 个;首次发布于 2026/10/30。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。