@justanarthur/modo-ui 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE.beui +21 -0
  3. package/LICENSE.fluid-functionalism +21 -0
  4. package/LICENSE.motion-primitives +21 -0
  5. package/LICENSE.sileo +21 -0
  6. package/README.md +608 -2
  7. package/dist/base.css +994 -0
  8. package/dist/blocks/sidebar-app/index.d.ts +34 -0
  9. package/dist/blocks/sidebar-app/index.js +31 -0
  10. package/dist/components/badge/index.d.ts +59 -0
  11. package/dist/components/badge/index.js +16 -0
  12. package/dist/components/button/index.d.ts +59 -0
  13. package/dist/components/button/index.js +14 -0
  14. package/dist/components/card/index.d.ts +141 -0
  15. package/dist/components/card/index.js +42 -0
  16. package/dist/components/inputs/checkbox-group/index.d.ts +50 -0
  17. package/dist/components/inputs/checkbox-group/index.js +21 -0
  18. package/dist/components/inputs/color-picker/index.d.ts +137 -0
  19. package/dist/components/inputs/color-picker/index.js +35 -0
  20. package/dist/components/inputs/combobox/index.d.ts +161 -0
  21. package/dist/components/inputs/combobox/index.js +38 -0
  22. package/dist/components/inputs/date-time/calendar/index.d.ts +45 -0
  23. package/dist/components/inputs/date-time/calendar/index.js +18 -0
  24. package/dist/components/inputs/date-time/date-picker/index.d.ts +82 -0
  25. package/dist/components/inputs/date-time/date-picker/index.js +32 -0
  26. package/dist/components/inputs/date-time/date-range-picker/index.d.ts +64 -0
  27. package/dist/components/inputs/date-time/date-range-picker/index.js +31 -0
  28. package/dist/components/inputs/date-time/duration-picker/index.d.ts +57 -0
  29. package/dist/components/inputs/date-time/duration-picker/index.js +31 -0
  30. package/dist/components/inputs/date-time/month-picker/index.d.ts +57 -0
  31. package/dist/components/inputs/date-time/month-picker/index.js +30 -0
  32. package/dist/components/inputs/date-time/time-picker/index.d.ts +59 -0
  33. package/dist/components/inputs/date-time/time-picker/index.js +31 -0
  34. package/dist/components/inputs/input/index.d.ts +84 -0
  35. package/dist/components/inputs/input/index.js +19 -0
  36. package/dist/components/inputs/input-copy/index.d.ts +44 -0
  37. package/dist/components/inputs/input-copy/index.js +16 -0
  38. package/dist/components/inputs/input-message/index.d.ts +125 -0
  39. package/dist/components/inputs/input-message/index.js +25 -0
  40. package/dist/components/inputs/radio-group/index.d.ts +58 -0
  41. package/dist/components/inputs/radio-group/index.js +20 -0
  42. package/dist/components/inputs/search/index.d.ts +83 -0
  43. package/dist/components/inputs/search/index.js +25 -0
  44. package/dist/components/inputs/select/index.d.ts +105 -0
  45. package/dist/components/inputs/select/index.js +34 -0
  46. package/dist/components/inputs/slider/index.d.ts +108 -0
  47. package/dist/components/inputs/slider/index.js +14 -0
  48. package/dist/components/inputs/switch/index.d.ts +43 -0
  49. package/dist/components/inputs/switch/index.js +14 -0
  50. package/dist/components/navigation/accordion/index.d.ts +117 -0
  51. package/dist/components/navigation/accordion/index.js +28 -0
  52. package/dist/components/navigation/sidebar/index.d.ts +388 -0
  53. package/dist/components/navigation/sidebar/index.js +99 -0
  54. package/dist/components/navigation/tabs/index.d.ts +70 -0
  55. package/dist/components/navigation/tabs/index.js +23 -0
  56. package/dist/components/navigation/tabs-subtle/index.d.ts +73 -0
  57. package/dist/components/navigation/tabs-subtle/index.js +21 -0
  58. package/dist/components/overlays/alert-dialog/index.d.ts +67 -0
  59. package/dist/components/overlays/alert-dialog/index.js +22 -0
  60. package/dist/components/overlays/command/index.d.ts +121 -0
  61. package/dist/components/overlays/command/index.js +36 -0
  62. package/dist/components/overlays/context-menu/index.d.ts +97 -0
  63. package/dist/components/overlays/context-menu/index.js +29 -0
  64. package/dist/components/overlays/dialog/index.d.ts +130 -0
  65. package/dist/components/overlays/dialog/index.js +37 -0
  66. package/dist/components/overlays/dropdown/index.d.ts +222 -0
  67. package/dist/components/overlays/dropdown/index.js +44 -0
  68. package/dist/components/overlays/popover/index.d.ts +87 -0
  69. package/dist/components/overlays/popover/index.js +26 -0
  70. package/dist/components/overlays/preview-card/index.d.ts +75 -0
  71. package/dist/components/overlays/preview-card/index.js +19 -0
  72. package/dist/components/overlays/sheet/index.d.ts +67 -0
  73. package/dist/components/overlays/sheet/index.js +24 -0
  74. package/dist/components/overlays/toast/index.d.ts +107 -0
  75. package/dist/components/overlays/toast/index.js +21 -0
  76. package/dist/components/overlays/tooltip/index.d.ts +75 -0
  77. package/dist/components/overlays/tooltip/index.js +17 -0
  78. package/dist/components/table/index.d.ts +44 -0
  79. package/dist/components/table/index.js +26 -0
  80. package/dist/components/thinking-indicator/index.d.ts +32 -0
  81. package/dist/components/thinking-indicator/index.js +13 -0
  82. package/dist/index.d.ts +3926 -0
  83. package/dist/index.js +511 -0
  84. package/dist/preset.d.ts +104 -0
  85. package/dist/preset.js +124 -0
  86. package/dist/primitives/code/index.d.ts +23 -0
  87. package/dist/primitives/code/index.js +11 -0
  88. package/dist/primitives/craft/index.d.ts +31 -0
  89. package/dist/primitives/craft/index.js +11 -0
  90. package/dist/primitives/fluid-hover/index.d.ts +233 -0
  91. package/dist/primitives/fluid-hover/index.js +25 -0
  92. package/dist/primitives/morph/index.d.ts +127 -0
  93. package/dist/primitives/morph/index.js +19 -0
  94. package/dist/primitives/motion/index.d.ts +115 -0
  95. package/dist/primitives/motion/index.js +19 -0
  96. package/dist/primitives/scroll-area/index.d.ts +41 -0
  97. package/dist/primitives/scroll-area/index.js +12 -0
  98. package/dist/primitives/sizes/index.d.ts +128 -0
  99. package/dist/primitives/sizes/index.js +21 -0
  100. package/dist/primitives/surface/index.d.ts +69 -0
  101. package/dist/primitives/surface/index.js +17 -0
  102. package/dist/shared/components-0ke6xada.js +123 -0
  103. package/dist/shared/components-0q0sqfh4.js +146 -0
  104. package/dist/shared/components-0sn7ypjw.js +61 -0
  105. package/dist/shared/components-1j9791sn.js +345 -0
  106. package/dist/shared/components-1rfwbyyc.js +361 -0
  107. package/dist/shared/components-1v7rxytx.js +30 -0
  108. package/dist/shared/components-1xekx2hx.js +215 -0
  109. package/dist/shared/components-39jq9va1.js +86 -0
  110. package/dist/shared/components-3gae8nta.js +719 -0
  111. package/dist/shared/components-3k8xpnpq.js +1759 -0
  112. package/dist/shared/components-3ky44908.js +9 -0
  113. package/dist/shared/components-3mkepka5.js +111 -0
  114. package/dist/shared/components-3wk0w46f.js +667 -0
  115. package/dist/shared/components-4cnjmgex.js +124 -0
  116. package/dist/shared/components-59efxjd9.js +298 -0
  117. package/dist/shared/components-5xc2j8jz.js +159 -0
  118. package/dist/shared/components-62ybc7t2.js +496 -0
  119. package/dist/shared/components-66mv71kx.js +262 -0
  120. package/dist/shared/components-7ans7ce5.js +120 -0
  121. package/dist/shared/components-7btqt08d.js +270 -0
  122. package/dist/shared/components-7r3etddy.js +171 -0
  123. package/dist/shared/components-7x9kawxv.js +152 -0
  124. package/dist/shared/components-8fpr3s4p.js +292 -0
  125. package/dist/shared/components-9jvzzvt1.js +198 -0
  126. package/dist/shared/components-apdwtweq.js +484 -0
  127. package/dist/shared/components-asr5pg50.js +191 -0
  128. package/dist/shared/components-b2wyq2cq.js +232 -0
  129. package/dist/shared/components-c6s6b43g.js +15 -0
  130. package/dist/shared/components-ca2c898n.js +1828 -0
  131. package/dist/shared/components-cbxjn9p8.js +141 -0
  132. package/dist/shared/components-chacnk61.js +216 -0
  133. package/dist/shared/components-cv16mryv.js +62 -0
  134. package/dist/shared/components-ez65vtbq.js +110 -0
  135. package/dist/shared/components-ezxhrmer.js +17 -0
  136. package/dist/shared/components-gc5vtq2f.js +149 -0
  137. package/dist/shared/components-gf5s6ap9.js +42 -0
  138. package/dist/shared/components-gvr9w41t.js +193 -0
  139. package/dist/shared/components-h2dpcwgc.js +137 -0
  140. package/dist/shared/components-hawvy2qz.js +105 -0
  141. package/dist/shared/components-hqeq3w9q.js +375 -0
  142. package/dist/shared/components-jjw69c4c.js +28 -0
  143. package/dist/shared/components-jwst3vtj.js +346 -0
  144. package/dist/shared/components-kvr7f9hy.js +45 -0
  145. package/dist/shared/components-men63rwj.js +9 -0
  146. package/dist/shared/components-mhd39z7x.js +22 -0
  147. package/dist/shared/components-n7mjr357.js +26 -0
  148. package/dist/shared/components-ng72k97e.js +128 -0
  149. package/dist/shared/components-nh8tma1n.js +986 -0
  150. package/dist/shared/components-njzgqdwy.js +445 -0
  151. package/dist/shared/components-ny774h8p.js +293 -0
  152. package/dist/shared/components-q4daerx0.js +58 -0
  153. package/dist/shared/components-q5xj5c16.js +79 -0
  154. package/dist/shared/components-rr7a6ja9.js +160 -0
  155. package/dist/shared/components-s2bz1xpk.js +63 -0
  156. package/dist/shared/components-tqvarbfv.js +288 -0
  157. package/dist/shared/components-v6xp325t.js +304 -0
  158. package/dist/shared/components-v7wtz8hk.js +144 -0
  159. package/dist/shared/components-vc7vq5nz.js +32 -0
  160. package/dist/shared/components-vfbhxf5r.js +58 -0
  161. package/dist/shared/components-vynjtrk6.js +19 -0
  162. package/dist/shared/components-wvp1m52g.js +1200 -0
  163. package/dist/shared/components-yh3kwdhr.js +981 -0
  164. package/dist/shared/components-ytb2tefr.js +291 -0
  165. package/dist/shared/components-zfnmn5d4.js +220 -0
  166. package/dist/shared/components-zrkzcayz.js +141 -0
  167. package/dist/shared/components-zx6b6yby.js +42 -0
  168. package/dist/styles.css +1949 -0
  169. package/dist/tailwind.css +219 -0
  170. package/dist/tokens.css +345 -0
  171. package/package.json +103 -4
package/README.md CHANGED
@@ -1,3 +1,609 @@
1
- # Temporary Holding Version
1
+ # UI — Fluid Functionalism for modo
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The UI design system is a port of [Fluid Functionalism](https://www.fluidfunctionalism.com)
4
+ (FF) into modo's structure. FF is a shadcn-style component registry by Micka
5
+ Touillaud / mickadesign ([repo](https://github.com/mickadesign/fluid-functionalism),
6
+ MIT). It layers motion springs, a fluid hover highlight, a two-step size ladder,
7
+ an eight-level elevated surface system and Inter weight animation on top of
8
+ Radix or Base UI primitives.
9
+
10
+ This port differs from the upstream registry in three ways:
11
+ - It uses the **Base UI** flavor.
12
+ - **UnoCSS** (`presetWind4`) replaces Tailwind CSS v4.
13
+ - **[`cn`](https://github.com/shadcn-ui/cn)** replaces clsx + tailwind-merge.
14
+
15
+ The sibling `../fluid-functionalism` package is a different project: a Radix
16
+ flavor pulled with the shadcn CLI, still on Tailwind.
17
+
18
+ On top of FF, the overlays morph: they grow out of whatever opened them, with
19
+ a liquid "goo" neck by default. That layer is local code (`lib/use-morph.ts`,
20
+ `lib/morph-layers.tsx`, documented by `primitives/morph`), with techniques
21
+ taken from three more MIT projects (see [Morph sources](#morph-sources)).
22
+
23
+ ## Install
24
+
25
+ ```sh
26
+ npm install @justanarthur/modo-ui
27
+ ```
28
+
29
+ React 18 or later is a peer dependency. Import components from the package
30
+ root, or one item per subpath (`@justanarthur/modo-ui/components/button`,
31
+ `@justanarthur/modo-ui/components/overlays/dialog`, …):
32
+
33
+ ```tsx
34
+ import { Button, Switch } from '@justanarthur/modo-ui'
35
+ ```
36
+
37
+ ### Styles
38
+
39
+ Pick one of three ways in. The components are styled with utility classes,
40
+ so either take them prebuilt or let your own Tailwind or UnoCSS generate them
41
+ by scanning the package's `dist/`. Scanning emits each class once, beside your
42
+ own.
43
+
44
+ **Prebuilt**: no CSS tooling needed. It includes the reset, the tokens, the
45
+ base styles and every utility the components use:
46
+
47
+ ```ts
48
+ import '@justanarthur/modo-ui/styles.css'
49
+ ```
50
+
51
+ **Tailwind CSS v4**: the theme, the token utilities, the `dark:` variant and
52
+ `@source` for the components ship in one file:
53
+
54
+ ```css
55
+ @import "tailwindcss";
56
+ @import "@justanarthur/modo-ui/tailwind.css";
57
+ ```
58
+
59
+ **UnoCSS** (`presetWind4`): add the preset and the components' content, then
60
+ import the tokens and base styles:
61
+
62
+ ```ts
63
+ // uno.config.ts
64
+ import presetModoUi, { modoUiContent } from '@justanarthur/modo-ui/unocss'
65
+ import { defineConfig, presetWind4 } from 'unocss'
66
+
67
+ export default defineConfig({
68
+ presets: [presetWind4(), presetModoUi()],
69
+ content: { inline: [modoUiContent] },
70
+ })
71
+ ```
72
+
73
+ ```ts
74
+ import 'virtual:uno.css'
75
+ import '@justanarthur/modo-ui/base.css'
76
+ ```
77
+
78
+ `tokens.css` holds the CSS variables alone.
79
+
80
+ ### Theme
81
+
82
+ Every color is a `light-dark()` token, so `color-scheme` picks the side: it
83
+ follows the OS until `<html>` (or any subtree) carries `light` or `dark`.
84
+ `dark:` utilities match inside a `.dark` ancestor, so set the class whenever
85
+ you pin dark mode:
86
+
87
+ ```ts
88
+ document.documentElement.classList.toggle('dark', dark)
89
+ document.documentElement.classList.toggle('light', !dark)
90
+ ```
91
+
92
+ ## Source
93
+
94
+ - Repository: `github.com/mickadesign/fluid-functionalism`
95
+ - Commit: `b3587bdbd83fc66c2a6aae3817ffb856cb09260b`
96
+ - License: MIT © 2026 Micka Touillaud. The full text is in
97
+ [`LICENSE.fluid-functionalism`](./LICENSE.fluid-functionalism), copied
98
+ verbatim.
99
+ - Flavor: `registry/base/*` wherever upstream ships a flavored file, and
100
+ `registry/default/*` otherwise. The Base UI flavor is pinned to
101
+ `@base-ui/react@1.4.1`, the version upstream was tested with.
102
+
103
+ Every vendored file opens with a plain `/* */` header. The header names the
104
+ upstream path and commit, carries the MIT notice, and lists every local
105
+ modification. The vendored code below the header keeps upstream's formatting.
106
+ Files written for this port (`preset.ts`, `uno.config.ts`, `vite.ts`, `index.ts`,
107
+ `scripts/build-css.ts`, `modo.components.tsx`,
108
+ `lib/use-controllable-state.ts`, `primitives/code/index.tsx`, `primitives/craft/*`, the morph files
109
+ in `lib/` (`use-morph.ts`, `reduced-motion.ts`, `morph-layers.tsx`, `morph-part.tsx`, `goo-indicator.tsx`,
110
+ `slot.ts`) and `primitives/morph/index.tsx`, `blocks/sidebar-app/index.tsx`,
111
+ and the local components marked "— (local)" below) use this repo's style: single quotes and no semicolons.
112
+
113
+ ### Morph sources
114
+
115
+ | Project | Repository @ commit | License | What it contributes |
116
+ |---|---|---|---|
117
+ | beUI | `github.com/starc007/ui-components` @ `de52f337e520e7ee37749b36eb1c32df86137bcb` | MIT © 2026 Saurabh Chauhan, [`LICENSE.beui`](./LICENSE.beui) | the gooey popover's rect interpolation, rounded-rect path, source cutout and goo filter; the morph select; the morphing search (idea only) |
118
+ | motion-primitives | `github.com/ibelick/motion-primitives` @ `120f64f6ca60348e251f929e9c81f11ccbe45eda` | MIT © 2024 ibelick, [`LICENSE.motion-primitives`](./LICENSE.motion-primitives) | the morphing popover and morphing dialog (trigger growing into the panel, shared parts) |
119
+ | Sileo | `github.com/hiaaryan/sileo` @ `9793f844349983e140cf33cebbb8f51626d41407` | MIT, [`LICENSE.sileo`](./LICENSE.sileo) | the goo blur as a share of the surface radius; the gooey toast |
120
+
121
+ Sileo declares MIT in its `package.json` but ships no LICENSE file, so
122
+ `LICENSE.sileo` is the standard MIT text naming the repository's owner.
123
+ beUI's and motion-primitives' are copied verbatim.
124
+
125
+ Only visuals come from these projects. Base UI keeps all behavior
126
+ (positioning, focus, dismissal, hover and tap, scroll lock), so none of
127
+ their own hooks for it (click-outside, focus traps, dismissal, gestures,
128
+ positioning) are ported.
129
+
130
+ ## Layout
131
+
132
+ | Path | What it holds |
133
+ |---|---|
134
+ | `AGENTS.md` | The ui-specific guide for coding agents (where things live, the rules that matter most, the checks); `CLAUDE.md` imports it. |
135
+ | `DESIGN.md` | The design language in Google's [DESIGN.md](https://github.com/google-labs-code/design.md) format: the tokens as front matter, the rules as prose. Agents building here follow the `modo-ui` skill (`.claude/skills/modo-ui` at the repo root). |
136
+ | `tokens/*.css` | Colors, typography (`--font-sans`, `--text-*`), motion (`--duration-*`), radius and spacing. |
137
+ | `base.css` | Theme switching, base styles, focus fallback, scrollbars, shimmer/spinner keyframes and `.scroll-fade`. Shipped in the package. |
138
+ | `global.css` | The docs site's stylesheet (`modo.config.ts` `css`): imports `base.css`, then the modo-only focus revert and the docs chrome. |
139
+ | `preset.ts` | The UnoCSS preset (theme and token utilities), shipped as `@justanarthur/modo-ui/unocss`. |
140
+ | `uno.config.ts`, `vite.ts` | The UnoCSS setup of the docs site and the prebuilt CSS (see below). |
141
+ | `index.ts`, `bunup.config.ts`, `scripts/build-css.ts` | The package build: `bun run build` writes the components, their types and the stylesheets to `dist/`. The docs site builds with `bun run build:docs` into `docs-dist/`. |
142
+ | `lib/` | FF's shared system files (upstream `lib/`, `hooks/` and the fluid-hover highlight, flat, upstream names), imported relatively by items. modo only scans the tier folders, so it never mistakes them for items. |
143
+ | `modo.components.tsx` | Components only the docs site uses: the `Icon`, `Select` and `DocsNav` shell-slot adapters and the `ThemeSwitcher` panel item, picked in `modo.config.ts` by export name (`./modo.components.tsx#Select`). |
144
+ | `primitives/<id>/` | FF's "System" pages: `index.tsx` (component + TSDoc prose) and `examples.mdx`, pulled in by `@example {@include ./examples.mdx}`. |
145
+ | `components/[<group>/]<id>/` | FF's "Components" pages, same shape. The `inputs/`, `navigation/` and `overlays/` group folders become sidebar sub-groups and overview sections; the core display items stay ungrouped. |
146
+ | `blocks/<id>/` | Compositions of several items, same shape: `sidebar-app`, FF's sidebar preset `sa1FQfCxH6` as an app shell, with the header and footer blocks it is built from beside it. |
147
+
148
+ ## Upstream → local file map
149
+
150
+ ### Shared system (`lib/`)
151
+
152
+ | Upstream (`registry/default/…`) | Local | Modifications |
153
+ |---|---|---|
154
+ | `lib/utils.ts` | `lib/utils.ts` | `extendTailwindMerge` → `createCn` from `cn/config`, with the same font-size group extension; the local token utilities join their groups too (compact and `micro` type roles as font-size, `weight-*`, the `duration-*` / `delay-*` motion tiers, `rounded-box\|glyph`) |
155
+ | `lib/springs.ts` | `lib/springs.ts` | `goo` tier added (the morph engine's goo: 250ms to land, bounce 0.15, a 200ms spring back); `exitFallbackMs` also reads a spring exit's `visualDuration` |
156
+ | `lib/font-weight.ts` | — | not ported: its `fontWeights` became the `--weight-*` tokens in `tokens/typography.css` (the `weight-*` utilities) |
157
+ | `lib/popup.ts` | `lib/popup.ts` | `popupMotionClass` dropped (unused since the popups morph) |
158
+ | `lib/shape-context.tsx` | `lib/shape-context.tsx` | `"use client"` dropped |
159
+ | `lib/size-context.tsx` | `lib/size-context.tsx` | `"use client"` dropped; `SizeProvider` moved to `primitives/sizes` (the documented item), with `SizeContext` exported for it; `sizeMap` text classes → `text-body` / `text-body-compact` |
160
+ | `lib/icon-context.tsx` | `lib/icon-context.tsx` | `"use client"` dropped; `size`/`strokeWidth` widened to `number \| string` for lucide v1 + React 18 types |
161
+ | `lib/surface-context.tsx` | `lib/surface-context.tsx` | `"use client"` dropped |
162
+ | `lib/surface-classes.ts` | `lib/surface-classes.ts` | `SURFACE_HOVER_*` and `surfaceHoverClasses` dropped (only Sidebar used them) |
163
+ | `hooks/use-fluid-hover.ts` | `lib/use-fluid-hover.ts` | `"use client"` dropped; `sessionRef` typed `MutableRefObject<number>` (React 18) |
164
+ | `hooks/use-merge-split.tsx` | `lib/use-merge-split.tsx` | `"use client"` dropped; `framer-motion` → `motion/react`; relative imports; the merge/split choreography replaced by per-row blocks melted through `GooLayer`; exports and `SelBlock`'s `key`/rect/`radii`/`instant` kept |
165
+ | `hooks/use-keyboard-nav-gate.ts` | `lib/use-keyboard-nav-gate.ts` | `"use client"` dropped; relative imports |
166
+ | `hooks/use-touch-primary.tsx` | `lib/use-touch-primary.tsx` | `"use client"` dropped |
167
+ | `sidebar-core.tsx` (`useIsMobile`) | `lib/use-is-mobile.ts` | Moved out of the sidebar, unchanged, so the date and time pickers share it |
168
+ | `fluid-hover-highlight.tsx` | `lib/fluid-hover-highlight.tsx` | `"use client"` dropped; `framer-motion` → `motion/react`; relative imports; the fill is a `GooIndicator` (melts from row to row), props unchanged |
169
+ | — (new) | `lib/use-controllable-state.ts` | Local addition: controlled + uncontrolled state, Base UI style |
170
+ | — (new) | `lib/use-morph.ts` | Local addition: the morph engine (source rect → popup box on a spring tier, exit hold for Base UI's unmount), geometry after beUI's popover; goo runs on its own `spring.goo` with a blur of at least 8px that eases to nothing as the surface lands, so the neck pinches off; a close-only `exit` override, an `instant` predicate that ends a run at once, press points from clicks, context menus and long presses (a droplet source for `from="pointer"`), cleanup when the popup unmounts mid-run; focus goes back to the trigger when a closed popup that held it is gone and Base UI skipped the return (the exit hold makes a menu read the pointer leaving the inert popup as a hover close) |
171
+ | — (new) | `lib/reduced-motion.ts` | Local addition: `useReduceMotion`, the one reduced-motion read for the morph layer and the liquid indicators — the OS setting, or a `MotionConfig` asking for it (motion's `useReducedMotionConfig` alone ignores the OS without `<MotionConfig reducedMotion="user">`) |
172
+ | — (new) | `lib/field-classes.ts` | Local addition: the field ladder FF repeats per text field (rest → muted fill on hover or a group's `data-active` → card fill on focus; `data-invalid` keeps a destructive ring), as `fieldVariants` + `fieldIconClasses`, shared by Input, Combobox and Sidebar's input |
173
+ | — (new) | `lib/date-locale.ts` | Local addition: what the date and time pickers read off a react-day-picker (date-fns) locale — the hour cycle (Intl), typed dates, times and months in the locale's formats (date-fns `parse` with `P`…`PPPP`), field names for column labels (`Intl.DisplayNames`) |
174
+ | — (new) | `lib/scroll-column.tsx` | Local addition: `ScrollColumn`, the scrolling option list beside a calendar (months, years, hours, minutes) or a strip above it: the pick gets a dot, a run of `inView` options one band, the followed option stays centred; fluid hover, listbox semantics, arrow keys; `dividedColumns` puts a hairline between neighbours |
175
+ | — (new) | `lib/slot.ts` | Local addition: Dialog's `render` / `asChild` slot shape (`SlotProps`, `slotRender`), shared by every overlay part that wraps a control |
176
+ | — (new) | `lib/morph-part.tsx` | Local addition: `MorphPart` (shared parts over a motion `layoutId`, after motion-primitives' morphing dialog) and the scope contexts overlays provide |
177
+ | — (new) | `lib/goo-indicator.tsx` | Local addition: `GooIndicator` (melts between items, a lingering blob follows it) and `GooLayer` (goo over the shapes' coverage, filled with their own token, on only while moving), on the engine's `GOO_MATRIX` / `GOO_BLUR_RATIO` |
178
+ | — (new) | `lib/morph-layers.tsx` | Local addition: `MorphSurface`, the renderer (shapes layer with beUI's goo filter plus a `--shadow-color` hairline around the melted shape, surface background and shadow, clipped content) |
179
+
180
+ ### Styles
181
+
182
+ | Upstream | Local |
183
+ |---|---|
184
+ | `app/globals.css` §1 tokens | `tokens/colors.css`, with HEX converted to oklch. The surface ladder and shadows live in `primitives/surface/surface.css`. Local addition: the `--status-*` roles (toast states), by reference to the badge palette, `--destructive` and `--brand`. |
185
+ | `app/globals.css` §2 theme switching | `base.css` (`color-scheme`, `--overlay`) and `surface.css` (`--shadow-N` per scheme) |
186
+ | `app/globals.css` §3 `@theme inline` | `preset.ts` `theme` |
187
+ | `app/globals.css` `--fs-*` + `@utility text-*` | `tokens/typography.css` `--text-*` and `preset.ts` `utilities` |
188
+ | `app/globals.css` §4–6 | `base.css` |
189
+ | `registry/default/lib/springs.ts` (as CSS) | `tokens/motion.css` (plus `--duration-goo` / `--duration-goo-exit`) |
190
+
191
+ ### Items
192
+
193
+ Every item goes through the same mechanical pass, so the tables below list
194
+ only what is specific to it:
195
+
196
+ - `"use client"` dropped (no RSC here).
197
+ - Imports rewritten: `@/lib/*` → `../../lib/*`, `@/hooks/*` →
198
+ `../../lib/*`, `SizeProvider` from `@/lib/size-context` →
199
+ `../../primitives/sizes`, `@/lib/elevated` → `../../primitives/surface`,
200
+ `@/components/ui/scroll-area` → `../../primitives/scroll-area`,
201
+ `@/components/ui/fluid-hover-highlight` → `../../lib/fluid-hover-highlight`,
202
+ `@/registry/radix/*` → the Base-flavor sibling, `framer-motion` →
203
+ `motion/react`, `next/link` → a plain anchor.
204
+ - A local `interface <Item>Props` whose members are each declared on one line
205
+ with the FF docs API-table text (modo's parser lists only members declared
206
+ in the interface body), modo TSDoc written from the FF docs page, and
207
+ `export default <Item>`. Upstream's named exports are kept.
208
+
209
+ #### primitives
210
+
211
+ | Item | Upstream | Local | Specific modifications |
212
+ |---|---|---|---|
213
+ | `Elevated` | `registry/default/lib/elevated.tsx` (+ `lib/surface-context.tsx`; docs "Surfaces") | `primitives/surface/index.tsx` | `.Provider` static (= `SurfaceProvider`); `useSurface` / `surfaceClasses` re-exported; `className`/`children` re-declared; `surface.css` co-located (modo injects it) |
214
+ | `ScrollArea` | `registry/base/scroll-area.tsx` (docs "Scrollbars") | `primitives/scroll-area/index.tsx` | the root's forwardRef element type written out as `HTMLDivElement` (the parser needs a first type argument without `<`/`,`); `viewportClassName` documented; `.Bar` static (= `ScrollBar`) |
215
+ | `SizeProvider` | `registry/default/lib/size-context.tsx` + `app/docs/sizes/page.tsx` | `primitives/sizes/index.tsx` | upstream's `SizeProvider`, moved here from the vendored context so modo documents it (sized components import it from here), with the hooks, maps and types re-exported; the live demos become static examples (Select/TabsSubtle/InputGroup/CheckboxGroup/Dropdown swapped for Button/Badge/Switch/Tabs, token-inspector overlay dropped); the token table and type scale are written out rather than rendered from `sizeMap` / `typeScale` |
216
+ | `FluidHover` | `app/docs/fluid-hover/{page,demos}.tsx` over `registry/default/hooks/use-fluid-hover.ts` + `fluid-hover-highlight.tsx` | `primitives/fluid-hover/index.tsx` | new container (`FluidHover` + `FluidHover.Item`) around the hook/highlight pair so examples need no hooks; `items`, `renderItem`, `disabledIndices`, `columns`, `highlightClassName` are the container's own API, `axis` and `gapClick` pass through to the hook; row classes are upstream's `rowClass` with per-axis variants; the two scripted-cursor demos are dropped |
217
+ | `Motion` | `registry/default/lib/springs.ts` + `app/docs/motion/page.tsx` | `primitives/motion/index.tsx` | `Motion` is local demo code (FF ships none): it plays one tier's enter spring and exit tween on its children, adding `defaultShow`, `sameExit` and `reducedMotion`; `spring` / `exitFallbackMs` re-exported from the vendored `springs.ts`; the ball-on-track and fake-modal visuals become show/hide of arbitrary children |
218
+ | `Morph` | — (local; see [Morph sources](#morph-sources)) | `primitives/morph/index.tsx` | a docs stage like `Motion`: a trigger and a panel playing one `from` × `effect` pair over `lib/use-morph.ts`; static `.Part` (= `MorphPart`); also documents the liquid indicators (`lib/goo-indicator.tsx`) |
219
+ | `Code` | — (local; FF has no code item) | `primitives/code/index.tsx` | sugar-high tokens colored by the `--syntax-*` tokens, one surface step above its substrate; also the docs chrome's Code slot |
220
+ | `Craft` | — (local; after the before/after pairs on fluidfunctionalism.com/docs/skill) | `primitives/craft/index.tsx` (+ `demos.tsx`) | a docs stage: a bad and a good version side by side with a one-line verdict each; the bad sides live in `demos.tsx` and break one rule each on purpose, the good sides are this package's items |
221
+
222
+ #### components
223
+
224
+ | Item | Upstream | Local | Specific modifications |
225
+ |---|---|---|---|
226
+ | `Accordion` | `registry/base/accordion.tsx` | `components/navigation/accordion/index.tsx` | open state runs through `useControllableState` (see [Behavior additions](#behavior-additions)); standalone `Accordion` accepts `highlight` and passes it to its items as their default; the panel morphs out of its row (a shape grows from the press point into the item, content clipped by it, layout height on its bottom edge; the `"item"` tint is that shape melted into the row through `GooFilter`), `effect` / `tier` on `Accordion.Content`; the group's hover highlight and focus ring snap with reflowed rows; statics `.Group/.Item/.Trigger/.Content` |
227
+ | `AlertDialog` | — (local) | `components/overlays/alert-dialog/index.tsx` | Base UI AlertDialog root (always modal, no outside-press dismissal) over Dialog's own parts and `DialogState`, so it morphs like Dialog; `AlertDialog.Content` drops the ✕ by default; statics as Dialog's |
228
+ | `Badge` | `registry/default/badge.tsx` | `components/badge/index.tsx` | `variant` re-declared (it came from cva's `VariantProps` upstream); changes animate: a new label or variant springs the width (inner content span, `spring.moderate`) while the label blur-crossfades, a new color fades, the dot scales in and out; "Changes" example over a local `change-demo.tsx` stage |
229
+ | `Button` | `registry/base/button.tsx` | `components/button/index.tsx` | `variant`/`disabled`/`children` re-declared; extra Sizes / Active / As-child / Loading morph examples; `loading` folds the surface into a circle around the spinner and back (`spring.moderate`, box unchanged), label and spinner crossfade, `aria-busy`, the disabled dim fades; "Loading morph" example over a local `loading-demo.tsx` stage; also serves modo's chrome Button (`shell.Button`) |
230
+ | `Calendar` | — (local, on react-day-picker) | `components/inputs/date-time/calendar/index.tsx` (+ `use-month-window.ts`) | DayPicker props pass through (as shadcn's Calendar); styled through `classNames` and custom `Month` / `Day` / `DayButton` (no react-day-picker stylesheet); one scrolling list of months rendered as a 25-month window that moves as you scroll; a pinned weekday row, muted weekends, today's dot; FF's fills (the pick `bg-foreground text-background` like the primary button, a range `bg-active`, its preview `bg-hover`); month and year columns that mark the month being read and jump on click, before or after the days (`columnsSide`), turning into strips when narrow; `extraColumns` / `extraColumnsSide` for the date picker's time |
231
+ | `Card` | `registry/default/card.tsx` | `components/card/index.tsx` | `next/link` → plain anchors (stretched card link, `CardButton`'s `href` form); statics `.Group/.Header/.Title/.Description/.Action/.Content/.Footer/.Media/.Image/.Eyebrow/.Feature/.Button` |
232
+ | `CheckboxGroup` | `registry/base/checkbox-group.tsx` | `components/inputs/checkbox-group/index.tsx` | uncontrolled mode + group-level callback; item `checked`/`onToggle` optional, falling back to group context; arrow-key focus guarded for `noUncheckedIndexedAccess`; shared `useSelectionRuns`, picks melt and split through the goo; static `.Item` |
233
+ | `ColorPicker` | `registry/default/color-picker.tsx` | `components/inputs/color-picker/index.tsx` | `@/registry/radix/{slider,tooltip}` → the Base siblings `../slider` / `../../overlays/tooltip`; `noUncheckedIndexedAccess` guards on regex captures, `rgb()`/`hsl()`/`oklch()` parts, `itemRects[i]` and the Slider's array value; static `.Popover`; uncontrolled support was already upstream (verified, kept); `ColorPicker.Popover` (goo on `spring.goo`; `from` / `effect` / `hideSource` / `tier`) and the format menu morph out of their triggers, replacing the motion wrappers and the ignored `actionsRef` |
234
+ | `Combobox` | `registry/base/combobox.tsx` | `components/inputs/combobox/index.tsx` | root de-generified (the parser needs a plain function plus a same-file `interface ComboboxProps`): items gained an index signature, `value`/`defaultValue` are `string \| string[]` and `onValueChange` is a union of the two handler shapes; `ComboboxValue<Multiple>` still exported; `creatable` added; `React.*` type refs replaced by named type imports; statics `.Input/.Chips/.Content/.List/.Item/.Empty`; the popup morphs out of the field (morph layer, goo on `spring.goo`; `from` / `effect` / `hideSource` / `tier` on `Combobox.Content`), replacing the `scaleY` wrapper and the `actionsRef` deferred unmount |
235
+ | `Command` | — (local) | `components/overlays/command/index.tsx` | Base UI Autocomplete rendered inline in Dialog's panel (`position="top"`, no ✕), morphing from its trigger with goo; grouped/flat command data filtered by label + keywords; one fluid highlight follows Base UI's highlight; ⌘K/Ctrl+K `hotkey` (one palette answers a press); fresh palette per open; statics `.Trigger/.Content/.Palette/.Shortcut`; `CommandList`/`CommandListRoot` exported for Search |
236
+ | `ContextMenu` | — (local; see [Morph sources](#morph-sources)) | `components/overlays/context-menu/index.tsx` | Base UI ContextMenu (right click, long press) with the morph layer growing from the press point (`from="pointer"` default, goo on `spring.goo`); rows are Dropdown's `MenuItem`, `Dropdown.Label`, `Dropdown.Separator` with fluid hover; `ContextMenu.Item` adds `shortcut`; `size` on the root; statics `.Trigger/.Content/.Item/.Label/.Separator` |
237
+ | `Dialog` | `registry/base/dialog.tsx` | `components/overlays/dialog/index.tsx` | `DialogProps` and `DialogContentProps` fully re-declared (the slot props now come from `lib/slot.ts`); statics attached as expando properties (`Dialog.Trigger = …`), so `<Dialog.Content>` types without a cast; FF's "With a sidebar" example replaced by "Surfaces inside a dialog" (written before the Sidebar was ported); the panel morphs out of its trigger (morph layer, goo on `spring.goo`; `from` / `effect` / `hideSource` / `tier` on `Dialog.Content`), the backdrop fades with the morph's progress, the root holds the open state and scopes `Morph.Part` pairs |
238
+ | `Dropdown` | `registry/base/dropdown.tsx` | `components/overlays/dropdown/index.tsx` | uncontrolled selection and uncontrolled search filtering (below); `items[next].focus()` guarded; statics `.Menu/.Trigger/.Content/.Item/.Label/.Separator/.Search/.Empty`; FF's "Create from the query" section skipped — adding a row to the list is real consumer state; the popup menu morphs out of its trigger (morph layer, goo on `spring.goo`; `from` / `effect` / `hideSource` / `tier` on `Dropdown.Content`), replacing the `scaleY` wrapper and the ignored `actionsRef`; popup rows hand activation to the Menu item's `onClick` so Enter / Space pick |
239
+ | `DatePicker`, `DateRangePicker`, `TimePicker`, `DurationPicker`, `MonthPicker` | — (local; props after shadcn's date picker and utegsk/ui's pickers) | `components/inputs/date-time/<id>/index.tsx` (+ the group's `picker-shell.tsx`, `time-columns.tsx`) | a field on the field ladder whose panel morphs out of it with Combobox's options (goo by default; `from` / `effect` / `hideSource` / `tier`, in place with `effect="morph"` + `hideSource`), or a bottom `Sheet` under 640px; one hour column with AM/PM on a 12-hour clock; `columnsSide` / `timeColumnsSide`; the panel header types the value in the locale's formats; `value` / `defaultValue` / `onValueChange` with `null` for empty — a `Date`, react-day-picker's `DateRange`, `"HH:mm"`, a date-fns `Duration`, a month's first day; `locale` is a react-day-picker locale throughout |
240
+ | — | `registry/default/menu-item.tsx` | `components/overlays/dropdown/menu-item.tsx` | a row with no `checked` derives it from a self-managed panel and toggles the panel's state; `sourceIndex` carries the authored index so selection survives filter re-indexing |
241
+ | — | `registry/default/dropdown-search.tsx` | `components/overlays/dropdown/dropdown-search.tsx` | `filter` mode: `value`/`onValueChange` optional, the field reads and writes the query held by the panel (`DropdownFilterContext`) |
242
+ | `InputCopy` | `registry/default/input-copy.tsx` | `components/inputs/input-copy/index.tsx` | `@/registry/radix/tooltip` → `../../overlays/tooltip`; `useRef<ReturnType<typeof setTimeout> \| null>(null)` (React 18 overloads); defaults spelled into the prop prose; the tooltip comment's "Radix" reads "Base UI"; the copied state morphs: the glyph's two sheets melt through the goo filter into a status-tinted disc behind the check (✕ on failure) and split back |
243
+ | `Input`, `Label`, `InputGroup` | `registry/default/input-group.tsx` | `components/inputs/input/index.tsx` | `InputField` split into `Input` (the ringed box, standalone: own hover outside a group, `invalid`, `size`) and `Label` (a plain `<label>`, rendered through `Field.Label` in a field), composed back by `InputGroup.Field`; item renamed `input`, `Input` the default export; uncontrolled mode (`InputHTMLAttributes`' own `defaultValue` omitted in favour of the local one); static `.Field` |
244
+ | `InputMessage` | `registry/default/input-message.tsx` | `components/inputs/input-message/index.tsx` | uncontrolled twins for value, files, queue and status (below); `FilePreviewTile` turned into a `forwardRef` (it exits inside `<AnimatePresence mode="popLayout">`, which measures through a ref); guards on `queue[0]`, `history[i]`, `suggestions[i]` and the `moveQueued` swap; FF's Playground section skipped and the surrounding transcript dropped (`ChatMessage` is not ported) |
245
+ | — | `registry/default/file-thumbnail.tsx` | `components/inputs/input-message/file-thumbnail.tsx` | the lazy `import("pdfjs-dist")` is kept as-is — verified against modo's esbuild settings, it lands in a ~790 kB chunk fetched only when a PDF is attached; the worker still comes from jsDelivr, so no bundler-side worker config is needed; the `@next/next/no-img-element` eslint-disable dropped |
246
+ | `Popover` | — (local; after beUI's gooey popover and motion-primitives' morphing popover, see [Morph sources](#morph-sources)) | `components/overlays/popover/index.tsx` | Base UI Popover with the morph layer (goo by default); `openOnHover` on the trigger; statics `.Trigger/.Content/.Title/.Description/.Close` |
247
+ | `PreviewCard` | — (local; after beUI's gooey popover, see [Morph sources](#morph-sources)) | `components/overlays/preview-card/index.tsx` | Base UI PreviewCard with the morph layer (goo from the link on `spring.goo`); `delay` / `closeDelay` / `render` on the trigger, a token-styled link by default; `from` without `'pointer'`; statics `.Trigger/.Content` |
248
+ | `RadioGroup` | `registry/base/radio-group.tsx` | `components/inputs/radio-group/index.tsx` | uncontrolled index and value modes; group context carries a `selectIndex` setter every item calls, so items need no `selected`/`onSelect`; arrow-key focus guarded; selected background is a `GooIndicator`; static `.Item` |
249
+ | `Search` | — (local; after beUI's morphing search, see [Morph sources](#morph-sources)) | `components/inputs/search/index.tsx` | pill or icon button springing its own width in place (`spring.slow`), growing over its neighbors (`align`); with `items`, Autocomplete suggestions ooze out of the field via the morph layer (goo on `spring.goo`, `effect`/`tier`) with Command's rows; Escape closes → clears → collapses; blur collapses when empty; `shortcut`; `<search>` landmark + form, `onSubmit` |
250
+ | `Select` | `registry/base/select.tsx` | `components/inputs/select/index.tsx` | members re-declared one per line (the parser drops members whose type spans lines); upstream's uncontrolled `defaultValue` verified and kept rather than rerouted through `useControllableState`; expando statics `.Trigger/.Content/.Item/.Group/.Label/.Separator`; the popup morphs out of its trigger (morph layer, goo on `spring.goo`; `from` / `effect` / `hideSource` / `tier` on `Select.Content`), replacing the `scaleY` wrapper and the `actionsRef` deferred unmount |
251
+ | `Sheet` | — (local; replaces beUI's bottom sheet and drawer, see [Morph sources](#morph-sources)) | `components/overlays/sheet/index.tsx` | Base UI Drawer (swipe to dismiss, snap points, `Drawer.Viewport` for the swipe) with the morph growing from the sheet's own edge; a swipe-dismissed sheet slides on off its edge from the release point (the morph's `exit`); `side`, `snapPoints`, `defaultSnapPoint`; Dialog's parts and `DialogState` (Base UI's Drawer parts are Dialog's); statics as Dialog's plus `.Provider` / `.Indent` |
252
+ | `Sidebar` | `registry/base/sidebar.tsx` (+ `registry/default/sidebar-core.tsx`, `sidebar-menu.tsx`; `app/globals.css` `.scroll-divider`) | `components/navigation/sidebar/index.tsx` (+ `sidebar-core.tsx`, `sidebar-menu.tsx`, `sidebar.css`) | the mobile sheet is the DS's `Sheet` (goo from its edge, swipe to dismiss) instead of a Dialog with a framer slide; no collapsed peek: `openOnHover` on the provider opens the sidebar in flow while the pointer is between its edge and the far side of a trigger beside it, the trigger switching between open and on hover (`panel-*-dashed` glyphs added to the icon context); the active row backgrounds are `GooIndicator`s and the hover highlight the liquid `FluidHoverHighlight`; uncontrolled selection (`value` on the buttons, `value` / `defaultValue` / `onValueChange` on `Sidebar`) and collapsible rows (`Sidebar.MenuItem collapsible`, below); statics `.Provider/.Trigger/.Rail/.Inset/.Input/.Header/.Content/.Footer/.Separator/.Group/.GroupLabel/.GroupAction/.GroupActions/.GroupContent/.Menu/.MenuItem/.MenuButton/.MenuAction/.MenuActions/.MenuBadge/.MenuSkeleton/.MenuSub/.MenuSubItem/.MenuSubButton` through a `SidebarComponent` cast; `fontVariationSettings` → `weight-*`, `text-[Npx]` → type roles, hex focus ring → `ring-focus-ring`, `duration-80\|160\|240` → tiers |
253
+ | `Slider` | `registry/base/slider.tsx` | `components/inputs/slider/index.tsx` | `var(--color-accent)` → `var(--accent)` (no `--color-*` aliases here); uncontrolled support on the public wrapper, with both engines still fully controlled; `SliderProps extends Omit<SliderEngineProps, "value" \| "onChange">`; React 18 ref types (`MutableRefObject`); indexed reads of the value/step/pip arrays asserted |
254
+ | `Switch` | `registry/base/switch.tsx` | `components/inputs/switch/index.tsx` | uncontrolled support; every in-body read goes through the resolved `isChecked` and a `toggle()` that also calls `onToggle`; the thumb is a `GooIndicator` in Base UI's Thumb (stretches as it travels), dragged x held in state |
255
+ | `Table` | `registry/default/table.tsx` | `components/table/index.tsx` | statics `.Header/.Body/.Row/.Head/.Cell`; `TableProps` / `TableRowProps` exported |
256
+ | `Tabs` | `registry/base/tabs.tsx` | `components/navigation/tabs/index.tsx` | `defaultSelectedIndex` added as the index-mode twin of the existing `defaultValue`; indicator and hover pill are `GooIndicator`s (the hover pill returns to the active tab as it fades); statics `.List/.Item/.Panel` |
257
+ | `TabsSubtle` | `registry/base/tabs-subtle.tsx` | `components/navigation/tabs-subtle/index.tsx` | uncontrolled mode; panels may be authored inside the root (below); the tab list's ref write goes through `MutableRefObject`; selected and hover pills are `GooIndicator`s; statics `.Item/.Panel` |
258
+ | `ThinkingIndicator` | `registry/default/thinking-indicator.tsx` | `components/thinking-indicator/index.tsx` | upstream prop JSDoc kept (it already matches the FF API table); the `.shimmer-text` rule it rides lives in `base.css`; local `variant="goo"` (three dots merging and splitting through `GooFilter`, plain label; default stays `shimmer`); reduced motion also follows MotionConfig |
259
+ | `Toast` | — (local; after Sileo, see [Morph sources](#morph-sources)) | `components/overlays/toast/index.tsx` | Base UI Toast (queue, timers, swipe, announcements) with Sileo's pill melting into a body through the shared `GooFilter`; autopilot expand/collapse, six states on the `--status-*` tokens, promise toasts; `Toast` is the provider + viewport, `Toast.Trigger` shows one, `useToast()` the imperative API |
260
+ | `Tooltip` | `registry/base/tooltip.tsx` | `components/overlays/tooltip/index.tsx` | `className`/`children` docs filled in; statics `.Provider` / `.PortalContainer`; the slide + fade replaced by the morph layer (goo from the trigger on `spring.goo`, `from` / `effect` / `tier` props; `followCursor` tooltips fade); a hand-off between grouped tooltips skips the morph (Base UI's `data-instant`) |
261
+
262
+ #### blocks
263
+
264
+ | Item | Upstream | Local | Specific modifications |
265
+ |---|---|---|---|
266
+ | `SidebarApp` | the app-sidebar.tsx and page.tsx FF's preset generator emits for `sa1FQfCxH6` (`lib/preset/sidebar-install.ts` @ `c367a0d066bc10e663b3d269bc40539ea8417b25`, later than the pin: the generator postdates it) | `blocks/sidebar-app/index.tsx` (+ `nav-data.ts`) | the selected thread rides `Sidebar`'s `value` and shows in the topbar; the switcher keeps its own pick (`defaultCheckedIndex`); the footer's raw icon buttons are the ghost `Button`; `variant`, `side`, `openOnHover`, `contained` props |
267
+ | — | `registry/blocks/sidebar-workspace-header.tsx` | `blocks/sidebar-app/workspace-header.tsx` | `fontVariationSettings` → `weight-semibold`, `text-[13px]` / `text-[10px]` → `text-body` / `text-micro-compact`; `defaultCheckedIndex` passed through; the peek-only trigger over the tile dropped |
268
+ | — | `registry/blocks/sidebar-user-footer.tsx` | `blocks/sidebar-app/user-footer.tsx` | `text-[13px]` → `text-body` |
269
+ | — | `registry/blocks/sidebar-app/search-field.tsx` | `blocks/sidebar-app/search-field.tsx` | `text-[11px]` → `text-micro` |
270
+ | — | `registry/blocks/sidebar-app/inset-topbar.tsx` | `blocks/sidebar-app/inset-topbar.tsx` | the trigger's peek-time fade dropped (the sidebar opens on hover in flow, so the trigger stays) |
271
+ | — | `registry/default/lib/sidebar-menu-grid.ts` | `blocks/sidebar-app/menu-grid.ts` | the width reads `--anchor-width` only; the `-ml-1` shift dropped (it would move the morph surface's content off its background) |
272
+
273
+ `Select` in `modo.components.tsx` is not an item: modo's docs chrome renders its Select slot
274
+ with a flat `value` / `onChange` / `options` contract
275
+ (`lib/src/lib/slots.tsx`), and this adapter maps it onto
276
+ `Select.Trigger` / `Select.Content` / `Select.Item` so the chrome runs on the
277
+ design system's own Select. The
278
+ `components/inputs/select` item keeps the upstream API untouched.
279
+
280
+ `DocsNav` in `modo.components.tsx` is the chrome's Sidebar slot, pinned as
281
+ `shell.Sidebar` (by name alone modo would adopt the documented `Sidebar`, an
282
+ app shell that needs its provider). Each aside is an inset `Sidebar` with its
283
+ own provider, and `global.css` turns the content between them into the inset
284
+ card (the lib's grid columns become `auto`, so they follow the sidebars' width
285
+ springs). Each provider has `openOnHover`, and its `Sidebar.Trigger` sits in
286
+ the card's corner: the trigger, `[` for the nav and `]` for the panel, switch
287
+ a side between open and on hover, and on hover it opens once the pointer is
288
+ in the column under the trigger (so a browser's own edge hover sidebar stays
289
+ out of the way). `DocsNav` owns those keys, so a demo sidebar in the content keeps them only
290
+ while it has focus. Below md each side is a drawer; the panel starts on hover
291
+ below xl. A section of links is a collapsible `Sidebar.Group` with one
292
+ `Sidebar.Menu`, so the current page, the TOC's current heading and the hover
293
+ highlight melt from link to link; a panel section (Theme) keeps its control
294
+ under the group label.
295
+
296
+ `ThemeSwitcher` in `modo.components.tsx` is not an item either: it is the `Theme` entry in
297
+ `modo.config.ts` `panel.items`, a `Select` of Light / Dark / System (each row
298
+ with its icon, the trigger showing the active one) that reads and sets
299
+ `window.__uiTheme` and re-reads it on `ui:themechange`. All switching logic lives in the pre-paint controller
300
+ described under **Tailwind → UnoCSS**, so the control holds no theme state of
301
+ its own. `demo/scripts/run-design-systems.ts` appends its own `Switcher` panel
302
+ item to whatever a design system declares, so both show up under the runner.
303
+
304
+ ## Tier mapping
305
+
306
+ The tiers mirror the FF docs navigation. FF's "System" pages become
307
+ `primitives/`; its "Components" become `components/`. Its sidebar preset
308
+ becomes the one block.
309
+
310
+ | Tier | Item | Upstream source | FF docs page |
311
+ |---|---|---|---|
312
+ | primitives | `surface` (Elevated, `.Provider`, `useSurface`) | `lib/elevated.tsx` + `lib/surface-context.tsx` | surfaces |
313
+ | primitives | `scroll-area` (ScrollArea) | `base/scroll-area.tsx` | scrollbars |
314
+ | primitives | `sizes` (SizeProvider) | `lib/size-context.tsx` | sizes |
315
+ | primitives | `fluid-hover` (FluidHover) | `hooks/use-fluid-hover.ts` + `fluid-hover-highlight.tsx` | fluid-hover |
316
+ | primitives | `motion` (Motion) | `lib/springs.ts` | motion |
317
+ | primitives | `code` (Code) | — (local, sugar-high) | — |
318
+ | primitives | `morph` (Morph) | — (local, `lib/use-morph.ts`) | — |
319
+ | primitives | `craft` (Craft) | — (local, bad/good pairs) | skill (before/after) |
320
+ | components | accordion, button, checkbox-group, combobox, dialog, dropdown, radio-group, select, slider, switch, tabs, tabs-subtle, tooltip | `registry/base/<id>.tsx` | same slug |
321
+ | components | badge, card, color-picker, input-copy, input-message, table, thinking-indicator | `registry/default/<id>.tsx` | same slug |
322
+ | components | `input` (Input, Label, InputGroup) | `registry/default/input-group.tsx` | input-group |
323
+ | components | alert-dialog, command, context-menu, popover, preview-card, search, sheet, toast | — (local, see [Morph sources](#morph-sources)) | — |
324
+ | components | calendar, date-picker, date-range-picker, time-picker, duration-picker, month-picker (group `inputs/date-time`) | — (local, on react-day-picker) | — |
325
+ | components | sidebar | `base/sidebar.tsx` + `default/sidebar-core.tsx` + `default/sidebar-menu.tsx` | sidebar |
326
+ | blocks | `sidebar-app` (SidebarApp) | the preset generator's output + `blocks/sidebar-*` | sidebar, preset `sa1FQfCxH6` |
327
+
328
+ Button lives in `components/` to mirror the FF docs, and `modo.config.ts`
329
+ points `shell.Button` at it explicitly (an explicit path skips modo's tier
330
+ check). The chrome renders its icon buttons as `<Button variant="ghost"
331
+ size="icon-sm">`, which works because `icon-sm` is upstream's legacy alias for
332
+ `icon-compact`.
333
+
334
+ Compound components export their root by default, with the parts attached
335
+ as statics (`Select.Item`, `Accordion.Trigger`, …). Upstream's named exports
336
+ are kept as well.
337
+
338
+ ## Omitted
339
+
340
+ These are not ported: ThinkingSteps, CommandMenu, ChatMessage and
341
+ AskUserQuestions. CommandMenu is the FF docs site's own chrome, which modo's
342
+ shell already provides; ThinkingSteps, ChatMessage and
343
+ AskUserQuestions are the agent-chat composites, left out of this port's scope.
344
+ No ported file imports any of them.
345
+
346
+ The files only they, or the FF docs site's nav, use are left out too:
347
+ `mobile-drawer`, `nav-item`, `nav-menu`, `lib/theme-context.tsx` (the
348
+ `ThemeSwitcher` panel item and vite.ts's pre-paint controller take its
349
+ place) and the blocks other than the sidebar's (`queued-stack`,
350
+ `dialog-sidebar`). Inside kept files, the same rule removed
351
+ `SURFACE_HOVER_BG`, `SURFACE_HOVER_SHADOW` and `surfaceHoverClasses` from
352
+ `surface-classes.ts` (only the Sidebar docs' callout cards used them; the
353
+ callouts are not ported).
354
+
355
+ The same goes for the docs-site-only CSS in `app/globals.css`: `.xl-fade-*`,
356
+ `.inview-fade-block`, `.bento-*` and
357
+ `.shiki`, plus the Tailwind directives, the self-hosted Inter `@font-face`
358
+ and the Radix-only scroll-lock margin fix (`body[data-scroll-locked]`).
359
+
360
+ `surface.css` does keep the inset shadow variants (`--shadow-light-2-inset`,
361
+ `--shadow-light-3-inset` and the per-scheme `--shadow-2-inset` /
362
+ `--shadow-3-inset` aliases), but no ported item consumes them and
363
+ `preset.ts` maps no utility to them.
364
+
365
+ ## Tailwind → UnoCSS
366
+
367
+ `uno.config.ts` uses `presetWind4` (UnoCSS's Tailwind v4 preset) and
368
+ `preset.ts` (the theme and token utilities the package ships) with
369
+ `outputToCssLayers`. `vite.ts` adds the UnoCSS Vite plugin to modo's Vite
370
+ config. `scripts/build-css.ts` runs the same config over `dist/` for the
371
+ prebuilt `styles.css`, and turns `preset.ts` into `tailwind.css`.
372
+
373
+ - **Dark mode.** `dark: 'class'` produces `.dark .dark\:x`, the same "inside a
374
+ `.dark` ancestor" contract as FF's `@custom-variant dark (&:is(.dark *))`.
375
+ modo ships no theme of its own, so `vite.ts` injects a small controller into
376
+ `index.html` (`transformIndexHtml`, `head-prepend`). Before first paint it
377
+ puts `light` or `dark` on `<html>` — the stored preference, or the OS one
378
+ under `system` — which is also what flips every `light-dark()` token through
379
+ `color-scheme`. It exposes `window.__uiTheme` (`get` / `set`), persists the
380
+ choice in `localStorage` under `modo-ui-theme`, and fires `ui:themechange`
381
+ on every apply, including when the OS flips underneath `system`.
382
+ - **Load order.** The same plugin prepends `import 'virtual:uno.css'` to
383
+ modo's `\0virtual:modo-config-css` module, so UnoCSS's
384
+ `@layer properties, theme, base, default` statement lands before
385
+ `global.css` (and the `base.css` it imports) and fixes the order of the
386
+ layer its `@layer base` rules join.
387
+ `vite.ts` is loaded by Node with type stripping, so it may only use erasable
388
+ TypeScript and hands `uno.config.ts` to UnoCSS by absolute path.
389
+ - **Colors.** Theme colors point at the CSS variables directly (for example
390
+ `accent: 'var(--accent)'`, `surface.1..8`). wind4 applies opacity modifiers
391
+ to `var()` colors with `color-mix(in srgb, …)`, so `bg-accent/12` works on
392
+ the `light-dark()` tokens.
393
+ - **Shadows.** `shadow-surface-N` maps to `var(--shadow-N)`, which is
394
+ re-pointed per scheme in `surface.css`.
395
+ - **Radius.** wind4's default scale stays (`rounded-lg` = 8px). shape-context
396
+ pairs `rounded-lg` with 8px JS radii, so the scale must not follow
397
+ `--radius`.
398
+ - **Font.** `font.sans` repeats the literal family from
399
+ `tokens/typography.css` rather than `var(--font-sans)`: wind4 emits theme
400
+ keys as same-named variables, and a self-reference would be a cycle.
401
+ - **Type roles.** `text-display|title|subtitle|body|caption` is a custom rule
402
+ reading `var(--text-<role>)`.
403
+ - **Transitions.** wind4's arbitrary `transition-[a,b]` only accepts CSS
404
+ properties it knows. `stroke-width` (icon stroke on hover) and
405
+ `font-variation-settings` (the Inter weight ladder) are added through
406
+ `theme.property`.
407
+ - **Layers.** UnoCSS declares `@layer properties, theme, base, default` and
408
+ puts its reset in `base`. `base.css`'s `@layer base` rules join that layer
409
+ after the reset, so utilities (in `default`) still win. This is the same
410
+ arrangement as Tailwind's base/utilities split.
411
+ - **Content.** UnoCSS scans `{lib,primitives,components,blocks}/**/*.{ts,tsx,mdx}`
412
+ and `modo.components.tsx` from disk (`content.filesystem`, and the same
413
+ globs in `content.pipeline.include`, since the default pipeline skips
414
+ `.ts`). Two reasons:
415
+ - modo serves items pre-bundled, so the sources never pass through Vite's
416
+ transform pipeline;
417
+ - example code exists only in each item's `examples.mdx`.
418
+
419
+ The class-name literal tables (`SURFACE_BG`, `sizeMap`, …) do the same job
420
+ for UnoCSS's extractor that they do for Tailwind's scanner.
421
+ - **Focus.** modo's structural `shell.css` draws an unlayered `*:focus-visible`
422
+ outline. `global.css` reverts it with `outline: revert-layer` (and
423
+ `outline-offset: revert-layer`), handing focus styling back to `@layer base`
424
+ and the utilities, so FF's ring (1px `--focus-ring`) is the only one drawn.
425
+
426
+ Every Tailwind idiom FF uses has a wind4 equivalent, among them:
427
+ - named groups (`group-[.is-active]/row:`)
428
+ - `has-data-[…]:`
429
+ - arbitrary variants (`[[data-side=top]_&]:`, `[&>div[style]]:!block`)
430
+ - `supports-[…]:`
431
+ - arbitrary properties (`[text-box:…]`, `[--popup-enter-y:-4px]`)
432
+ - typed arbitrary values (`ring-[color:…]`)
433
+ - `duration-80`, `!h-auto`
434
+ - `transition-[color,stroke-width]`, `transition-[font-variation-settings]`
435
+ - `bg-accent/12` → `color-mix`, `shadow-surface-3`, `dark:bg-surface-2`
436
+ - the type-role rule and wind4's unchanged `--radius-lg: 0.5rem`
437
+
438
+ No upstream class string had to change. The current run covers 45 files and
439
+ 3299 class tokens (561 unique), and all of them generate.
440
+
441
+ ## `cn`
442
+
443
+ `lib/utils.ts` exports `cn = createCn({ extend: { classGroups: {
444
+ 'font-size': ['text-display', …] } } })` from `cn/config`. `cn` is a compiled
445
+ drop-in for clsx + tailwind-merge with the same `extend` shape.
446
+
447
+ The font-size extension keeps upstream's fix. Without it, `text-body` would
448
+ be read as a text *color*, and `cn('text-body', 'text-muted-foreground')`
449
+ would drop the size.
450
+
451
+ ## Behavior additions
452
+
453
+ Base UI components work both controlled and uncontrolled. The FF wrappers
454
+ that were controlled-only gain a `default*` prop, backed by
455
+ `lib/use-controllable-state.ts`: a component is controlled while its
456
+ value prop is not `undefined`, and it reports every change through its
457
+ callback either way. Where FF only had per-item callbacks, a group-level one
458
+ was added and the item's own state props became optional.
459
+
460
+ | Item | Controlled prop | Added uncontrolled prop | Added change callback |
461
+ |---|---|---|---|
462
+ | Accordion, Accordion.Group | `value` | `defaultValue` (semantics fixed, below) | — (`onValueChange` now also fires uncontrolled) |
463
+ | CheckboxGroup | `checkedIndices` (now optional) | `defaultCheckedIndices` (`Set` or array) | `onCheckedIndicesChange` |
464
+ | Dropdown, Dropdown.Content | `checkedIndex`, `checkedIndices` | `defaultCheckedIndex`, `defaultCheckedIndices` | `onCheckedIndexChange`, `onCheckedIndicesChange` |
465
+ | Input, InputGroup.Field | `value`, `onChange` (now optional) | `defaultValue` | — |
466
+ | InputMessage | `value`, `files`, `queue`, `status` | `defaultValue`, `defaultFiles`, `defaultQueue`, `defaultStatus` | `onValueChange` optional; `onStatusChange` added |
467
+ | RadioGroup | `selectedIndex`, `value` | `defaultSelectedIndex`, `defaultValue` | `onSelectedIndexChange` (`onValueChange` now also fires uncontrolled) |
468
+ | Slider | `value`, `onChange` (now optional) | `defaultValue` | — |
469
+ | Switch | `checked`, `onToggle` (now optional) | `defaultChecked` | `onCheckedChange` |
470
+ | Sidebar | `value` (new: the row whose `value` is selected) | `defaultValue` | `onValueChange` |
471
+ | Sidebar.MenuItem `collapsible` | `open` (new: its sub-menu; upstream's docs held it in page state) | `defaultOpen` | `onOpenChange` |
472
+ | Tabs | `value`, `selectedIndex` | `defaultSelectedIndex` (`defaultValue` is upstream's) | — |
473
+ | TabsSubtle | `selectedIndex`, `onSelect` (now optional) | `defaultSelectedIndex` | — |
474
+
475
+ Already uncontrolled upstream and left on their own internal state (none of
476
+ these go through `useControllableState`): Select `defaultValue` — checked
477
+ explicitly, see its file header — ColorPicker and ColorPicker.Popover
478
+ (`defaultValue`, `defaultFormat`, `defaultFormatOpen`, `defaultOpen`),
479
+ Combobox `defaultValue`, Tabs `defaultValue`, and the `defaultOpen` of
480
+ Dropdown.Menu. Dialog's `open` / `defaultOpen` now go through
481
+ `useControllableState`, because the morph reads the open state.
482
+
483
+ Notable local behavior changes beyond the `default*` props:
484
+
485
+ | Item | Change |
486
+ |---|---|
487
+ | Accordion | Upstream treated the presence of `onValueChange` as "controlled", so an uncontrolled accordion with a listener never opened. Now `value` alone controls, and `defaultValue` seeds an accordion that still reports changes. |
488
+ | RadioGroup | The group context carries a `selectIndex` setter that every item selection calls (after its own `onValueChange` / `onSelect`), so items need no `selected` / `onSelect` of their own. |
489
+ | CheckboxGroup | The group context carries the checked set plus a setter; an item's toggle always reports to the group as well. |
490
+ | TabsSubtle | Panels may now be written inside `<TabsSubtle>`: the root partitions its children, keeps the tabs inside the tab list and renders the panels as its siblings right after it (the root element *is* the `role="tablist"`, so panels cannot nest in it). A panel with no `selectedIndex` / `idPrefix` reads both from context, the prefix falling back to a `useId()` one; a panel rendered outside `<TabsSubtle>` still takes them as props. |
491
+ | Dropdown | `Dropdown.Search` with `filter` hands the query to the panel, which drops the `Dropdown.Item` rows whose `label` doesn't match, re-indexes the survivors for fluid hover, and shows `Dropdown.Empty` only when nothing matched — the work the controlled form asks the consumer to do. Selection stays keyed on the authored index (`sourceIndex`), so it survives re-indexing. |
492
+ | Combobox | `creatable` offers the create row with no callback and the component keeps the items it makes (merged into `items`, deduped by value), so a combobox can grow its own list; `onCreate` is unchanged and still wins when it returns an item. The root is also no longer generic — see the file map. |
493
+ | InputMessage | Attachments are enabled by `onFilesChange` **or** `defaultFiles`; the queue by `onQueueChange` **or** `defaultQueue` (still only alongside a status). With the status uncontrolled the Stop control is offered even without `onStop` and flips the status to `"idle"` itself, which is the edge that auto-dispatches the head of the queue. An uncontrolled draft and file list clear themselves after `onSend`. |
494
+ | Card | `next/link` is gone: the stretched card link and `CardButton`'s `href` form render plain anchors. Examples use inline `data:image/svg+xml` gradients instead of real image files. |
495
+ | Dialog, Select, Combobox | Statics are attached as expando properties (`Dialog.Trigger = …`) rather than through `Object.assign`, so TypeScript infers them without a cast and `check-items` can see them. |
496
+
497
+ ## Dependency deltas (vs upstream `package.json`)
498
+
499
+ | Upstream | Here | Why |
500
+ |---|---|---|
501
+ | `tailwindcss`, `@tailwindcss/postcss` ^4 | `unocss` 66.10.5 (pinned) | UnoCSS instead of Tailwind |
502
+ | `clsx`, `tailwind-merge` | `cn` ^0.3.0 | Compiled drop-in |
503
+ | `framer-motion` ^12 + `motion` ^12 | `motion` ^13.4.0 | One package; imports use `motion/react` |
504
+ | `@base-ui/react` ^1.4.1 | `@base-ui/react` 1.4.1 (exact) | FF styling relies on its `--anchor-width`, `--available-height` and `data-side` hooks |
505
+ | `lucide-react` ^0.564 | `lucide-react` ^1.47.0 | Workspace version; `icon-context`'s `size`/`strokeWidth` widened to `number \| string` for it |
506
+ | `react` / `react-dom` ^19 | ^18.3 | The workspace pins React 18. React 19's ref typings are adapted per file: `MutableRefObject` casts where a forwarded ref is written, `useRef<T \| null>(null)` instead of the 1-arg null form, and `FilePreviewTile` turned into a `forwardRef` (React 19 passes `ref` as a plain prop; React 18 warns and drops it). |
507
+ | self-hosted `InterVariable.ttf` | `@fontsource-variable/inter` ^5.3.0 (`opsz.css`) | npm-distributed. The opsz axis is needed by the `--weight-*` ladder. |
508
+ | `pdfjs-dist` ^5.7.284 | same | InputMessage PDF thumbnails. It bundles under modo's esbuild as a lazy ~790 kB chunk; the worker is loaded from `cdn.jsdelivr.net`, as upstream does. |
509
+ | `class-variance-authority` ^0.7.1 | same | |
510
+ | `next` (`next/link`, `next/image` lint) | — | No Next.js here |
511
+ | — | `@justanarthur/modo` (workspace) | The design system's own host |
512
+ | — | `sugar-high` ^2.5.1 | Tokenizer for the local `primitives/code` item |
513
+ | — | `react-day-picker` ^10.0.2, `date-fns` ^4.4.0 | The calendar under the date pickers, and its date math, formats and locales (`react-day-picker/locale`); date-fns is declared because the pickers import it directly |
514
+ | — | `vite` ^5.4, `esbuild` ^0.20, `typescript` ^5.4, `@types/react(-dom)` ^18.3 (dev) | `vite` ^5.4 makes UnoCSS's peer reuse modo's Vite 5. The others serve the scripts and `tsc`. |
515
+
516
+ Every import is declared here, because bun's isolated linker only exposes
517
+ declared packages.
518
+
519
+ ## Docs and examples
520
+
521
+ What modo's parser (`lib/src/lib/tsdoc.ts`) and example compiler
522
+ (`lib/src/lib/example.ts`) need, and how this package answers it:
523
+
524
+ - **The item is the file's `export default`** — either `function X(…)` or a
525
+ `const X = forwardRef<HTMLXElement, XProps>(…)`. The forwardRef form's first
526
+ type argument must contain no `<` or `,`, which is why ScrollArea spells out
527
+ `HTMLDivElement` instead of `ComponentRef<typeof …>`, and why Combobox's
528
+ root lost its generics.
529
+ - **Props come from a same-file `interface XProps`**, and only from members
530
+ declared in its body — inherited members are invisible, and a member whose
531
+ type spans several lines is dropped. Every member needs a description, and
532
+ defaults are read from that prose (`Defaults to \`false\`.`), not from the
533
+ destructuring. Hence the FF API-table text re-declared on every prop.
534
+ - **Examples live in `examples.mdx`** next to the item, included by the
535
+ TSDoc's `@example {@include ./examples.mdx}`: a `# <FF section title>`,
536
+ an optional caption, then one JSX block (or a fenced code block for code to
537
+ read). Expressions are plain JS (no `as const`), and multi-line JSX text
538
+ stays inline (modo unwraps MDX's paragraphs inside examples).
539
+ - **Examples are hook-free JSX.** Nothing runs a component body, so
540
+ interactivity in the docs comes from the `default*` props above instead of
541
+ `useState`.
542
+ - **Imports are real**: the item is `./index`, other items are
543
+ `../../<tier>/<id>` (one `../` more from or to a group folder), icons come from `lucide-react`. Every free identifier
544
+ must be imported (or be an item name or a JS/DOM global), and every
545
+ `Item.Part` used must really be attached to that item.
546
+
547
+ Each item follows its FF docs page: the TSDoc holds the one-liner, a concept
548
+ paragraph and the list of statics; `examples.mdx` holds one `# <FF section
549
+ title>` example per docs section. Playground and API Reference sections are never ported (the API
550
+ tables become the prop docs instead). Other per-item deviations:
551
+
552
+ | Item | Docs deviation |
553
+ |---|---|
554
+ | Button | Extra Sizes / Active / As-child examples, for API-table props the FF page has no section for. |
555
+ | Dialog | "With a sidebar" replaced by "Surfaces inside a dialog" (a Select popover lifting off the dialog's own level) — the same point about composing inside an `xl` panel, with items this port ships. |
556
+ | Sidebar | Layouts split into three examples; "Callouts" (docs-only cards), "Inside a dialog", the "No icon rail" prose (folded into the TSDoc) and "Functional and perfectly aligned" (an alignment-guides overlay) skipped; "Mobile drawer" added. Frames pass `mobileBreakpoint={0}` like FF's, so a rail stays a rail inside them. |
557
+ | SidebarApp | One block page for FF's preset playground: the preset itself (collapsed), open, open on hover, floating on the right. |
558
+ | Dropdown | "Create from the query" skipped: adding a row to the list is real consumer state, which an uncontrolled panel cannot stand in for. |
559
+ | InputMessage | Playground skipped; the transcript around the Attachments / Send Handler demos dropped, since `ChatMessage` is not ported. |
560
+ | FluidHover | The two scripted-cursor demos are dropped — they exist only to film the mechanism. |
561
+ | SizeProvider | Live demos become static examples; the token-inspector overlay is dropped and the demo components are swapped for ones this port ships. |
562
+ | Motion | The ball-on-track and fake-modal visuals become a show/hide of arbitrary children; component chips are plain text. |
563
+ | Card | Images are inline `data:image/svg+xml` gradients. |
564
+
565
+ ## Known limitations
566
+
567
+ - The 22px mobile display size and the `html[data-size="compact"]` type-scale
568
+ override are not carried over. Both are FF docs-site chrome. Compact type
569
+ reaches components through `SizeProvider` / `useTypeScale()`.
570
+ - Theme switching is docs-site glue, not a ported `ThemeProvider`: vite.ts's
571
+ pre-paint controller and the `ThemeSwitcher` panel item. A `.light` /
572
+ `.dark` class on a subtree still forces that scheme, which is what the
573
+ forced-theme previews use.
574
+ - `transition-[…]` with a CSS property outside wind4's list needs a
575
+ `theme.property` entry in `preset.ts`.
576
+ - Cross-item context (a `SizeProvider` example shrinking a Button, a surface
577
+ level crossing from Dialog into Dropdown) depends on modo bundling the whole
578
+ design system in **one** esbuild build with `splitting: true`, so a shared
579
+ module exists once at runtime. The output lives in this package's
580
+ `.modo-tmp/build`, which is keyed on the package and not on the port: two
581
+ `modo dev` servers started in this directory share it. `run-design-systems.ts`
582
+ avoids that by killing stray `modo dev` processes before it starts.
583
+ - PDF thumbnails in InputMessage need network access: the pdfjs worker is
584
+ fetched from `cdn.jsdelivr.net` on the first PDF attachment.
585
+ - The inset shadow tokens in `surface.css` are dead weight today — they ship,
586
+ but nothing in this package reads them.
587
+
588
+ ## How to run
589
+
590
+ ```sh
591
+ cd demo && bun scripts/run-design-systems.ts ui # http://127.0.0.1:5173
592
+ ```
593
+
594
+ The runner kills stray `modo dev` processes, splices a temporary
595
+ `.modo.config.ts` with the demo switcher panel, and assigns ports from 5173
596
+ upward. To run the package on its own, without the switcher:
597
+
598
+ ```sh
599
+ cd ui && MODO_PORT=5180 bunx modo dev
600
+ ```
601
+
602
+ ## How to verify
603
+
604
+ ```sh
605
+ cd ui && bunx tsc -p . --noEmit
606
+ ```
607
+
608
+ modo reports parser and build problems itself, as `[modo:bundle]` lines in
609
+ the dev server's output.