@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.
- package/LICENSE +21 -0
- package/LICENSE.beui +21 -0
- package/LICENSE.fluid-functionalism +21 -0
- package/LICENSE.motion-primitives +21 -0
- package/LICENSE.sileo +21 -0
- package/README.md +608 -2
- package/dist/base.css +994 -0
- package/dist/blocks/sidebar-app/index.d.ts +34 -0
- package/dist/blocks/sidebar-app/index.js +31 -0
- package/dist/components/badge/index.d.ts +59 -0
- package/dist/components/badge/index.js +16 -0
- package/dist/components/button/index.d.ts +59 -0
- package/dist/components/button/index.js +14 -0
- package/dist/components/card/index.d.ts +141 -0
- package/dist/components/card/index.js +42 -0
- package/dist/components/inputs/checkbox-group/index.d.ts +50 -0
- package/dist/components/inputs/checkbox-group/index.js +21 -0
- package/dist/components/inputs/color-picker/index.d.ts +137 -0
- package/dist/components/inputs/color-picker/index.js +35 -0
- package/dist/components/inputs/combobox/index.d.ts +161 -0
- package/dist/components/inputs/combobox/index.js +38 -0
- package/dist/components/inputs/date-time/calendar/index.d.ts +45 -0
- package/dist/components/inputs/date-time/calendar/index.js +18 -0
- package/dist/components/inputs/date-time/date-picker/index.d.ts +82 -0
- package/dist/components/inputs/date-time/date-picker/index.js +32 -0
- package/dist/components/inputs/date-time/date-range-picker/index.d.ts +64 -0
- package/dist/components/inputs/date-time/date-range-picker/index.js +31 -0
- package/dist/components/inputs/date-time/duration-picker/index.d.ts +57 -0
- package/dist/components/inputs/date-time/duration-picker/index.js +31 -0
- package/dist/components/inputs/date-time/month-picker/index.d.ts +57 -0
- package/dist/components/inputs/date-time/month-picker/index.js +30 -0
- package/dist/components/inputs/date-time/time-picker/index.d.ts +59 -0
- package/dist/components/inputs/date-time/time-picker/index.js +31 -0
- package/dist/components/inputs/input/index.d.ts +84 -0
- package/dist/components/inputs/input/index.js +19 -0
- package/dist/components/inputs/input-copy/index.d.ts +44 -0
- package/dist/components/inputs/input-copy/index.js +16 -0
- package/dist/components/inputs/input-message/index.d.ts +125 -0
- package/dist/components/inputs/input-message/index.js +25 -0
- package/dist/components/inputs/radio-group/index.d.ts +58 -0
- package/dist/components/inputs/radio-group/index.js +20 -0
- package/dist/components/inputs/search/index.d.ts +83 -0
- package/dist/components/inputs/search/index.js +25 -0
- package/dist/components/inputs/select/index.d.ts +105 -0
- package/dist/components/inputs/select/index.js +34 -0
- package/dist/components/inputs/slider/index.d.ts +108 -0
- package/dist/components/inputs/slider/index.js +14 -0
- package/dist/components/inputs/switch/index.d.ts +43 -0
- package/dist/components/inputs/switch/index.js +14 -0
- package/dist/components/navigation/accordion/index.d.ts +117 -0
- package/dist/components/navigation/accordion/index.js +28 -0
- package/dist/components/navigation/sidebar/index.d.ts +388 -0
- package/dist/components/navigation/sidebar/index.js +99 -0
- package/dist/components/navigation/tabs/index.d.ts +70 -0
- package/dist/components/navigation/tabs/index.js +23 -0
- package/dist/components/navigation/tabs-subtle/index.d.ts +73 -0
- package/dist/components/navigation/tabs-subtle/index.js +21 -0
- package/dist/components/overlays/alert-dialog/index.d.ts +67 -0
- package/dist/components/overlays/alert-dialog/index.js +22 -0
- package/dist/components/overlays/command/index.d.ts +121 -0
- package/dist/components/overlays/command/index.js +36 -0
- package/dist/components/overlays/context-menu/index.d.ts +97 -0
- package/dist/components/overlays/context-menu/index.js +29 -0
- package/dist/components/overlays/dialog/index.d.ts +130 -0
- package/dist/components/overlays/dialog/index.js +37 -0
- package/dist/components/overlays/dropdown/index.d.ts +222 -0
- package/dist/components/overlays/dropdown/index.js +44 -0
- package/dist/components/overlays/popover/index.d.ts +87 -0
- package/dist/components/overlays/popover/index.js +26 -0
- package/dist/components/overlays/preview-card/index.d.ts +75 -0
- package/dist/components/overlays/preview-card/index.js +19 -0
- package/dist/components/overlays/sheet/index.d.ts +67 -0
- package/dist/components/overlays/sheet/index.js +24 -0
- package/dist/components/overlays/toast/index.d.ts +107 -0
- package/dist/components/overlays/toast/index.js +21 -0
- package/dist/components/overlays/tooltip/index.d.ts +75 -0
- package/dist/components/overlays/tooltip/index.js +17 -0
- package/dist/components/table/index.d.ts +44 -0
- package/dist/components/table/index.js +26 -0
- package/dist/components/thinking-indicator/index.d.ts +32 -0
- package/dist/components/thinking-indicator/index.js +13 -0
- package/dist/index.d.ts +3926 -0
- package/dist/index.js +511 -0
- package/dist/preset.d.ts +104 -0
- package/dist/preset.js +124 -0
- package/dist/primitives/code/index.d.ts +23 -0
- package/dist/primitives/code/index.js +11 -0
- package/dist/primitives/craft/index.d.ts +31 -0
- package/dist/primitives/craft/index.js +11 -0
- package/dist/primitives/fluid-hover/index.d.ts +233 -0
- package/dist/primitives/fluid-hover/index.js +25 -0
- package/dist/primitives/morph/index.d.ts +127 -0
- package/dist/primitives/morph/index.js +19 -0
- package/dist/primitives/motion/index.d.ts +115 -0
- package/dist/primitives/motion/index.js +19 -0
- package/dist/primitives/scroll-area/index.d.ts +41 -0
- package/dist/primitives/scroll-area/index.js +12 -0
- package/dist/primitives/sizes/index.d.ts +128 -0
- package/dist/primitives/sizes/index.js +21 -0
- package/dist/primitives/surface/index.d.ts +69 -0
- package/dist/primitives/surface/index.js +17 -0
- package/dist/shared/components-0ke6xada.js +123 -0
- package/dist/shared/components-0q0sqfh4.js +146 -0
- package/dist/shared/components-0sn7ypjw.js +61 -0
- package/dist/shared/components-1j9791sn.js +345 -0
- package/dist/shared/components-1rfwbyyc.js +361 -0
- package/dist/shared/components-1v7rxytx.js +30 -0
- package/dist/shared/components-1xekx2hx.js +215 -0
- package/dist/shared/components-39jq9va1.js +86 -0
- package/dist/shared/components-3gae8nta.js +719 -0
- package/dist/shared/components-3k8xpnpq.js +1759 -0
- package/dist/shared/components-3ky44908.js +9 -0
- package/dist/shared/components-3mkepka5.js +111 -0
- package/dist/shared/components-3wk0w46f.js +667 -0
- package/dist/shared/components-4cnjmgex.js +124 -0
- package/dist/shared/components-59efxjd9.js +298 -0
- package/dist/shared/components-5xc2j8jz.js +159 -0
- package/dist/shared/components-62ybc7t2.js +496 -0
- package/dist/shared/components-66mv71kx.js +262 -0
- package/dist/shared/components-7ans7ce5.js +120 -0
- package/dist/shared/components-7btqt08d.js +270 -0
- package/dist/shared/components-7r3etddy.js +171 -0
- package/dist/shared/components-7x9kawxv.js +152 -0
- package/dist/shared/components-8fpr3s4p.js +292 -0
- package/dist/shared/components-9jvzzvt1.js +198 -0
- package/dist/shared/components-apdwtweq.js +484 -0
- package/dist/shared/components-asr5pg50.js +191 -0
- package/dist/shared/components-b2wyq2cq.js +232 -0
- package/dist/shared/components-c6s6b43g.js +15 -0
- package/dist/shared/components-ca2c898n.js +1828 -0
- package/dist/shared/components-cbxjn9p8.js +141 -0
- package/dist/shared/components-chacnk61.js +216 -0
- package/dist/shared/components-cv16mryv.js +62 -0
- package/dist/shared/components-ez65vtbq.js +110 -0
- package/dist/shared/components-ezxhrmer.js +17 -0
- package/dist/shared/components-gc5vtq2f.js +149 -0
- package/dist/shared/components-gf5s6ap9.js +42 -0
- package/dist/shared/components-gvr9w41t.js +193 -0
- package/dist/shared/components-h2dpcwgc.js +137 -0
- package/dist/shared/components-hawvy2qz.js +105 -0
- package/dist/shared/components-hqeq3w9q.js +375 -0
- package/dist/shared/components-jjw69c4c.js +28 -0
- package/dist/shared/components-jwst3vtj.js +346 -0
- package/dist/shared/components-kvr7f9hy.js +45 -0
- package/dist/shared/components-men63rwj.js +9 -0
- package/dist/shared/components-mhd39z7x.js +22 -0
- package/dist/shared/components-n7mjr357.js +26 -0
- package/dist/shared/components-ng72k97e.js +128 -0
- package/dist/shared/components-nh8tma1n.js +986 -0
- package/dist/shared/components-njzgqdwy.js +445 -0
- package/dist/shared/components-ny774h8p.js +293 -0
- package/dist/shared/components-q4daerx0.js +58 -0
- package/dist/shared/components-q5xj5c16.js +79 -0
- package/dist/shared/components-rr7a6ja9.js +160 -0
- package/dist/shared/components-s2bz1xpk.js +63 -0
- package/dist/shared/components-tqvarbfv.js +288 -0
- package/dist/shared/components-v6xp325t.js +304 -0
- package/dist/shared/components-v7wtz8hk.js +144 -0
- package/dist/shared/components-vc7vq5nz.js +32 -0
- package/dist/shared/components-vfbhxf5r.js +58 -0
- package/dist/shared/components-vynjtrk6.js +19 -0
- package/dist/shared/components-wvp1m52g.js +1200 -0
- package/dist/shared/components-yh3kwdhr.js +981 -0
- package/dist/shared/components-ytb2tefr.js +291 -0
- package/dist/shared/components-zfnmn5d4.js +220 -0
- package/dist/shared/components-zrkzcayz.js +141 -0
- package/dist/shared/components-zx6b6yby.js +42 -0
- package/dist/styles.css +1949 -0
- package/dist/tailwind.css +219 -0
- package/dist/tokens.css +345 -0
- package/package.json +103 -4
package/README.md
CHANGED
|
@@ -1,3 +1,609 @@
|
|
|
1
|
-
#
|
|
1
|
+
# UI — Fluid Functionalism for modo
|
|
2
2
|
|
|
3
|
-
|
|
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.
|