{"_id":"@1game/solid-ui","_rev":"7-b3ae8db736a7a1cf1025619aa960c916","name":"@1game/solid-ui","dist-tags":{"latest":"1.21.0"},"versions":{"1.17.0":{"name":"@1game/solid-ui","version":"1.17.0","author":"linlinfaxin@163.com","license":"MIT","_id":"@1game/solid-ui@1.17.0","maintainers":[{"name":"linfaxin","email":"linlinfaxin@163.com"}],"dist":{"shasum":"bf84b856558785d677688c1d0d5b70b8adac2ecd","tarball":"https://registry.npmjs.org/@1game/solid-ui/-/solid-ui-1.17.0.tgz","fileCount":207,"integrity":"sha512-SAt3K2AftNOzh0bsFQjGdgfzUEY8oS9sfmf7YU99RwDu/GUlpdGTpYpRDip85Tl7QNtpqq6Kw3hKcvDTBqkKow==","signatures":[{"sig":"MEUCIQCRlr26ZFFqdjzI10PAJZpZvt9qFJe+KXIxjMlY5SqsxAIgWx3Ff6T/CBFSQDqiACZ7TS7/nxf7ZndTuoK/vUY/7VE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":349819},"main":"lib/index.js","type":"module","types":"lib/index.d.ts","exports":{".":{"types":"./lib/index.d.ts","default":"./lib/index.js"}},"scripts":{"test":"vitest run","build":"pnpm run clean && tsc","clean":"rimraf lib","prepublish":"pnpm run build","test:watch":"vitest"},"_npmUser":{"name":"linfaxin","email":"linlinfaxin@163.com"},"description":"Headless UI primitives for 1Game Engine (controlled, unstyled)","directories":{},"_nodeVersion":"22.14.0","dependencies":{"@1game/engine-bundle":"1.17.0","@1game/solid-popover":"1.17.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"rimraf":"^6.1.2","vitest":"^1.0.0","solid-js":"^1.9.0","typescript":"^5.9.3","@1game/solid-dsl":"1.0.0","vite-plugin-solid":"^2.11.11","@1game/render-data":"1.0.0"},"peerDependencies":{"solid-js":"^1.9.0"},"_npmOperationalInternal":{"tmp":"tmp/solid-ui_1.17.0_1786464866085_0.5117458311961471","host":"s3://npm-registry-packages-npm-production"}},"1.17.1":{"name":"@1game/solid-ui","version":"1.17.1","author":"linlinfaxin@163.com","license":"MIT","_id":"@1game/solid-ui@1.17.1","maintainers":[{"name":"linfaxin","email":"linlinfaxin@163.com"}],"dist":{"shasum":"db8a23ed6d5db0fe4314e4beb4010c11d6e90827","tarball":"https://registry.npmjs.org/@1game/solid-ui/-/solid-ui-1.17.1.tgz","fileCount":207,"integrity":"sha512-V/j7P2jUr0M7sLUSlx8rGK9vabvrg2fKVeIahvzIVpcxMiYJnj4L3omZM4JoMgadaTd7lEwg38DiHs+/H3aTug==","signatures":[{"sig":"MEUCIQCr+868dG0AFDfSdTygZe2IQhJtws/bphCulD2h9Iu75QIgT6kxMPO47RdcpjzR5psPH/YNj/cTfwKYPlc0M/N3ahc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":349819},"main":"lib/index.js","type":"module","types":"lib/index.d.ts","exports":{".":{"types":"./lib/index.d.ts","default":"./lib/index.js"}},"scripts":{"test":"vitest run","build":"pnpm run clean && tsc","clean":"rimraf lib","prepublish":"pnpm run build","test:watch":"vitest"},"_npmUser":{"name":"linfaxin","email":"linlinfaxin@163.com"},"description":"Headless UI primitives for 1Game Engine (controlled, unstyled)","directories":{},"_nodeVersion":"22.14.0","dependencies":{"@1game/engine-bundle":"1.17.1","@1game/solid-popover":"1.17.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"rimraf":"^6.1.2","vitest":"^1.0.0","solid-js":"^1.9.0","typescript":"^5.9.3","@1game/solid-dsl":"1.0.0","vite-plugin-solid":"^2.11.11","@1game/render-data":"1.0.0"},"peerDependencies":{"solid-js":"^1.9.0"},"_npmOperationalInternal":{"tmp":"tmp/solid-ui_1.17.1_1786465276109_0.6821253273028569","host":"s3://npm-registry-packages-npm-production"}},"1.17.2":{"name":"@1game/solid-ui","version":"1.17.2","author":"linlinfaxin@163.com","license":"MIT","_id":"@1game/solid-ui@1.17.2","maintainers":[{"name":"linfaxin","email":"linlinfaxin@163.com"}],"dist":{"shasum":"f782ac9cded9451d40ce8b96f4cdad979d8e7f9c","tarball":"https://registry.npmjs.org/@1game/solid-ui/-/solid-ui-1.17.2.tgz","fileCount":207,"integrity":"sha512-p1wzXtbtnV+E9CEVl8sBZegDNJdaSZT6ON3FFcLkztne3CSBhw4WYWfXGj79c/5tit+2Wir03Lk/j98K6s4PyA==","signatures":[{"sig":"MEUCIQD/pDzI945ZMp3kVjIr3eAK0YVTieecgQwH2fjY91VtpQIgSbxlg0xfEWtVR3YeO8tAMaF11T8vYGDAC3ulcwpuTZU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":349819},"main":"lib/index.js","type":"module","types":"lib/index.d.ts","exports":{".":{"types":"./lib/index.d.ts","default":"./lib/index.js"}},"scripts":{"test":"vitest run","build":"pnpm run clean && tsc","clean":"rimraf lib","prepublish":"pnpm run build","test:watch":"vitest"},"_npmUser":{"name":"linfaxin","email":"linlinfaxin@163.com"},"description":"Headless UI primitives for 1Game Engine (controlled, unstyled)","directories":{},"_nodeVersion":"22.14.0","dependencies":{"@1game/engine-bundle":"1.17.2","@1game/solid-popover":"1.17.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"rimraf":"^6.1.2","vitest":"^1.0.0","solid-js":"^1.9.0","typescript":"^5.9.3","@1game/solid-dsl":"1.0.0","vite-plugin-solid":"^2.11.11","@1game/render-data":"1.0.0"},"peerDependencies":{"solid-js":"^1.9.0"},"_npmOperationalInternal":{"tmp":"tmp/solid-ui_1.17.2_1786500662616_0.6684050282820779","host":"s3://npm-registry-packages-npm-production"}},"1.18.0":{"name":"@1game/solid-ui","version":"1.18.0","author":"linlinfaxin@163.com","license":"MIT","_id":"@1game/solid-ui@1.18.0","maintainers":[{"name":"linfaxin","email":"linlinfaxin@163.com"}],"dist":{"shasum":"e748ebe73ac17df839eca42bb3a1eac3a42d849f","tarball":"https://registry.npmjs.org/@1game/solid-ui/-/solid-ui-1.18.0.tgz","fileCount":231,"integrity":"sha512-7vm6kHU1O6dnGBCzcsH+2wzgXQcv1EnwXE1Oy+hfvlSc11qDoXF0sxitGJokpXmGD+Q3v7nnr8D3kC1jMKiUIw==","signatures":[{"sig":"MEYCIQCcIbLliz6T4VreHb4Jk+29t6XGm/tu73wWwH3XGJxBSAIhAPJDJI3frM11hdh8hncTbVcqw5uwk8JufMSpSQMndOZY","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":417943},"main":"lib/index.js","type":"module","types":"lib/index.d.ts","exports":{".":{"types":"./lib/index.d.ts","default":"./lib/index.js"}},"scripts":{"test":"vitest run","build":"pnpm run clean && tsc","clean":"rimraf lib","prepublish":"pnpm run build","test:watch":"vitest"},"_npmUser":{"name":"linfaxin","email":"linlinfaxin@163.com"},"description":"Headless UI primitives for 1Game Engine (controlled, unstyled)","directories":{},"_nodeVersion":"22.14.0","dependencies":{"@1game/engine-bundle":"1.18.0","@1game/solid-popover":"1.18.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"rimraf":"^6.1.2","vitest":"^1.0.0","solid-js":"^1.9.0","typescript":"^5.9.3","@1game/solid-dsl":"1.0.0","vite-plugin-solid":"^2.11.11","@1game/render-data":"1.0.0"},"peerDependencies":{"solid-js":"^1.9.0"},"_npmOperationalInternal":{"tmp":"tmp/solid-ui_1.18.0_1786988120193_0.402894708188817","host":"s3://npm-registry-packages-npm-production"}},"1.19.0":{"name":"@1game/solid-ui","version":"1.19.0","author":"linlinfaxin@163.com","license":"MIT","_id":"@1game/solid-ui@1.19.0","maintainers":[{"name":"linfaxin","email":"linlinfaxin@163.com"}],"dist":{"shasum":"b09ce90cd2edcbe325797438ce8c3f88a98e9ada","tarball":"https://registry.npmjs.org/@1game/solid-ui/-/solid-ui-1.19.0.tgz","fileCount":249,"integrity":"sha512-Y/Pg5u6297bKWuOZalt2ZNxMMkynzmcPt12LOyZTZtv0z8Y1bNavIGlu5htt2bpOUISb0XWlvPBzGNbQzIt+7g==","signatures":[{"sig":"MEQCIAiekniebKC17KobrQ87bb2/9Jvj5Y06OAZKr78M51UFAiAG9oWotKr5cEmdfSe8lygRuHuz3nLdaW2sBuKgGoPIQg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":450605},"main":"lib/index.js","type":"module","types":"lib/index.d.ts","exports":{".":{"types":"./lib/index.d.ts","default":"./lib/index.js"}},"scripts":{"test":"vitest run","build":"pnpm run clean && tsc","clean":"rimraf lib","prepublish":"pnpm run build","test:watch":"vitest"},"_npmUser":{"name":"linfaxin","email":"linlinfaxin@163.com"},"description":"Headless UI primitives for 1Game Engine (controlled, unstyled)","directories":{},"_nodeVersion":"22.14.0","dependencies":{"@1game/engine-bundle":"1.19.0","@1game/solid-popover":"1.19.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"rimraf":"^6.1.2","vitest":"^1.0.0","solid-js":"^1.9.0","typescript":"^5.9.3","vite-plugin-solid":"^2.11.11","@1game/render-data":"1.0.0","@1game/solid-runtime":"1.0.0"},"peerDependencies":{"solid-js":"^1.9.0"},"_npmOperationalInternal":{"tmp":"tmp/solid-ui_1.19.0_1787246469199_0.6048266095233992","host":"s3://npm-registry-packages-npm-production"}},"1.20.0":{"name":"@1game/solid-ui","version":"1.20.0","author":"linlinfaxin@163.com","license":"MIT","_id":"@1game/solid-ui@1.20.0","maintainers":[{"name":"linfaxin","email":"linlinfaxin@163.com"}],"dist":{"shasum":"913155e4dd59f6d7b1da50a1f1fa515bb86a76d9","tarball":"https://registry.npmjs.org/@1game/solid-ui/-/solid-ui-1.20.0.tgz","fileCount":249,"integrity":"sha512-o25usM/PgaxxXHGM3Q+p8VJ1qfw3XQXzNXnAfHt6LcdS8f5jhWph5TOsAEhHDI8XHRerjGbH7uHLMmEMo+oXIA==","signatures":[{"sig":"MEUCIQCd0FU+68xj4TUhgpklu7IGd2xuuHVfvRev5WWNX73gmQIgSTFva98ofb1qXqg4HpG+gpRCp7WUJT0WQEupWCUV1iI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":450605},"main":"lib/index.js","type":"module","types":"lib/index.d.ts","exports":{".":{"types":"./lib/index.d.ts","default":"./lib/index.js"}},"scripts":{"test":"vitest run","build":"pnpm run clean && tsc","clean":"rimraf lib","prepublish":"pnpm run build","test:watch":"vitest"},"_npmUser":{"name":"linfaxin","email":"linlinfaxin@163.com"},"description":"Headless UI primitives for 1Game Engine (controlled, unstyled)","directories":{},"_nodeVersion":"22.14.0","dependencies":{"@1game/engine-bundle":"1.20.0","@1game/solid-popover":"1.20.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"rimraf":"^6.1.2","vitest":"^1.0.0","solid-js":"^1.9.0","typescript":"^5.9.3","vite-plugin-solid":"^2.11.11","@1game/render-data":"1.0.0","@1game/solid-runtime":"1.0.0"},"peerDependencies":{"solid-js":"^1.9.0"},"_npmOperationalInternal":{"tmp":"tmp/solid-ui_1.20.0_1787844439888_0.5665523035878408","host":"s3://npm-registry-packages-npm-production"}},"1.21.0":{"name":"@1game/solid-ui","description":"Headless UI primitives for 1Game Engine (controlled, unstyled)","version":"1.21.0","license":"MIT","author":"linlinfaxin@163.com","type":"module","publishConfig":{"access":"public"},"main":"lib/index.js","types":"lib/index.d.ts","exports":{".":{"types":"./lib/index.d.ts","default":"./lib/index.js"}},"dependencies":{"@1game/engine-bundle":"1.21.0","@1game/solid-popover":"1.21.0"},"devDependencies":{"rimraf":"^6.1.2","solid-js":"^1.9.0","typescript":"^5.9.3","vite-plugin-solid":"^2.11.11","vitest":"^1.0.0","@1game/solid-runtime":"1.0.0","@1game/render-data":"1.0.0"},"peerDependencies":{"solid-js":"^1.9.0"},"scripts":{"clean":"rimraf lib","prepublish":"pnpm run build","build":"pnpm run clean && tsc","test":"vitest run","test:watch":"vitest"},"_nodeVersion":"22.14.0","_id":"@1game/solid-ui@1.21.0","dist":{"integrity":"sha512-BPZugD4tXqGESlcB0cXbAFwWYcgnJOo1R8E1HlVXsBspsqTqs7Th2yW5TfDRPy8PGVEzvyoITCMp9Le4YG1hwg==","shasum":"361da5b69aac35db484dace89cc05e527400b131","tarball":"https://registry.npmjs.org/@1game/solid-ui/-/solid-ui-1.21.0.tgz","fileCount":249,"unpackedSize":450605,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIGQmeL1bOV2E1bCJ7RdLfJIjCeM/Z0cJaiwzDeM3qZ/+AiAYyP0LzHTOMybHatSGZI8G0+8/+ixDrGE8d5hdwLrUMg=="}]},"_npmUser":{"name":"linfaxin","email":"linlinfaxin@163.com"},"directories":{},"maintainers":[{"name":"linfaxin","email":"linlinfaxin@163.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/solid-ui_1.21.0_1788340355677_0.32704111721009066"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-11T16:14:25.823Z","modified":"2026-09-02T09:12:36.004Z","1.17.0":"2026-08-11T16:14:26.257Z","1.17.1":"2026-08-11T16:21:16.252Z","1.17.2":"2026-08-12T02:11:02.756Z","1.18.0":"2026-08-17T17:35:20.366Z","1.19.0":"2026-08-20T17:21:09.346Z","1.20.0":"2026-08-27T15:27:20.018Z","1.21.0":"2026-09-02T09:12:35.817Z"},"author":"linlinfaxin@163.com","license":"MIT","description":"Headless UI primitives for 1Game Engine (controlled, unstyled)","maintainers":[{"name":"linfaxin","email":"linlinfaxin@163.com"}],"readme":"# @1game/solid-ui\n\nHeadless UI primitives for 1Game Engine — **controlled**, **unstyled**, built for canvas /\nSolid DSL.\n\n**Status: Phase 3** (`SOLID_UI_PHASE = '3'`, `SOLID_UI_VERSION = '0.3.0-phase-3'`).\n\nPhase 2 surface: `Select` (+ internal `list-selection` shared with Menu),\n`Accordion`, `Tooltip` (+ `Tooltip.Provider`). Phase 3: `Toast` (`scopeStore` +\n`Toast.show`). See [`docs/solid-ui-contract.md`](../../docs/solid-ui-contract.md).\n\n### Migration: Content keep-mount (`hidden`)\n\n`Tabs.Content` / `Disclosure.Content` / `Accordion.Content` **stay mounted** when\ninactive/closed; the host sets `hidden` (skips paint + hit-test). This avoids Worker\n`createElement` empty→children `stableUid` faults.\n\n**Breaking for authors who used Accordion while Content still unmounted via `<Show>`:**\nclosing a section no longer tears down the Solid tree — `onCleanup` inside Content does\n**not** run on close; layout and effects still run. Gate expensive work on `open` /\n`selected`. Do **not** wrap these Content hosts in author `<Show>`. Full policy:\n`docs/solid-ui-contract.md`. Next public train should call this out\nin `packages-public/cli/whats-new/<version>.md`.\n\nContract: [`docs/solid-ui-contract.md`](../../docs/solid-ui-contract.md).\n\n## Install\n\n```bash\npnpm add @1game/solid-ui solid-js\n```\n\nPeer: `solid-js`. Depends on `@1game/engine-bundle` (Worker runtime / JSX) and\n`@1game/solid-popover` (Dialog / Menu / Select compose Modal + menu-preset Popover). Import\nraw `Popover` / `Modal` from `@1game/solid-popover` — solid-ui does **not** re-export them.\n\n## Phase 1 surface\n\n| Primitive              | Role                                                             |\n| ---------------------- | ---------------------------------------------------------------- |\n| `Pressable`            | Pointer host; `usePressableState()` → hover / active / disabled  |\n| `Checkbox` / `Switch`  | Controlled `checked` + `onCheckedChange` (Pressable)             |\n| `RadioGroup` / `Radio` | Controlled `value` + roving select-on-arrow                      |\n| `Tabs`                 | Compound list / triggers / panels; horizontal roving by default  |\n| `Disclosure`           | Controlled open / close building block                           |\n| `Dialog`               | Thin `Modal` wrap (zero new dismiss logic)                       |\n| `Menu`                 | Popover `menu` + collection + vertical roving                    |\n| `ScrollArea`           | Thin `<group scroll>` wrapper; opt-in `scrollbar` overlay thumbs |\n| RovingHighlight        | Pure `reduceRovingHighlight` helpers                             |\n\nAll interactive state is **controlled** via `createGameStore` / `commitChange` (replay). No\ndefault paint attrs — authors own visuals — except opt-in `ScrollArea` `scrollbar` overlay\nthumbs.\n\n### Pressable\n\nHeadless press host (`group`, default 0×0 + `overflowVisible`, host `clickable` when enabled).\n\n```tsx\nimport { Pressable, usePressableState } from '@1game/solid-ui';\n\nfunction Skin() {\n  const { hover, active } = usePressableState();\n  return <node width={80} height={32} backgroundColor={active() ? '#b45309' : hover() ? '#d97706' : '#f59e0b'} />;\n}\n\n<Pressable\n  width={80}\n  height={32}\n  onClick={() =>\n    commitChange('press', (d) => {\n      d.n++;\n    })\n  }\n>\n  <Skin />\n</Pressable>;\n```\n\n### Checkbox / Switch\n\nSame toggle machine (`checked` + `onCheckedChange`). Separate hooks / host sids:\n\n| Component  | Hook                 | Host sid            |\n| ---------- | -------------------- | ------------------- |\n| `Checkbox` | `useCheckboxState()` | `CHECKBOX_HOST_SID` |\n| `Switch`   | `useSwitchState()`   | `SWITCH_HOST_SID`   |\n\n### Slider\n\nHeadless numeric slider (path A): Root is the hit host; **authors paint** track / fill / thumb from `useSliderState().percent()`. No `Track` / `Range` / `Thumb` parts. Controlled `value` + `onValueChange`; optional `onValueCommit` (pointer Up / UpOutside). Defaults `min=0` `max=100` `step=1`. Pointer only — the host does **not** set `onKeyDown` and `useSliderState` has no `handleKeyDown`.\n\n```tsx\nimport { Slider, useSliderState } from '@1game/solid-ui';\n\nfunction VolumeSkin() {\n  const { percent, hover, dragging, disabled } = useSliderState();\n  const W = 200;\n  const H = 24;\n  const TRACK_H = 4;\n  const THUMB = 16;\n  return (\n    <group width={W} height={H} overflowVisible>\n      <node x={0} y={(H - TRACK_H) / 2} width={W} height={TRACK_H} backgroundColor={disabled() ? '#1f2937' : '#334155'} />\n      <node x={0} y={(H - TRACK_H) / 2} width={percent() * W} height={TRACK_H} backgroundColor=\"#38bdf8\" />\n      <node\n        x={percent() * W - THUMB / 2}\n        y={(H - THUMB) / 2}\n        width={THUMB}\n        height={THUMB}\n        shape=\"circle\"\n        backgroundColor={dragging() ? '#e0f2fe' : hover() ? '#7dd3fc' : '#38bdf8'}\n      />\n    </group>\n  );\n}\n\n<Slider\n  value={store.volume}\n  onValueChange={(v) =>\n    commitChange('vol', (d) => {\n      d.volume = v;\n    })\n  }\n  width={200}\n  height={24}\n>\n  <VolumeSkin />\n</Slider>;\n```\n\n### RadioGroup / Radio\n\nControlled `value` + `onValueChange`. Arrow / Home / End **select-on-move** (WAI-ARIA radio).\nDefault orientation `vertical`.\n\n**Keyboard (required):** engine KeyDown hits the **scene** only. Forward via\n`useRadioGroupState().handleKeyDown` from `<scene onKeyDown={…}>` (see\n`apps/cli-demos/solid-ui-showcase/`). RadioGroup hosts do **not** set `onKeyDown`.\n\n### Tabs\n\n`Tabs` / `Tabs.List` / `Tabs.Trigger` / `Tabs.Content`. Controlled `value` + `onValueChange`.\nDefault orientation **`horizontal`**. Inactive `Tabs.Content` stays mounted with\n`hidden` (skips paint / hit-test) so Worker createElement hosts avoid empty→children\nstableUid faults. **Layout and Solid effects inside Content still run** while hidden —\nkeep panels light; do not wrap Content in author `<Show>` / there is no `unmountOnHide`\nin Phase 1. See `docs/solid-ui-contract.md`.\n\n**Keyboard (required):** same as Radio — forward `useTabsState().handleKeyDown` from the\nauthor `<scene onKeyDown>`. `Tabs.List` does **not** set host `onKeyDown`.\n\n### Disclosure\n\n`Disclosure` / `Disclosure.Trigger` / `Disclosure.Content`. Controlled `open` + `onOpenChange`.\nNo mutual exclusion (use `Accordion` for that). Closed `Content` stays mounted with `hidden`\n(same keep-mount tradeoffs as Tabs.Content).\n\n### Dialog\n\nThin compound over `@1game/solid-popover` `Modal`. **Zero new dismiss logic.**\n\n| Part                                   | Maps to                                                           |\n| -------------------------------------- | ----------------------------------------------------------------- |\n| `Dialog`                               | `Modal` (`open` + `onOpenChange` + overlay / dismiss passthrough) |\n| `Dialog.Trigger` / `Content` / `Close` | `Popover.*`                                                       |\n| `Dialog.Title` / `Description`         | Structure-only unstyled groups (no a11y wiring)                   |\n\n```tsx\nimport { Dialog } from '@1game/solid-ui';\n\n<Dialog\n  open={store.open}\n  onOpenChange={(v) =>\n    commitChange('dialog', (d) => {\n      d.open = v;\n    })\n  }\n  overlayKey=\"confirm\"\n>\n  <Dialog.Trigger>\n    <node width={80} height={32} />\n  </Dialog.Trigger>\n  <Dialog.Content x={100} y={80} width={240} height={160}>\n    <Dialog.Title>\n      <text text=\"Confirm\" />\n    </Dialog.Title>\n    <Dialog.Close>\n      <node width={64} height={28} />\n    </Dialog.Close>\n  </Dialog.Content>\n</Dialog>;\n```\n\n### Menu\n\nPopover with `menu: true` (`MENU_DEFAULTS`) + collection + vertical RovingHighlight.\n\nPrefer controlled `highlightIndex` + `onHighlightIndexChange` (Public Store). When omitted,\nRoot keeps a **local** signal for the open session only (**non-replay**). Mode is reactive:\nproviding / omitting `highlightIndex` later switches controlled vs local (not frozen at mount).\nLeaving controlled seeds local from the last controlled index (continuity).\n\nKeyboard: ArrowUp/Down / Home/End move highlight; Enter/Space activate; Escape via popover\n`closeOnEscape`. Keys are handled on the **overlay scene** (`Popover` → `OverlayEntry.onKeyDown`)\nso the default `menu` preset (`keyboardPassThrough: false`) works without author scene\nforwarding. PointerEnter/Move also updates highlight (with a ~3px gate after keyboard/open\nreset to avoid remount snap-back). No submenu / typeahead.\n\n```tsx\nimport { Menu, useMenuItemState } from '@1game/solid-ui';\n\n<Menu\n  open={store.menuOpen}\n  onOpenChange={(v) =>\n    commitChange('menu', (d) => {\n      d.menuOpen = v;\n    })\n  }\n  highlightIndex={store.highlight}\n  onHighlightIndexChange={(i) =>\n    commitChange('menu-hl', (d) => {\n      d.highlight = i;\n    })\n  }\n  overlayKey=\"file\"\n>\n  <Menu.Trigger>\n    <node width={80} height={32} />\n  </Menu.Trigger>\n  <Menu.Content x={20} y={40} width={168} height={120}>\n    <Menu.Item value=\"new\" width={160} height={28} onSelect={() => {}}>\n      <ItemSkin />\n    </Menu.Item>\n  </Menu.Content>\n</Menu>;\n```\n\n### ScrollArea\n\nThin headless wrapper over DSL `<group scroll>`. Default: **no scrollbar chrome**.\nPass `scrollbar` for an iOS-overlay thumb (opt-in default paint). Overscroll pins\nthe thumb to the hit edge and shortens it (visual `contentOffset*`, no extra clamp).\n\n| Prop                                    | Notes                                                                                                                               |\n| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |\n| `axis`                                  | `'x'` \\| `'y'` \\| `'xy'` → DSL `scroll.axis` (`xy` → `both`)                                                                        |\n| `contentOffsetX` / `contentOffsetY`     | Seed only when `scopeStore` slice field is missing; slice number is SoT                                                             |\n| `scopeStore`                            | Public Store SoT (preferred / **replay-safe**)                                                                                      |\n| `deceleration` / `shareMode`            | Forwarded to `scroll.*`                                                                                                             |\n| `scrollbar`                             | `true` / `'auto'` / `'x'` / `'y'` / `'xy'` — overlay thumbs                                                                         |\n| `scrollbarAutoHide`                     | Default `true` (local; not replay SoT). Reveals on first overflow, scroll, and content/viewport size change; `false` = keep visible |\n| `scrollbarThickness` / `scrollbarColor` | Optional overlay overrides                                                                                                          |\n| `width` / `height` / `flex` / …         | Layout passthrough                                                                                                                  |\n| `ScrollArea.Content`                    | Optional alias group (Root + children is enough)                                                                                    |\n\n**Flex note:** Root `flex` only lays out **direct** children of Root (e.g. Content).\nChildren of Content are **not** pulled into Root’s Taffy tree unless Content also sets\n`flex`. Author `x`/`y` under Content remain author-positioned by default.\nWhen `scrollbar` is on, a clip wrap sits **outside** Root; `flex` still applies to the\nscroll host (`900_118`), not the wrap or overlay thumbs.\n\n**Controlled (replay-safe):** `scopeStore` from `createScopeStore` (same as raw group scroll).\n**Uncontrolled** (omit `scopeStore`): gesture-owned offsets —\n**non-replay-safe**. There is **no** `onContentOffsetChange` — journaling is `scopeStore`\nonly; read offsets back from the store.\n\n```tsx\nimport { ScrollArea } from '@1game/solid-ui';\nimport { createScopeStore } from '@1game/engine-bundle/runtime/worker';\n\n<ScrollArea axis=\"y\" width={200} height={160} scrollbar scopeStore={createScopeStore(store, commitChange, (d) => d.list)}>\n  <For each={rows}>{(row) => <node key={row.id} y={row.y} width={200} height={40} />}</For>\n</ScrollArea>;\n```\n\n### Select (Phase 2A)\n\nRadix-like compound over Popover `menu: true` + internal `list-selection` (does **not**\npublicly wrap / re-export `Menu`). Required controlled `value` + `onValueChange` and\n`open` + `onOpenChange`. Optional highlight follows Menu rules (controlled pair **or**\nlocal open-session signal).\n\nParts: `Trigger` / `Value` / `Content` / `Item` / `ItemIndicator` / `Separator`.\n`Value` is a structure slot (+ optional `placeholder` prop) — **no** cloning of Item\nchildren. Open → highlight prefers the current `value` when present & enabled; else first\nenabled. Selecting an item calls `onValueChange` then closes. Keyboard on the overlay\nscene (same menu preset as Menu).\n\n`useSelectState().textValue()` follows the controlled `value` and **keeps the last\nregistered Item label after Content closes** (Item unregister clears live meta only).\nSkins can safely bind Trigger text to `textValue() || placeholder`.\n\n```tsx\nimport { Select, useSelectState, useSelectItemState } from '@1game/solid-ui';\n\nfunction ValueSkin() {\n  const { textValue } = useSelectState();\n  return <text text={textValue() || 'Pick one'} />;\n}\n\n<Select\n  value={store.value}\n  onValueChange={(v) =>\n    commitChange('sel', (d) => {\n      d.value = v;\n    })\n  }\n  open={store.open}\n  onOpenChange={(v) =>\n    commitChange('sel', (d) => {\n      d.open = v;\n    })\n  }\n  overlayKey=\"fruit\"\n>\n  <Select.Trigger width={120} height={32}>\n    <Select.Value placeholder=\"Pick one\">\n      <ValueSkin />\n    </Select.Value>\n  </Select.Trigger>\n  <Select.Content x={0} y={36} width={160} height={90}>\n    <Select.Item value=\"apple\" textValue=\"Apple\" width={160} height={28}>\n      <ItemSkin />\n    </Select.Item>\n  </Select.Content>\n</Select>;\n```\n\n### Accordion (Phase 2B)\n\nDiscriminated `type=\"single\"` (default) / `\"multiple\"`. Parts: `Item` / `Trigger` /\n`Content` (no `Header`). Does **not** nest public `Disclosure`.\n\n| `type`     | Value                        | Notes                                                                         |\n| ---------- | ---------------------------- | ----------------------------------------------------------------------------- |\n| `single`   | `string` + `onValueChange`   | `collapsible` default **`false`** (Radix); `''` = all closed when collapsible |\n| `multiple` | `string[]` + `onValueChange` | Independent open set (immutable add/remove)                                   |\n\n**Keyboard (required):** vertical RovingHighlight — Arrow / Home / End move **local**\nhighlight only (**non-replay**); Enter / Space toggles the highlighted item (not\nselect-on-move). Forward `useAccordionState().handleKeyDown` from author\n`<scene onKeyDown>` — hosts do **not** set `onKeyDown`. Skins read\n`useAccordionItemState().highlighted` (plus `hover` / `active` under Trigger).\n\nClosed `Accordion.Content` stays mounted with `hidden` (same keep-mount tradeoffs as\nTabs / Disclosure Content — do not wrap in author `<Show>`). **`hidden` does not shrink\nlayout** — for collapse that moves siblings up, recompute Item `y`/`height` from `open`\n(see cli-demo `ShowcaseAccordion`).\n**Breaking vs early Phase 2 `<Show>` unmount:** closed panels no longer run Content\n`onCleanup`; gate timers / side effects on `open` / item state instead. Document this in\nthe next public `whats-new` train when bumping `@1game/solid-ui` for npm consumers.\nTrigger click also syncs the local highlight to that item.\n\n```tsx\nimport { Accordion, useAccordionState, useAccordionItemState } from '@1game/solid-ui';\n\nfunction TriggerSkin() {\n  const { open, highlighted } = useAccordionItemState();\n  return <node width={200} height={32} backgroundColor={highlighted() ? '#b45309' : open() ? '#334155' : '#1e293b'} />;\n}\n\n<Accordion\n  type=\"single\"\n  value={store.section}\n  onValueChange={(v) =>\n    commitChange('acc', (d) => {\n      d.section = v;\n    })\n  }\n  collapsible\n>\n  <Accordion.Item value=\"a\" width={200} height={80}>\n    <Accordion.Trigger width={200} height={32}>\n      <TriggerSkin />\n    </Accordion.Trigger>\n    <Accordion.Content width={200} height={40}>\n      <node width={200} height={40} />\n    </Accordion.Content>\n  </Accordion.Item>\n</Accordion>;\n```\n\n### Tooltip (Phase 2C)\n\nPlain Popover (NOT `menu: true`) with leave-based dismiss:\n\n| Flag                  | Default |\n| --------------------- | ------- |\n| `closeOnOutside`      | `false` |\n| `pointerPassThrough`  | `true`  |\n| `keyboardPassThrough` | `true`  |\n| `closeOnEscape`       | `true`  |\n| Content `placement`   | `top`   |\n\n**Open modes:** omit `open` + `onOpenChange` → ephemeral local signal (**non-replay**);\ncontrolled requires **both**. Hover calls `onOpenChange` only (never store SoT).\nControlled `open` that becomes true **without** hover (tutorial / seek) is held through\nleave until Escape / Trigger press / a later hover cycle.\n\nDismiss: pointer leave (Trigger ± Content when hoverable), Escape, **press Trigger closes\nimmediately**. `Tooltip.Provider` shares `delayDuration` (700) / `skipDelayDuration` (300):\nafter close, opens within the skip window are instant. Root works without Provider.\n\n```tsx\nimport { Tooltip, useTooltipState } from '@1game/solid-ui';\n\nfunction TipSkin() {\n  const { open } = useTooltipState();\n  return open() ? <text text=\"Hint\" /> : null;\n}\n\n<Tooltip.Provider delayDuration={700} skipDelayDuration={300}>\n  <Tooltip delayDuration={400} overlayKey=\"tip\">\n    <Tooltip.Trigger width={80} height={32}>\n      <node width={80} height={32} />\n    </Tooltip.Trigger>\n    <Tooltip.Content x={0} y={0} width={120} height={28}>\n      <TipSkin />\n    </Tooltip.Content>\n  </Tooltip>\n</Tooltip.Provider>;\n```\n\n### RovingHighlight\n\n```ts\nimport { reduceRovingHighlight } from '@1game/solid-ui';\n\nconst next = reduceRovingHighlight(\n  { highlightIndex: store.highlightIndex },\n  { key: 'ArrowDown' },\n  { itemCount: 5, orientation: 'vertical', loop: true, disabledIndices: [false, true, false, false, false] },\n);\nif (next !== null)\n  commitChange('roving', (d) => {\n    d.highlightIndex = next;\n  });\n```\n\n## Phase 2 (complete)\n\n| Primitive   | Status                   |\n| ----------- | ------------------------ |\n| `Select`    | **Complete** — see above |\n| `Accordion` | **Complete** — see above |\n| `Tooltip`   | **Complete** — see above |\n\nDetails: [`docs/solid-ui-contract.md`](../../docs/solid-ui-contract.md).\n\n## Phase 3 (`Toast`)\n\nReplay-safe notifications. Slice `{ items, nextSeq }` via `createScopeStore`. Command\n`Toast.show(scope, 'Saved')` / `Toast.dismiss(scope, id)` (no factory). Provider ticks\nexpiry; Viewport is a headless stack for a `pointerPassThrough` + `keyboardPassThrough` HUD scene.\n\n```tsx\nimport { createGameStore, createScopeStore, renderGame } from '@1game/engine-bundle/runtime/worker';\nimport { Toast } from '@1game/solid-ui';\n\ntype GameState = { toast: { items: { id: string; title: string; durationMs: number; remainingMs: number }[]; nextSeq: number } };\n\nconst { store, commitChange, bindStore } = createGameStore<GameState>({\n  toast: { items: [], nextSeq: 0 },\n});\nconst toastScope = createScopeStore(store, commitChange, (d: GameState) => d.toast);\n\nToast.show(toastScope, 'Saved');\n\n<scene name=\"hud\" width={400} height={400} pointerPassThrough keyboardPassThrough>\n  <Toast.Provider scopeStore={toastScope}>\n    <Toast.Viewport placement=\"top-right\" x={184} y={16} width={200} height={40}>\n      {(t) => (\n        <Toast.Root toastId={t.id} width={200} height={32}>\n          <text text={t.title} />\n          <Toast.Close width={24} height={24}>\n            <node width={24} height={24} />\n          </Toast.Close>\n        </Toast.Root>\n      )}\n    </Toast.Viewport>\n  </Toast.Provider>\n</scene>;\n```\n\n`durationMs: 0` is sticky. Same `id` upserts. Empty-list ticks do not `commitChange`.\n`Toast.Close` is a 0×0 Pressable — give it a sized child (or `width`/`height`). Live\ntitle / remaining time: `useToastItemState().item()`. Dismissed items unmount (do not\nwrap Viewport in author `<Show>`). Command API is `Toast.show` / `Toast.dismiss` only.\n\n## Scripts\n\n```bash\npnpm --filter @1game/solid-ui run build\npnpm --filter @1game/solid-ui run test\n```\n\nCLI demo (monorepo): `apps/cli-demos/solid-ui-showcase/` —\n\n```bash\npnpm --filter @1game/solid-ui-showcase build\npnpm --filter @1game/solid-ui-showcase test:gameplay\n```\n","readmeFilename":""}