@kennycorrea/ui 0.9.1 → 0.11.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/CHANGELOG.md +120 -14
- package/README.md +204 -273
- package/dist/atoms/ActionSheet/ActionSheet.d.ts +30 -0
- package/dist/atoms/ActionSheet/ActionSheet.js +42 -0
- package/dist/atoms/ActionSheet/confirm.d.ts +21 -0
- package/dist/atoms/ActionSheet/confirm.js +65 -0
- package/dist/atoms/ActionSheet/index.d.ts +2 -0
- package/dist/atoms/ActionSheet/index.js +2 -0
- package/dist/atoms/Button/Button.js +3 -3
- package/dist/atoms/Button/Button.types.d.ts +7 -1
- package/dist/atoms/Card/Card.d.ts +28 -7
- package/dist/atoms/Card/Card.js +45 -9
- package/dist/atoms/ChipGroup/ChipGroup.d.ts +31 -0
- package/dist/atoms/ChipGroup/ChipGroup.js +69 -0
- package/dist/atoms/ChipGroup/index.d.ts +1 -0
- package/dist/atoms/ChipGroup/index.js +1 -0
- package/dist/atoms/Drawer/Drawer.d.ts +1 -1
- package/dist/atoms/Drawer/Drawer.js +9 -13
- package/dist/atoms/Input/Input.js +7 -1
- package/dist/atoms/List/List.d.ts +73 -0
- package/dist/atoms/List/List.js +88 -0
- package/dist/atoms/List/index.d.ts +1 -0
- package/dist/atoms/List/index.js +1 -0
- package/dist/atoms/List/swipe.d.ts +68 -0
- package/dist/atoms/List/swipe.js +207 -0
- package/dist/atoms/NavBar/BackButton.d.ts +10 -0
- package/dist/atoms/NavBar/BackButton.js +11 -0
- package/dist/atoms/NavBar/NavBar.d.ts +22 -0
- package/dist/atoms/NavBar/NavBar.js +29 -0
- package/dist/atoms/NavBar/index.d.ts +2 -0
- package/dist/atoms/NavBar/index.js +2 -0
- package/dist/atoms/NotificationCard/NotificationCard.d.ts +52 -0
- package/dist/atoms/NotificationCard/NotificationCard.js +78 -0
- package/dist/atoms/NotificationCard/index.d.ts +1 -0
- package/dist/atoms/NotificationCard/index.js +1 -0
- package/dist/atoms/PullToRefresh/PullToRefresh.d.ts +31 -0
- package/dist/atoms/PullToRefresh/PullToRefresh.js +110 -0
- package/dist/atoms/PullToRefresh/index.d.ts +1 -0
- package/dist/atoms/PullToRefresh/index.js +1 -0
- package/dist/atoms/ResponsiveDialog/ResponsiveDialog.d.ts +13 -0
- package/dist/atoms/ResponsiveDialog/ResponsiveDialog.js +27 -0
- package/dist/atoms/ResponsiveDialog/index.d.ts +1 -0
- package/dist/atoms/ResponsiveDialog/index.js +1 -0
- package/dist/atoms/ScrollStrip/ScrollStrip.d.ts +17 -0
- package/dist/atoms/ScrollStrip/ScrollStrip.js +15 -0
- package/dist/atoms/ScrollStrip/index.d.ts +1 -0
- package/dist/atoms/ScrollStrip/index.js +1 -0
- package/dist/atoms/SegmentedControl/SegmentedControl.d.ts +23 -0
- package/dist/atoms/SegmentedControl/SegmentedControl.js +41 -0
- package/dist/atoms/SegmentedControl/index.d.ts +1 -0
- package/dist/atoms/SegmentedControl/index.js +1 -0
- package/dist/atoms/Select/Select.js +1 -1
- package/dist/atoms/Sheet/Sheet.d.ts +63 -0
- package/dist/atoms/Sheet/Sheet.js +261 -0
- package/dist/atoms/Sheet/index.d.ts +1 -0
- package/dist/atoms/Sheet/index.js +1 -0
- package/dist/atoms/StatTile/StatTile.d.ts +36 -0
- package/dist/atoms/StatTile/StatTile.js +17 -0
- package/dist/atoms/StatTile/index.d.ts +1 -0
- package/dist/atoms/StatTile/index.js +1 -0
- package/dist/atoms/TabBar/TabBar.d.ts +42 -0
- package/dist/atoms/TabBar/TabBar.js +36 -0
- package/dist/atoms/TabBar/index.d.ts +1 -0
- package/dist/atoms/TabBar/index.js +1 -0
- package/dist/atoms/Textarea/Textarea.d.ts +4 -0
- package/dist/atoms/Textarea/Textarea.js +1 -1
- package/dist/atoms/Toast/Toast.d.ts +8 -2
- package/dist/atoms/Toast/Toast.js +19 -4
- package/dist/atoms/index.d.ts +12 -0
- package/dist/atoms/index.js +12 -0
- package/dist/primitives/index.d.ts +1 -1
- package/dist/primitives/index.js +1 -1
- package/dist/strings.d.ts +13 -3
- package/dist/strings.js +5 -0
- package/dist/utils/themePortal.d.ts +19 -0
- package/dist/utils/themePortal.js +28 -0
- package/package.json +4 -2
- package/tokens.css +454 -0
package/README.md
CHANGED
|
@@ -1,350 +1,281 @@
|
|
|
1
1
|
# @kennycorrea/ui
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
| | dashboard | site | visible? |
|
|
20
|
-
| --- | --- | --- | --- |
|
|
21
|
-
| `Button` `secondary` | Themes' `soft` | Themes' `surface` | **yes** |
|
|
22
|
-
| `danger` variant | yes | absent | **yes** |
|
|
23
|
-
| sizes | `sm`/`md`/`lg` | raw Themes `1`–`4` | **yes** |
|
|
24
|
-
| `href` prop | absent | yes | **yes** |
|
|
25
|
-
| `--font-sans` | `'Inter', …` | `ui-sans-serif, …` | no — neither app loads Inter |
|
|
26
|
-
| `--color-ink` fallback | `#1d3b31` | `#1a211f` | only if Radix's CSS fails to load |
|
|
27
|
-
| `--color-hairline` fallback | 12% | 10% | same |
|
|
28
|
-
|
|
29
|
-
The `.radix-themes` blocks — the *live* colour assignments — had been kept in step by hand and
|
|
30
|
-
agreed. It was the `@theme` fallbacks underneath them that had not, which is drift that is real
|
|
31
|
-
and invisible: the least urgent kind and the easiest to leave lying around for years.
|
|
32
|
-
|
|
33
|
-
So the honest case for this package is the component API, not the palette.
|
|
34
|
-
|
|
35
|
-
## The Button disagreement, resolved
|
|
36
|
-
|
|
37
|
-
**`secondary` now means exactly one thing: Themes' `soft`.** The site's bordered, translucent
|
|
38
|
-
look is still available and is called **`outline`**.
|
|
39
|
-
|
|
40
|
-
Making one of them simply win would have been a decision about the word rather than about the
|
|
41
|
-
need — the site wants the bordered button because it sits over a dark hero, and the dashboard
|
|
42
|
-
wants the tinted one because it sits on a panel. Both needs are real; only the name was
|
|
43
|
-
ambiguous.
|
|
44
|
-
|
|
45
|
-
The package's `Button` is the superset: `loading`, `fullWidth`, ref forwarding and `danger` from
|
|
46
|
-
the dashboard, `href` from the site. Nothing was dropped.
|
|
47
|
-
|
|
48
|
-
## Using it
|
|
49
|
-
|
|
50
|
-
```ts
|
|
51
|
-
import { Button, Card, Select, UiStringsProvider } from '@kennycorrea/ui';
|
|
3
|
+
A small React + TypeScript component library built on top of
|
|
4
|
+
[Radix Themes](https://www.radix-ui.com/themes). It wraps Radix's components in a consistent,
|
|
5
|
+
opinionated API, adds a few components Radix does not ship, and bridges Radix's design tokens to
|
|
6
|
+
Tailwind utilities.
|
|
7
|
+
|
|
8
|
+
- **Atoms** — `Button`, `Input`, `Select`, `Checkbox`, `Switch`, `Slider`, `Dialog`, `Drawer`,
|
|
9
|
+
`Popover`, `Tooltip`, `Toast`, `NotificationCard`, `RichText` and more.
|
|
10
|
+
- **Primitives** — Radix Themes' layout and typography components, re-exported.
|
|
11
|
+
- **Tokens** — `tokens.css` maps Radix Themes variables to Tailwind colour utilities.
|
|
12
|
+
- **Theming** — light/dark/system appearance and a configurable accent list.
|
|
13
|
+
- **No words of its own** — the few strings components need come from `UiStringsProvider`.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @kennycorrea/ui @radix-ui/themes react react-dom
|
|
52
19
|
```
|
|
53
20
|
|
|
54
|
-
|
|
21
|
+
In your stylesheet:
|
|
55
22
|
|
|
56
23
|
```css
|
|
57
24
|
@import 'tailwindcss';
|
|
58
25
|
@source '../node_modules/@kennycorrea/ui/dist';
|
|
26
|
+
@import '@radix-ui/themes/styles.css';
|
|
59
27
|
@import '@kennycorrea/ui/tokens.css';
|
|
60
28
|
```
|
|
61
29
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
generated CSS. The components come out subtly unstyled and nothing appears in the console.
|
|
65
|
-
|
|
66
|
-
### Colour scales — required
|
|
30
|
+
The `@source` line matters: Tailwind does not scan `node_modules`, so without it the utility
|
|
31
|
+
classes used inside the components are missing from the generated CSS.
|
|
67
32
|
|
|
68
|
-
|
|
69
|
-
**amber** (warning), **red** (danger/error) and **blue** (`Toast`/`Alert` info). If the app
|
|
70
|
-
imports Radix Themes piecemeal rather than the whole `@radix-ui/themes/styles.css`, every one of
|
|
71
|
-
them must be imported, **after `base.css`** as Radix documents it, plus the app's own accent:
|
|
33
|
+
Wrap the app in Radix's `<Theme>` and, optionally, `UiStringsProvider`:
|
|
72
34
|
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
@import '@radix-ui/themes/utilities.css';
|
|
35
|
+
```tsx
|
|
36
|
+
import { Theme } from '@radix-ui/themes';
|
|
37
|
+
import { Button, UiStringsProvider } from '@kennycorrea/ui';
|
|
38
|
+
|
|
39
|
+
<Theme appearance="dark" accentColor="teal">
|
|
40
|
+
<UiStringsProvider strings={{ close: 'Fechar', expand: 'Expandir', collapse: 'Recolher' }}>
|
|
41
|
+
<Button>Save</Button>
|
|
42
|
+
</UiStringsProvider>
|
|
43
|
+
</Theme>;
|
|
83
44
|
```
|
|
84
45
|
|
|
85
|
-
|
|
86
|
-
render colourless. Since 0.8.1 the first `Badge` or `Toaster` to mount checks for them and
|
|
87
|
-
`console.warn`s the missing imports by name, outside production builds.
|
|
46
|
+
### Colour scales
|
|
88
47
|
|
|
89
|
-
|
|
48
|
+
If you import Radix Themes piecemeal instead of `styles.css`, import **gray**, **jade**, **amber**,
|
|
49
|
+
**red** and **blue** (used by status variants) plus your accent, after `base.css`. A missing scale
|
|
50
|
+
is not a CSS error — the variables are just undefined — so in development the first `Badge` or
|
|
51
|
+
`Toaster` to mount warns about missing scales in the console.
|
|
90
52
|
|
|
91
|
-
|
|
92
|
-
were hardcoded to the first app that used this package: the fallback accent was jade, the accent
|
|
93
|
-
list was six fixed colours, and preferences were stored under `upfront.theme` / `upfront.accent`.
|
|
53
|
+
## Theming
|
|
94
54
|
|
|
95
55
|
```ts
|
|
96
|
-
|
|
56
|
+
import { createTheming, applyTheme } from '@kennycorrea/ui';
|
|
57
|
+
|
|
97
58
|
export const theming = createTheming({
|
|
98
59
|
accents: ['teal', 'crimson'],
|
|
99
|
-
defaultAccent: 'teal',
|
|
100
|
-
storagePrefix: 'myapp',
|
|
60
|
+
defaultAccent: 'teal', // must be one of `accents` — a type error otherwise
|
|
61
|
+
storagePrefix: 'myapp', // required: namespaces stored preferences
|
|
101
62
|
});
|
|
102
63
|
|
|
103
64
|
theming.readAccent(); // 'teal' | 'crimson'
|
|
104
65
|
```
|
|
105
66
|
|
|
106
|
-
|
|
107
|
-
order a picker should render.
|
|
67
|
+
Every accent you offer needs its Radix colour scale imported.
|
|
108
68
|
|
|
109
|
-
|
|
110
|
-
`TS2322` rather than a colourless first paint — verified by compiling the mistake.
|
|
69
|
+
### Dark mode and portals
|
|
111
70
|
|
|
112
|
-
|
|
113
|
-
|
|
71
|
+
Radix Themes defines its colour variables (`--color-panel-solid`, `--gray-*`, `--accent-*`, …)
|
|
72
|
+
only on and under the `.radix-themes` element that `<Theme>` renders, and switches light/dark
|
|
73
|
+
with a `.light`/`.dark` class on that element. **Anything rendered outside it gets light colours,
|
|
74
|
+
or none, whatever appearance the app is in.**
|
|
114
75
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
76
|
+
So no component in this library portals to `document.body`. Floating components (`Drawer`,
|
|
77
|
+
`NotificationCard`) portal into the nearest `.radix-themes` ancestor, wrapped in a nested
|
|
78
|
+
`<Theme>` that inherits appearance and accent — or into a `container` you pass — and render in
|
|
79
|
+
place when there is no Theme above them. Their colours, borders and shadows use only theme
|
|
80
|
+
variables: the appearance is whatever `<Theme>` says, never `prefers-color-scheme`.
|
|
118
81
|
|
|
119
|
-
|
|
120
|
-
|
|
82
|
+
Do the same in your own code: set appearance on `<Theme>`, not with a `dark` class on
|
|
83
|
+
`<html>`/`<body>`.
|
|
121
84
|
|
|
122
|
-
|
|
123
|
-
before the first read, making correctness depend on import order — the same class of silent bug as
|
|
124
|
-
the CSS import order this package already has to warn about. An instance cannot be read before it
|
|
125
|
-
is made.
|
|
85
|
+
## Strings
|
|
126
86
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
87
|
+
Components never contain literal text. The handful they need — a spinner's label, a select's
|
|
88
|
+
placeholder, close/expand/collapse names — come from `UiStringsProvider`, which takes a partial
|
|
89
|
+
`UiStrings` object. Defaults are English. Most components also accept the label as a prop.
|
|
130
90
|
|
|
131
|
-
|
|
91
|
+
## Mobile presentation layer
|
|
132
92
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
implementation, not two** — they are literally a pre-made instance, so no call site had to change
|
|
136
|
-
and nobody who already picked a theme loses it. `ACCENTS` remains as a deprecated alias for
|
|
137
|
-
`DEFAULT_ACCENTS`, the same array.
|
|
93
|
+
An opt-in "grouped" surface style — rows in rounded cards on a tinted canvas, as in native
|
|
94
|
+
settings screens. Nothing existing reads these tokens, so atoms render exactly as before.
|
|
138
95
|
|
|
139
|
-
|
|
96
|
+
| Token | Value |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `--surface-grouped` | Card background (white / gray-3 in dark), no border |
|
|
99
|
+
| `--canvas-grouped` | Tinted page background (gray-3 / gray-1 in dark) |
|
|
100
|
+
| `--separator-inset` | Hairline for row separators |
|
|
101
|
+
| `--radius-group` | 12px |
|
|
102
|
+
| `--space-row-y` / `--space-row-x` | 10px / 12px |
|
|
103
|
+
| `--tap-min` | 44px |
|
|
104
|
+
| `--safe-top/right/bottom/left` | `env(safe-area-inset-*)`, `0px` fallback |
|
|
105
|
+
| `--material-bar`, `--material-bar-filter` | Translucent bar colour and `saturate(180%) blur(20px)` |
|
|
140
106
|
|
|
141
|
-
|
|
107
|
+
Tailwind utilities: `bg-grouped`, `bg-canvas-grouped`, `border-separator-inset`, `rounded-group`,
|
|
108
|
+
`min-h-tap`, `pt-safe`, `pb-safe`, `px-safe`, `bg-material`.
|
|
142
109
|
|
|
143
|
-
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
wrap gets something legible rather than something wrong.
|
|
147
|
-
- **Auth, stores, API clients.** Same reason, and they were never shared.
|
|
148
|
-
- **`ACCENT_LABELS`.** The accent *list* is structure and lives here; what each one is called is
|
|
149
|
-
words and does not.
|
|
150
|
-
- **Molecules and organisms.** `FormField`, `ConfirmDialog` and the rest stay in the dashboard
|
|
151
|
-
until a second app wants one. Moving a component here should be a response to a second
|
|
152
|
-
consumer, not an anticipation of one.
|
|
110
|
+
Gray-11 and gray-12 text pass WCAG AA on the card, the canvas and the bar (over white or black
|
|
111
|
+
content) in both appearances — see `test/grouped.test.tsx`. Remember `viewport-fit=cover` in the
|
|
112
|
+
viewport meta, or the safe-area insets are always 0.
|
|
153
113
|
|
|
154
|
-
|
|
114
|
+
**Before** — a bordered panel:
|
|
155
115
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
## ⚠ 0.1.0 was broken for any Node consumer
|
|
116
|
+
```tsx
|
|
117
|
+
<div className="panel p-4">
|
|
118
|
+
<button className="py-2">Profile</button>
|
|
119
|
+
</div>
|
|
120
|
+
```
|
|
162
121
|
|
|
163
|
-
**
|
|
164
|
-
imports extensionless in the output — `export * from './atoms'`. Vite resolves that happily, so
|
|
165
|
-
every build passed and the linked `file:../ui` setup never complained. **Node does not support
|
|
166
|
-
directory imports**, so the moment the package was installed from the registry it failed:
|
|
122
|
+
**After** — a grouped list under a blurred bar:
|
|
167
123
|
|
|
168
|
-
|
|
169
|
-
|
|
124
|
+
```tsx
|
|
125
|
+
<div className="min-h-screen bg-canvas-grouped px-safe">
|
|
126
|
+
<header className="bg-material pt-safe sticky top-0">Settings</header>
|
|
127
|
+
<ul className="rounded-group bg-grouped mx-3 divide-y divide-separator-inset">
|
|
128
|
+
<li><button className="min-h-tap w-full px-3 text-left">Profile</button></li>
|
|
129
|
+
<li><button className="min-h-tap w-full px-3 text-left">Notifications</button></li>
|
|
130
|
+
</ul>
|
|
131
|
+
<div className="pb-safe" />
|
|
132
|
+
</div>
|
|
133
|
+
```
|
|
170
134
|
|
|
171
|
-
|
|
172
|
-
bundler-only consumer would never have noticed, which is why it shipped.
|
|
135
|
+
## Components
|
|
173
136
|
|
|
174
|
-
|
|
175
|
-
explicit `.js` on every relative import**, and `/index.js` for a directory. `tsconfig.json` is now
|
|
176
|
-
`moduleResolution: nodenext`, so `tsc` *refuses* the extensionless form — verified by putting one
|
|
177
|
-
back and watching it error — rather than silently emitting it again.
|
|
137
|
+
### Toast
|
|
178
138
|
|
|
179
|
-
|
|
180
|
-
a browser check confirmed the styling, all while the package was unimportable by Node. Nothing was
|
|
181
|
-
wrong with those checks; they simply all went through the same bundler. The first consumer with
|
|
182
|
-
different resolution semantics found it immediately.
|
|
139
|
+
Mount `<Toaster />` once inside `<Theme>` and call `toast()` from anywhere.
|
|
183
140
|
|
|
184
|
-
|
|
141
|
+
```tsx
|
|
142
|
+
<Toaster duration={5000} position="bottom-right" />
|
|
185
143
|
|
|
186
|
-
|
|
187
|
-
|
|
144
|
+
toast({ title: 'Saved' });
|
|
145
|
+
toast.success({ title: 'Moved', description: 'Tue 14:30' });
|
|
146
|
+
const id = toast.error({ title: 'Payment failed', duration: Infinity });
|
|
147
|
+
toast.info({ title: 'Deleted', action: { label: 'Undo', onClick: restore } });
|
|
148
|
+
toast.dismiss(id);
|
|
149
|
+
```
|
|
188
150
|
|
|
189
|
-
|
|
190
|
-
`
|
|
191
|
-
|
|
151
|
+
`position` also takes `'top-center'` and `'bottom-center'`; centred stacks swipe away vertically
|
|
152
|
+
(towards their edge). `offset` is the distance from the anchored edge and accepts any CSS length, so
|
|
153
|
+
the stack can clear a bar: `<Toaster position="top-center" offset="var(--navbar-height)" />`, or
|
|
154
|
+
`<Toaster position="bottom-center" offset="calc(var(--tabbar-height) + 8px)" />`.
|
|
192
155
|
|
|
193
|
-
|
|
194
|
-
otherwise publish silently, with a version number that says nothing about it. `dist/` is
|
|
195
|
-
gitignored and present in the tarball, which is the right way round: the build output is not
|
|
196
|
-
worth reviewing in a diff and is worth shipping.
|
|
156
|
+
### Touch feedback and sizing
|
|
197
157
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
158
|
+
- Button, list rows, `StatTile` and `TabBar` items share one pressed state: `:active` scales to
|
|
159
|
+
`--press-scale` (0.97) with a gray tint over `--press-duration` (120 ms); off under
|
|
160
|
+
`prefers-reduced-motion`. Our hover styles are inside `@media (hover: hover)`.
|
|
161
|
+
- `<Button size="touch">` has a 44px minimum height. `<Button iconOnly variant="ghost">` keeps its
|
|
162
|
+
visual size but gets a 44px hit area on coarse pointers.
|
|
163
|
+
- `Input`, `Textarea` and `Select` render at 16px on coarse pointers so iOS does not zoom on focus.
|
|
164
|
+
`inputMode` and `enterKeyHint` on `Input` / `Textarea` pass through to the native element:
|
|
165
|
+
`<Input type="text" inputMode="decimal" enterKeyHint="done" />`.
|
|
202
166
|
|
|
203
|
-
|
|
204
|
-
the scope to match the repository owner. `publishConfig.access: "public"` is set, because a scoped
|
|
205
|
-
package defaults to restricted and restricted needs a paid plan.
|
|
167
|
+
### NotificationCard
|
|
206
168
|
|
|
207
|
-
|
|
169
|
+
A persistent rich toast: it floats in a viewport corner and stays until it is closed.
|
|
208
170
|
|
|
209
|
-
|
|
210
|
-
|
|
171
|
+
```tsx
|
|
172
|
+
import { Button, NotificationCard } from '@kennycorrea/ui';
|
|
173
|
+
|
|
174
|
+
const [open, setOpen] = useState(true);
|
|
175
|
+
|
|
176
|
+
<NotificationCard
|
|
177
|
+
position="bottom-right" // bottom-left | top-right | top-left
|
|
178
|
+
open={open}
|
|
179
|
+
onOpenChange={setOpen}
|
|
180
|
+
onDismiss={(reason) => track(reason)} // 'close-button' | 'escape' | 'programmatic'
|
|
181
|
+
icon={<AppIcon />}
|
|
182
|
+
title="Add this app to your computer"
|
|
183
|
+
description="Open it like any other app."
|
|
184
|
+
expandLabel="Show me how"
|
|
185
|
+
collapseLabel="Hide steps"
|
|
186
|
+
actions={<Button onClick={install}>Install</Button>}
|
|
187
|
+
>
|
|
188
|
+
<ol>
|
|
189
|
+
<li>Open the browser menu</li>
|
|
190
|
+
<li>Choose "Install app"</li>
|
|
191
|
+
</ol>
|
|
192
|
+
</NotificationCard>;
|
|
193
|
+
```
|
|
211
194
|
|
|
212
|
-
|
|
213
|
-
`
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
195
|
+
- **Never auto-dismisses.** Controlled (`open`/`onOpenChange`) or uncontrolled (`defaultOpen`).
|
|
196
|
+
- **Slots**: `icon`, `title`, `description`, `children` (collapsible body), `actions` (footer).
|
|
197
|
+
- **Collapsible body** with a built-in toggle (`aria-expanded`/`aria-controls`), controlled
|
|
198
|
+
(`expanded`/`onExpandedChange`) or uncontrolled (`defaultExpanded`). Labels come from
|
|
199
|
+
`expandLabel`/`collapseLabel` or the `expand`/`collapse` strings.
|
|
200
|
+
- **Close** button named by `closeLabel` or the `close` string. `onDismiss` reports why it
|
|
201
|
+
closed; a controlled `open` set to `false` by the host is `'programmatic'`.
|
|
202
|
+
- **Non-modal**: no focus trap, the page stays interactive, and Escape closes it only while focus
|
|
203
|
+
is inside it.
|
|
204
|
+
- **Quiet**: a `section` labelled by its title and described by its description — not
|
|
205
|
+
`role="alert"` or a live region, so it is not announced on every page load.
|
|
206
|
+
- 16px from the corner, `min(24rem, 100vw - 2rem)` wide, above page content and below dialogs.
|
|
207
|
+
Enters and exits with a transform, switched off under `prefers-reduced-motion`.
|
|
208
|
+
- Themed as described under [Dark mode and portals](#dark-mode-and-portals); pass `container`
|
|
209
|
+
to choose the portal target.
|
|
217
210
|
|
|
218
|
-
|
|
219
|
-
blaming the workflow:
|
|
211
|
+
Full example, in light and dark: `examples/NotificationCards.tsx`.
|
|
220
212
|
|
|
221
|
-
|
|
222
|
-
GitHub's account-level secrets are for Codespaces; Actions reads repository, environment and
|
|
223
|
-
organisation secrets. The step references `secrets.npm_token`, and secret names are
|
|
224
|
-
case-insensitive, so `NPM_TOKEN` matches.
|
|
225
|
-
- **It must be an npm _Automation_ token** (or a granular token with publish rights). A classic
|
|
226
|
-
*Publish* token fails with `EOTP` the moment the account or org requires 2FA for publishing,
|
|
227
|
-
because there is no one there to type a one-time password. That failure reads like a bad
|
|
228
|
-
credential and is not one.
|
|
213
|
+
### Drawer
|
|
229
214
|
|
|
230
|
-
|
|
231
|
-
it is load-bearing for a reason that has nothing to do with quality:
|
|
232
|
-
without a `test` script the `build` job fails on `Missing script: "test"`, `publish-npm` never
|
|
233
|
-
runs because it needs `build`, and **the release publishes nothing while looking like it worked.**
|
|
234
|
-
That was the state when the workflow was first added.
|
|
235
|
-
|
|
236
|
-
### Where it is consumed from today
|
|
215
|
+
A bottom sheet with `collapsed`/`expanded` snap points and a draggable handle.
|
|
237
216
|
|
|
238
|
-
|
|
239
|
-
|
|
217
|
+
```tsx
|
|
218
|
+
// Non-modal: floating banner, page stays usable.
|
|
219
|
+
<Drawer mode="non-modal" title="Get started" peek={<p>Swipe up.</p>}
|
|
220
|
+
footer={<Button>Start</Button>}>
|
|
221
|
+
<ol>…</ol>
|
|
222
|
+
</Drawer>
|
|
240
223
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
"@kennycorrea/ui": "^0.1.1"
|
|
224
|
+
// Modal: scrim, focus trapped, page inert; Escape and the scrim close it.
|
|
225
|
+
<Drawer open={open} onOpenChange={setOpen} defaultSnap="expanded" title="New item"
|
|
226
|
+
showCloseButton>
|
|
227
|
+
<form>…</form>
|
|
228
|
+
</Drawer>
|
|
247
229
|
```
|
|
248
230
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
documents nothing and would let a lockfile built from scratch land back on it.
|
|
252
|
-
|
|
253
|
-
⚠ **Under `0.x`, `^` does not cross a minor.** `^0.1.1` will **not** take 0.2.0, so moving a
|
|
254
|
-
consumer onto the new theming API is a `package.json` change in each app, not just a lockfile
|
|
255
|
-
refresh. The free functions still work, so an app that does not need `createTheming` can stay
|
|
256
|
-
where it is — and the three existing apps deliberately do, for now. Moving them is a one-line
|
|
257
|
-
change each plus a lockfile, worth doing to stop three consumers drifting a minor behind, and
|
|
258
|
-
worth doing as its own commit rather than inside this one.
|
|
231
|
+
Snap and open state are controllable (`snap`/`onSnapChange`, `open`/`onOpenChange`). The handle
|
|
232
|
+
is a real button with `aria-expanded`. Example: `examples/Drawers.tsx`.
|
|
259
233
|
|
|
260
|
-
|
|
261
|
-
a consumer. An app that needs an unreleased change wants `npm link` or a temporary `file:`
|
|
262
|
-
override — never a copied file, which is the drift this package exists to end.
|
|
234
|
+
### Sheet and ResponsiveDialog
|
|
263
235
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
236
|
+
`Sheet` is a bottom sheet on Radix Dialog (focus trap, Escape, scrim and `aria-modal` are
|
|
237
|
+
Radix's). `detent` is `'medium'` (50% of the visible viewport), `'large'` (92%) or `'fit'` (the
|
|
238
|
+
content, capped at 90%). Pass `detents` to let the user move between several; dragging down from
|
|
239
|
+
the lowest dismisses.
|
|
268
240
|
|
|
269
241
|
```tsx
|
|
270
|
-
<
|
|
242
|
+
<Sheet open={open} onOpenChange={setOpen} title="New booking" detents={['medium', 'large']}
|
|
243
|
+
defaultDetent="medium" scaleBackground footer={<Button>Save</Button>}>
|
|
244
|
+
<form>…</form>
|
|
245
|
+
</Sheet>
|
|
271
246
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
toast.warning({ title: 'Close to the IVA ceiling', duration: 10_000 });
|
|
275
|
-
const id = toast.error({ title: 'Payment failed', duration: Infinity });
|
|
276
|
-
toast.info({ title: 'Cancelled', action: { label: 'Undo', onClick: restore } });
|
|
277
|
-
toast.dismiss(id); // or toast.dismiss() for all
|
|
247
|
+
<ResponsiveDialog open={open} onOpenChange={setOpen} title="Edit" sheetDetent="large"
|
|
248
|
+
footer={<Button>Save</Button>}>…</ResponsiveDialog>
|
|
278
249
|
```
|
|
279
250
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
251
|
+
A flick faster than 0.5 px/ms, or a drag of 25% of the height, dismisses. The body scrolls
|
|
252
|
+
normally and hands the gesture to the sheet only when it is at its top and pulled down. The footer
|
|
253
|
+
clears `--safe-bottom` and follows `visualViewport`, so it and the focused field stay above the
|
|
254
|
+
on-screen keyboard. `scaleBackground` scales the rest of the Theme at `large`; a transformed
|
|
255
|
+
ancestor re-parents `position: fixed` descendants, so fixed bars in the page scale with it.
|
|
256
|
+
`ResponsiveDialog` takes `Dialog`'s props plus `sheetDetent` and is a Dialog at `md` (768px) and
|
|
257
|
+
up; crossing the breakpoint while open remounts `children`, so keep form state in the parent.
|
|
283
258
|
|
|
284
|
-
|
|
259
|
+
### Slider
|
|
285
260
|
|
|
286
|
-
|
|
287
|
-
|
|
261
|
+
`label` is required and becomes the `aria-label`. Values are arrays, one per thumb.
|
|
262
|
+
`fillFrom` fills from a value to the thumb; `marks` adds labelled ticks.
|
|
288
263
|
|
|
289
264
|
```tsx
|
|
290
|
-
<Slider label="
|
|
291
|
-
|
|
292
|
-
|
|
265
|
+
<Slider label="Lead time (hours)" min={1} max={48} value={[h]} onValueChange={([v]) => setH(v)} />
|
|
266
|
+
<Slider label="Cycle" min={0} max={29} value={day} onValueChange={setDay} fillFrom={16}
|
|
267
|
+
marks={[0, 16, 29].map((value) => ({ value, label: String(value) }))} />
|
|
293
268
|
```
|
|
294
269
|
|
|
295
|
-
|
|
270
|
+
## Development
|
|
296
271
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
```tsx
|
|
300
|
-
const marks = [0, 6, 11, 16, 22, 29].map((value) => ({ value, label: String(value) }));
|
|
301
|
-
<Slider label="Budget cycle" min={0} max={29} value={day} onValueChange={setDay}
|
|
302
|
-
fillFrom={16} marks={marks} />
|
|
272
|
+
```bash
|
|
273
|
+
npm test
|
|
303
274
|
```
|
|
304
275
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
Without it, nothing changed. Limits and details: [CHANGELOG.md](CHANGELOG.md); a full example
|
|
309
|
-
is in `examples/BudgetCycleSlider.tsx`.
|
|
310
|
-
|
|
311
|
-
⚠ `--ui-slider-thumb-size` mirrors Themes' size-2 thumb (`--space-2 + --space-1`). The fill
|
|
312
|
-
and the marks are placed from it, so if Themes ever resizes its thumb, change it there.
|
|
313
|
-
|
|
314
|
-
## Drawer — bottom sheet
|
|
315
|
-
|
|
316
|
-
A sheet anchored to the bottom of the viewport: full width on phones, `max-w-lg` and centred
|
|
317
|
-
above that (`maxWidthClassName` to change it), safe-area aware, themed from the house tokens.
|
|
318
|
-
|
|
319
|
-
```tsx
|
|
320
|
-
// Non-modal: a floating banner, the page stays usable, no focus trap.
|
|
321
|
-
<Drawer mode="non-modal" title="Take deposits" peek={<p>Swipe up to see how.</p>}
|
|
322
|
-
footer={<Button>Get started</Button>}>
|
|
323
|
-
<ol>…steps…</ol>
|
|
324
|
-
</Drawer>
|
|
325
|
-
|
|
326
|
-
// Modal: scrim, focus trapped, page inert, Escape and the scrim close it.
|
|
327
|
-
<Drawer open={open} onOpenChange={setOpen} defaultSnap="expanded" title="New client"
|
|
328
|
-
showCloseButton footer={<Button type="submit" form="f">Save</Button>}>
|
|
329
|
-
<form id="f">…</form>
|
|
330
|
-
</Drawer>
|
|
331
|
-
```
|
|
276
|
+
runs lint (Biome), a typecheck and the Vitest suite (jsdom + Testing Library). `npm run build`
|
|
277
|
+
compiles to `dist/`. Relative imports carry an explicit `.js` extension so the output works
|
|
278
|
+
under Node's ESM resolution as well as bundlers.
|
|
332
279
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
`open`/`onOpenChange`/`defaultOpen`.
|
|
336
|
-
- **Drag** the header with touch or mouse: it follows the pointer, then settles past 60px or a
|
|
337
|
-
flick faster than 0.5px/ms. Down from collapsed closes when `dismissible` (the default).
|
|
338
|
-
A drag from inside the body counts only when the body is scrolled to the top and moving down.
|
|
339
|
-
- **The handle is a real button** with `aria-expanded`/`aria-controls`, so keyboard and screen
|
|
340
|
-
reader users toggle it the same way.
|
|
341
|
-
- **No words of its own.** The handle and close button names come from `expandLabel`,
|
|
342
|
-
`collapseLabel` and `closeLabel`, or the new `expand`, `collapse` and `close` keys of
|
|
343
|
-
`UiStringsProvider`.
|
|
344
|
-
- Motion is a CSS transform, switched off under `prefers-reduced-motion`.
|
|
345
|
-
|
|
346
|
-
It portals into the nearest `<Theme>` so it inherits the appearance and accent, falling back to
|
|
347
|
-
`<body>`; `container` overrides that.
|
|
348
|
-
|
|
349
|
-
Built by hand rather than on Radix Dialog because the non-modal half must not do what Dialog
|
|
350
|
-
does to the rest of the page. Both examples are in `examples/Drawers.tsx`.
|
|
280
|
+
Publishing happens from the GitHub workflow on a release; `prepublishOnly` rebuilds `dist`.
|
|
281
|
+
Changes are listed in [CHANGELOG.md](CHANGELOG.md).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export type ActionSheetTone = 'neutral' | 'brand' | 'danger';
|
|
3
|
+
export interface ActionSheetAction {
|
|
4
|
+
label: string;
|
|
5
|
+
/** `danger` is red. Default `neutral`. */
|
|
6
|
+
tone?: ActionSheetTone;
|
|
7
|
+
icon?: ReactNode;
|
|
8
|
+
/** Runs, then the sheet closes. */
|
|
9
|
+
onSelect: () => void;
|
|
10
|
+
}
|
|
11
|
+
export interface ActionSheetProps {
|
|
12
|
+
open: boolean;
|
|
13
|
+
onOpenChange: (open: boolean) => void;
|
|
14
|
+
actions: ActionSheetAction[];
|
|
15
|
+
title?: ReactNode;
|
|
16
|
+
message?: ReactNode;
|
|
17
|
+
/** Overrides the Cancel button's label (otherwise the `cancel` UI string). */
|
|
18
|
+
cancelLabel?: string;
|
|
19
|
+
/** Where the portal mounts. Default: the nearest `<Theme>`, else rendered in place. */
|
|
20
|
+
container?: HTMLElement | null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A short list of actions to choose between, on Radix `AlertDialog`: below `md` anchored to the
|
|
24
|
+
* bottom as a grouped list with a separated Cancel; at `md` and up a small centred dialog.
|
|
25
|
+
*
|
|
26
|
+
* AlertDialog rather than Dialog because the user is being asked to decide — it cannot be
|
|
27
|
+
* dismissed by clicking outside, and focus lands on Cancel, so Enter never fires a destructive
|
|
28
|
+
* action by accident. Escape and Cancel both close it without running anything.
|
|
29
|
+
*/
|
|
30
|
+
export declare function ActionSheet({ open, onOpenChange, actions, title, message, cancelLabel, container, }: ActionSheetProps): import("react").JSX.Element;
|