@kennycorrea/ui 0.10.0 → 0.11.1
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 +117 -16
- package/README.md +92 -1
- 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/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/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 +21 -0
- package/dist/atoms/ScrollStrip/ScrollStrip.js +62 -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 +26 -0
- package/dist/atoms/SegmentedControl/SegmentedControl.js +44 -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 +11 -0
- package/dist/atoms/index.js +11 -0
- package/dist/primitives/index.d.ts +1 -1
- package/dist/primitives/index.js +1 -1
- package/dist/strings.d.ts +12 -0
- package/dist/strings.js +6 -0
- package/package.json +4 -1
- package/tokens.css +401 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,110 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.11.1 — 2026-10-19
|
|
4
|
+
|
|
5
|
+
- **`ScrollStrip`** (axe `scrollable-region-focusable`) — now `role="region"` with an accessible name (`aria-label`, default `UiStrings.scrollableRegion` "Scrollable content"). It is a tab stop (`tabIndex=0`) only while its content overflows (ResizeObserver), with a `:focus-visible` ring; ArrowLeft/Right scroll about one item, Home/End jump. Layout, snap and `-mx-4` usage unchanged.
|
|
6
|
+
- **`SegmentedControl`** (axe `target-size`, WCAG 2.5.8) — items no longer shrink below their content, so five items on a 375px screen scroll the track horizontally instead of dropping under 24px; each item has a 24px minimum width, and on `(pointer: coarse)` a 44px hit area via `::before` (visual size unchanged). Labels now sit in a `.ui-segmented__label` span. Arrow-key radio behaviour unchanged.
|
|
7
|
+
- **`ChipGroup`** — 44px hit area on coarse pointers via `::before`. `TabBar` already met both points (pressed state via `aria-current`, 44px items).
|
|
8
|
+
- New string: `UiStrings.scrollableRegion`. New dev dependency: `axe-core` (tests).
|
|
9
|
+
|
|
10
|
+
## 0.11.0 — 2026-10-19
|
|
11
|
+
|
|
12
|
+
- **Touch feedback** — Button, list rows, `StatTile` and `TabBar` items share a pressed state (`:active` scale `--press-scale` 0.97 + tint, `--press-duration` 120 ms); hover styles are in `@media (hover: hover)`. TabBar no longer dims on press.
|
|
13
|
+
- **`Button size="touch"`** (44px) and **`iconOnly`**, which grows the hit area to 44px on coarse pointers via `::before` without changing the visual size.
|
|
14
|
+
- **`Toaster`** — `position` adds `top-center` / `bottom-center` (vertical swipe-to-dismiss); `offset` takes any CSS length (e.g. `var(--tabbar-height)`).
|
|
15
|
+
- **Fields** — `Input`, `Textarea` and the `Select` trigger are 16px on coarse pointers (no iOS focus zoom); `inputMode` / `enterKeyHint` passthrough documented.
|
|
16
|
+
|
|
17
|
+
- **`PullToRefresh`** — wraps the page (`scroller="document"`, default) or its own scroll container (`"self"`). A touch drag from scroll-top past `threshold` shows a spinner and calls `onRefresh(): Promise<void>`, holding until it settles. Active only with `(pointer: coarse)` and only when the scroller is at the top; no effect on desktop. No transitions under `prefers-reduced-motion`. Label from `UiStrings.refreshing` (default "Refreshing"). A shortcut only: the parent must still offer a visible refresh action.
|
|
18
|
+
|
|
19
|
+
- **`StatTile`** — a compact KPI tile on `Card` (`variant`, `density`): `label`, `value` (tabular numerals), optional `delta` `{ value, direction, tone, label }` with an arrow and a positive/negative/neutral tone independent of direction, a `sparkline` slot, and `href` / `onClick` that make the whole tile a link or button with a chevron.
|
|
20
|
+
- **`ScrollStrip`** — a horizontal scroll-snap row (`snap-x mandatory`, scroll padding equal to `--page-gutter`, hidden scrollbar, items peek at the edge via `itemWidth`) that becomes a `columns`-wide CSS grid from `gridFrom` (default `md`). For KPI rows and card carousels.
|
|
21
|
+
- New token: `--page-gutter` (optional; falls back to 16px).
|
|
22
|
+
|
|
23
|
+
- **`SegmentedControl`** — one choice from 2–5 `options` `{ value, label, disabled? }` on a pill track with a sliding thumb (CSS transform, off under `prefers-reduced-motion`). Equal-width segments, `fullWidth`, `size` `sm` | `md`. Radio semantics with a roving tabindex: arrow keys move and select (wrapping, skipping disabled), Home/End jump. Hand-built; no new dependency.
|
|
24
|
+
- **`ChipGroup`** — horizontally scrolling filter chips, single (`value: string | null`, click the selected chip to clear) or `multiple` (`value: string[]`). Scroll-snap, fade edges that show only where there is more to scroll, and the selected chip scrolled into view. Chips are `aria-pressed` buttons; Left/Right/Home/End move focus.
|
|
25
|
+
- **Breaking:** `SegmentedControl` is now the new atom; Radix Themes' one moved to `RadixSegmentedControl` in `primitives`.
|
|
26
|
+
|
|
27
|
+
#
|
|
28
|
+
- **`NavBar`** — a sticky top bar for small screens: `title`, `leading` (a back-button slot) and
|
|
29
|
+
`trailing` (icon buttons). With `largeTitle`, a large left-aligned title sits in the content flow
|
|
30
|
+
and collapses into a small centred title in the bar as the page scrolls past it. The bar gains
|
|
31
|
+
`--material-bar` and a hairline only once scrolled; one `IntersectionObserver`, no scroll
|
|
32
|
+
listeners. Pads for `--safe-top`; height exposed as `--navbar-height`. Accessible: the large title
|
|
33
|
+
is the single `h1` and the small copy is `aria-hidden` (without `largeTitle` the small title is
|
|
34
|
+
the `h1`).
|
|
35
|
+
- **`BackButton`** — chevron plus optional `label`, 44px target; with no label its name is `UiStrings.back`.
|
|
36
|
+
- `UiStrings.back` (default "Back").
|
|
37
|
+
- **`TabBar`** / **`TabBarSpacer`** — fixed bottom navigation, 3–5 `items` `{ value, label, icon, activeIcon?, badge?, render? }`; `render` receives the props to spread on a router link (`aria-current="page"` on the active one). 49px + `--safe-bottom` (exposed as `--tabbar-height`), `--material-bar` ground, hairline top, brand-coloured active item, `:active` press feedback, hidden from `hideFrom` (default `md`).
|
|
38
|
+
- **`ActionSheet`** — `{ label, tone, icon?, onSelect }[]` with optional `title` / `message` and a
|
|
39
|
+
separated Cancel, on the grouped surface. Below `md` it is anchored to the bottom; at `md` and up
|
|
40
|
+
a small centred dialog. Built on Radix `AlertDialog`: no outside-click dismissal, and focus lands
|
|
41
|
+
on Cancel so Enter never fires a destructive action. Escape and Cancel run nothing.
|
|
42
|
+
- **`confirm({ title, message, confirmLabel, cancelLabel, tone })`** — resolves `true` on accept and
|
|
43
|
+
`false` on Cancel, Escape or any other dismissal; never rejects. Callable from anywhere, like
|
|
44
|
+
`toast()`. Mount one `<ConfirmProvider />` inside `<Theme>`; concurrent calls queue.
|
|
45
|
+
- `UiStrings.cancel` and `UiStrings.confirm` (defaults "Cancel", "Confirm").
|
|
46
|
+
- New dependency: `@radix-ui/react-alert-dialog`.
|
|
47
|
+
- **`Sheet`** — a bottom sheet on Radix Dialog: drag handle, `detent` `medium` | `large` | `fit`
|
|
48
|
+
(several at once via `detents`), velocity-based dismiss, a scrolling body that hands off to the
|
|
49
|
+
drag only at scroll-top, a footer above `--safe-bottom` and the keyboard (`visualViewport`), and
|
|
50
|
+
opt-in `scaleBackground` at `large`. Portals into the nearest Theme like the other floating atoms.
|
|
51
|
+
- **`ResponsiveDialog`** — `Dialog`'s props plus `sheetDetent`; a Dialog at `md` and up, a Sheet below.
|
|
52
|
+
- New dependency: `@radix-ui/react-dialog`.
|
|
53
|
+
- **`ListRow` `swipeActions`** `{ leading?, trailing? }` of `{ label, icon?, tone?, onAction }`, and opt-in
|
|
54
|
+
`fullSwipe` (past 60% of the row runs the first trailing action). Drag reveals the actions with
|
|
55
|
+
rubber-banding; one row open per `ListSection`; tap outside, Escape or tapping the row closes it.
|
|
56
|
+
The gesture is offered only on touch and without `prefers-reduced-motion`. The same actions are
|
|
57
|
+
always in the row's context menu (right-click, long-press) and in an overflow button — visible
|
|
58
|
+
when the gesture is off, visually hidden but focusable when it is on.
|
|
59
|
+
- `UiStrings.moreActions` — the overflow button's accessible name (default "More actions").
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
- **`StatTile`** — a compact KPI tile on `Card` (`variant`, `density`): `label`, `value` (tabular numerals), optional `delta` `{ value, direction, tone, label }` with an arrow and a positive/negative/neutral tone independent of direction, a `sparkline` slot, and `href` / `onClick` that make the whole tile a link or button with a chevron.
|
|
63
|
+
- **`ScrollStrip`** — a horizontal scroll-snap row (`snap-x mandatory`, scroll padding equal to `--page-gutter`, hidden scrollbar, items peek at the edge via `itemWidth`) that becomes a `columns`-wide CSS grid from `gridFrom` (default `md`). For KPI rows and card carousels.
|
|
64
|
+
- New token: `--page-gutter` (optional; falls back to 16px).
|
|
4
65
|
|
|
5
66
|
### Added
|
|
6
|
-
- **`
|
|
67
|
+
- **`ListSection`, `ListRow` and `ListRowSkeleton`** — a native-style grouped list. A section
|
|
68
|
+
wraps rows on the grouped surface, with optional `header` and `footer` text outside the group
|
|
69
|
+
and `variant: 'inset' | 'full'`. Hairlines start after the leading slot. A row takes
|
|
70
|
+
`leading`, `title`, `subtitle`, `trailing`, `chevron`, `disabled` and `destructive`, and renders
|
|
71
|
+
a `button` (`onClick`), an `a` (`href`), your own element (`asChild`) or a plain `div`. Rows
|
|
72
|
+
are at least `--tap-min` tall with a pressed state; hover applies only under
|
|
73
|
+
`@media (hover: hover)`. Titles truncate, trailing values use tabular numerals. The list is
|
|
74
|
+
`<ul role="list">`. The skeleton announces `loading` from the strings provider.
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
- **`StatTile`** — a compact KPI tile on `Card` (`variant`, `density`): `label`, `value` (tabular numerals), optional `delta` `{ value, direction, tone, label }` with an arrow and a positive/negative/neutral tone independent of direction, a `sparkline` slot, and `href` / `onClick` that make the whole tile a link or button with a chevron.
|
|
78
|
+
- **`ScrollStrip`** — a horizontal scroll-snap row (`snap-x mandatory`, scroll padding equal to `--page-gutter`, hidden scrollbar, items peek at the edge via `itemWidth`) that becomes a `columns`-wide CSS grid from `gridFrom` (default `md`). For KPI rows and card carousels.
|
|
79
|
+
- New token: `--page-gutter` (optional; falls back to 16px).
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
- **Mobile presentation layer in `tokens.css`** — an opt-in "grouped" surface style (cards on a
|
|
83
|
+
tinted canvas). Tokens: `--surface-grouped`, `--canvas-grouped`, `--separator-inset`,
|
|
84
|
+
`--radius-group` (12px), `--space-row-y` / `--space-row-x` (10 / 12px), `--tap-min` (44px),
|
|
85
|
+
`--safe-top/right/bottom/left` (`env(safe-area-inset-*)`, 0 fallback) and a translucent
|
|
86
|
+
`--material-bar` with `--material-bar-filter` (`saturate(180%) blur(20px)`).
|
|
87
|
+
- Tailwind utilities: `bg-grouped`, `bg-canvas-grouped`, `border-separator-inset`,
|
|
88
|
+
`rounded-group`, `min-h-tap`, `pt-safe`, `pb-safe`, `px-safe`, `bg-material`.
|
|
89
|
+
- Gray-11 and gray-12 text passes WCAG AA on the card, canvas and bar in light and dark
|
|
90
|
+
(`test/grouped.test.tsx`). README section with a before/after example.
|
|
91
|
+
|
|
92
|
+
- **`Card` `variant` and `density`** (also on `CardHeader`, `CardBody`; `density` on
|
|
93
|
+
`CardTitle`). `variant`: `outlined` (default, the current look), `grouped` (no border or
|
|
94
|
+
shadow, `bg-grouped`, `rounded-group`) or `plain` (no surface). `density`: `comfortable`
|
|
95
|
+
(default) or `compact` (row spacing tokens, smaller title). Both take Radix-style responsive
|
|
96
|
+
objects (`{ initial: 'grouped', md: 'outlined' }`, breakpoints `initial`, `xs`, `sm`, `md`,
|
|
97
|
+
`lg`, `xl`). Parts inherit from the `Card` and can override. `raised` is kept, and ignored by
|
|
98
|
+
`grouped` and `plain`. Exports `CardVariant`, `CardDensity`, `Responsive`. Responsive values
|
|
99
|
+
are data attributes matched by media queries in `tokens.css` (Radix breakpoints, not
|
|
100
|
+
Tailwind's). Example in `examples/Cards.tsx`, tests in `test/Card.test.tsx`.
|
|
101
|
+
|
|
102
|
+
No visual change to existing atoms.
|
|
103
|
+
|
|
104
|
+
## 0.10.0 — 2026-10-19
|
|
105
|
+
|
|
106
|
+
### Added
|
|
107
|
+
- **`NotificationCard`** — a persistent rich toast in a viewport corner: icon, title,
|
|
7
108
|
description, collapsible body and actions; controlled or uncontrolled; `onDismiss(reason)`;
|
|
8
109
|
non-modal, Escape only while focused; a labelled `section`, not a live region.
|
|
9
110
|
|
|
@@ -12,17 +113,17 @@
|
|
|
12
113
|
inside one, its portal is wrapped in a nested `<Theme>` so it keeps the appearance.
|
|
13
114
|
- README rewritten as a generic presentation of the library.
|
|
14
115
|
|
|
15
|
-
## 0.9.1
|
|
116
|
+
## 0.9.1 — 2026-10-18
|
|
16
117
|
|
|
17
118
|
### Fixed
|
|
18
119
|
- **`Drawer` renders inside the nearest `<Theme>`** (`.radix-themes`) instead of under
|
|
19
120
|
`<body>`. Outside a Theme none of the house tokens or the accent are defined, so in 0.9.0 the
|
|
20
|
-
sheet lost the app's appearance
|
|
121
|
+
sheet lost the app's appearance — a dark app got a light sheet. Falls back to `<body>` when
|
|
21
122
|
there is no Theme; `container` still overrides it.
|
|
22
123
|
- In `modal` mode, everything outside the sheet goes `inert` at every level up to `<body>`, not
|
|
23
124
|
only the sheet's siblings, now that the sheet sits inside the Theme rather than beside it.
|
|
24
125
|
|
|
25
|
-
## 0.9.0
|
|
126
|
+
## 0.9.0 — 2026-10-18
|
|
26
127
|
|
|
27
128
|
### Added
|
|
28
129
|
- **`Drawer`**, a bottom sheet with `collapsed` and `expanded` snap points, in two modes:
|
|
@@ -37,7 +138,7 @@
|
|
|
37
138
|
### Not verified
|
|
38
139
|
- Only exercised in jsdom with simulated pointer events, not on a real touch device.
|
|
39
140
|
|
|
40
|
-
## 0.8.1
|
|
141
|
+
## 0.8.1 — 2026-10-11
|
|
41
142
|
|
|
42
143
|
### Fixed
|
|
43
144
|
- **`Badge` text passes WCAG AA in both appearances.** Soft badges wrote their text in step a11
|
|
@@ -58,7 +159,7 @@
|
|
|
58
159
|
### Docs
|
|
59
160
|
- README: the colour scales the atoms need, and their order relative to `base.css`.
|
|
60
161
|
|
|
61
|
-
## 0.8.0
|
|
162
|
+
## 0.8.0 — 2026-10-09
|
|
62
163
|
|
|
63
164
|
### Added
|
|
64
165
|
- **Every Radix Themes component the atoms do not wrap**, re-exported unchanged from
|
|
@@ -68,11 +169,11 @@
|
|
|
68
169
|
RadioCards, RadioGroup, ScrollArea, SegmentedControl, TabNav, Table, Tabs, TextField,
|
|
69
170
|
AccessibleIcon, Portal, Reset, Slot, Slottable, ThemePanel, VisuallyHidden. Where an atom
|
|
70
171
|
exists it remains the export under that name.
|
|
71
|
-
- **`ThemeRoot`**
|
|
172
|
+
- **`ThemeRoot`** — Themes' `<Theme>`, renamed because `Theme` is already this package's type.
|
|
72
173
|
- **All of Radix Icons** (`@radix-ui/react-icons`, 320 `*Icon` components). `Icon` is
|
|
73
174
|
unchanged and still the house glyph set.
|
|
74
175
|
|
|
75
|
-
## 0.7.0
|
|
176
|
+
## 0.7.0 — 2026-10-09
|
|
76
177
|
|
|
77
178
|
### Added
|
|
78
179
|
- **`Alert`**: an inline message with `type` (`info` default, `warning`, `success`, `error`,
|
|
@@ -81,7 +182,7 @@
|
|
|
81
182
|
theme's own accent. Errors and warnings are `role="alert"`, the rest `role="status"`.
|
|
82
183
|
- **`Skeleton`** (Themes' skeleton) and **`SkeletonText`** (a `lines`-long paragraph placeholder).
|
|
83
184
|
|
|
84
|
-
## 0.6.1
|
|
185
|
+
## 0.6.1 — 2026-10-08
|
|
85
186
|
|
|
86
187
|
### Fixed
|
|
87
188
|
- **`Slider` thumbs always have an accessible name.** Without `fillFrom`, `label` landed on the
|
|
@@ -91,7 +192,7 @@
|
|
|
91
192
|
Radix's bare "Minimum"/"Maximum". Consumers using `fillFrom={min}` only to get a name can
|
|
92
193
|
drop it.
|
|
93
194
|
|
|
94
|
-
## 0.6.0
|
|
195
|
+
## 0.6.0 — 2026-10-07
|
|
95
196
|
|
|
96
197
|
### Added
|
|
97
198
|
- **`RichText`**: a Tiptap editor (StarterKit) under a Radix `Toolbar`. Marks (bold, italic,
|
|
@@ -99,22 +200,22 @@
|
|
|
99
200
|
one, so the toolbar is one tab stop with arrow keys. `value`/`onChange` speak HTML; also
|
|
100
201
|
`placeholder`, `disabled`, `invalid`, `rows`, `aria-label`/`aria-labelledby`, and a ref
|
|
101
202
|
exposing `{ editor, focus }`.
|
|
102
|
-
Styled as a Themes `TextArea` (surface, size 2) from public tokens
|
|
103
|
-
background, invalid and disabled states
|
|
203
|
+
Styled as a Themes `TextArea` (surface, size 2) from public tokens — border, focus ring,
|
|
204
|
+
background, invalid and disabled states — in `.ui-richtext*` in `tokens.css`.
|
|
104
205
|
- `UiStrings` gains `formatting`, `bold`, `italic`, `strike`, `heading`, `bulletList`,
|
|
105
206
|
`orderedList` (English defaults; hosts should translate them).
|
|
106
207
|
|
|
107
208
|
### Requires
|
|
108
209
|
- `tokens.css` imported; the editor is unstyled without it.
|
|
109
210
|
|
|
110
|
-
## 0.5.0
|
|
211
|
+
## 0.5.0 — 2026-10-06
|
|
111
212
|
|
|
112
213
|
### Added
|
|
113
214
|
- **`Slider` `fillFrom`**: fills from a fixed value to the thumb, in either direction, instead of
|
|
114
215
|
from `min`. Same units as `value`, clamped to [min, max]; nothing is filled when the thumb sits
|
|
115
216
|
on it. The fill's ends line up with the thumb's centre at every value, using Radix's own
|
|
116
217
|
in-bounds offset. Built on `@radix-ui/react-slider` and styled from Themes' public tokens
|
|
117
|
-
(`.ui-slider*` in `tokens.css`), because Themes' slider gives no way to place its range
|
|
218
|
+
(`.ui-slider*` in `tokens.css`), because Themes' slider gives no way to place its range —
|
|
118
219
|
consumers overriding `.rt-SliderRange` can drop that CSS.
|
|
119
220
|
Single horizontal left-to-right thumb only: with two thumbs, `orientation="vertical"`,
|
|
120
221
|
`inverted` or `dir="rtl"` it is ignored with a development warning. Margin props are not
|
|
@@ -127,7 +228,7 @@
|
|
|
127
228
|
- Without `fillFrom`, `Slider` is still the Radix Themes slider, rendered exactly as in 0.4.0.
|
|
128
229
|
|
|
129
230
|
### Requires
|
|
130
|
-
- `tokens.css` imported, as it already must be for toasts
|
|
231
|
+
- `tokens.css` imported, as it already must be for toasts — the `fillFrom` path is unstyled
|
|
131
232
|
without it.
|
|
132
233
|
|
|
133
234
|
## 0.4.0
|
package/README.md
CHANGED
|
@@ -85,9 +85,53 @@ Do the same in your own code: set appearance on `<Theme>`, not with a `dark` cla
|
|
|
85
85
|
## Strings
|
|
86
86
|
|
|
87
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
|
|
88
|
+
placeholder, close/expand/collapse names, the `ScrollStrip` region name (`scrollableRegion`) — come from `UiStringsProvider`, which takes a partial
|
|
89
89
|
`UiStrings` object. Defaults are English. Most components also accept the label as a prop.
|
|
90
90
|
|
|
91
|
+
## Mobile presentation layer
|
|
92
|
+
|
|
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.
|
|
95
|
+
|
|
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)` |
|
|
106
|
+
|
|
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`.
|
|
109
|
+
|
|
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.
|
|
113
|
+
|
|
114
|
+
**Before** — a bordered panel:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
<div className="panel p-4">
|
|
118
|
+
<button className="py-2">Profile</button>
|
|
119
|
+
</div>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**After** — a grouped list under a blurred bar:
|
|
123
|
+
|
|
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
|
+
```
|
|
134
|
+
|
|
91
135
|
## Components
|
|
92
136
|
|
|
93
137
|
### Toast
|
|
@@ -104,6 +148,22 @@ toast.info({ title: 'Deleted', action: { label: 'Undo', onClick: restore } });
|
|
|
104
148
|
toast.dismiss(id);
|
|
105
149
|
```
|
|
106
150
|
|
|
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)" />`.
|
|
155
|
+
|
|
156
|
+
### Touch feedback and sizing
|
|
157
|
+
|
|
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" />`.
|
|
166
|
+
|
|
107
167
|
### NotificationCard
|
|
108
168
|
|
|
109
169
|
A persistent rich toast: it floats in a viewport corner and stays until it is closed.
|
|
@@ -171,6 +231,31 @@ A bottom sheet with `collapsed`/`expanded` snap points and a draggable handle.
|
|
|
171
231
|
Snap and open state are controllable (`snap`/`onSnapChange`, `open`/`onOpenChange`). The handle
|
|
172
232
|
is a real button with `aria-expanded`. Example: `examples/Drawers.tsx`.
|
|
173
233
|
|
|
234
|
+
### Sheet and ResponsiveDialog
|
|
235
|
+
|
|
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.
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
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>
|
|
246
|
+
|
|
247
|
+
<ResponsiveDialog open={open} onOpenChange={setOpen} title="Edit" sheetDetent="large"
|
|
248
|
+
footer={<Button>Save</Button>}>…</ResponsiveDialog>
|
|
249
|
+
```
|
|
250
|
+
|
|
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.
|
|
258
|
+
|
|
174
259
|
### Slider
|
|
175
260
|
|
|
176
261
|
`label` is required and becomes the `aria-label`. Values are arrays, one per thumb.
|
|
@@ -194,3 +279,9 @@ under Node's ESM resolution as well as bundlers.
|
|
|
194
279
|
|
|
195
280
|
Publishing happens from the GitHub workflow on a release; `prepublishOnly` rebuilds `dist`.
|
|
196
281
|
Changes are listed in [CHANGELOG.md](CHANGELOG.md).
|
|
282
|
+
|
|
283
|
+
## Accessibility notes (mobile)
|
|
284
|
+
|
|
285
|
+
- **ScrollStrip** is a named `region` (`aria-label`, default `UiStrings.scrollableRegion`) and a tab stop only while it overflows; arrow keys scroll it by about one item.
|
|
286
|
+
- **SegmentedControl** scrolls horizontally when its items do not fit rather than shrinking them below 24px; on coarse pointers hit areas are 44px. With more than 4 options on a phone, prefer `ChipGroup`.
|
|
287
|
+
- **ChipGroup** chips are `aria-pressed` buttons with a 44px coarse-pointer hit area.
|
|
@@ -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;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import * as AlertDialog from '@radix-ui/react-alert-dialog';
|
|
3
|
+
import { Theme } from '@radix-ui/themes';
|
|
4
|
+
import { useSyncExternalStore } from 'react';
|
|
5
|
+
import { useUiStrings } from '../../strings.js';
|
|
6
|
+
import { cn } from '../../utils/cn.js';
|
|
7
|
+
import { useThemePortalTarget } from '../../utils/themePortal.js';
|
|
8
|
+
/** Tailwind's `md`. At and above it the sheet becomes a small centred dialog. */
|
|
9
|
+
const MD = '(min-width: 768px)';
|
|
10
|
+
function subscribe(onChange) {
|
|
11
|
+
const query = window.matchMedia(MD);
|
|
12
|
+
query.addEventListener('change', onChange);
|
|
13
|
+
return () => query.removeEventListener('change', onChange);
|
|
14
|
+
}
|
|
15
|
+
function useIsMdUp() {
|
|
16
|
+
return useSyncExternalStore(subscribe, () => window.matchMedia(MD).matches, () => true);
|
|
17
|
+
}
|
|
18
|
+
const TONE = {
|
|
19
|
+
neutral: 'text-ink',
|
|
20
|
+
brand: 'text-brand-700',
|
|
21
|
+
danger: 'text-[var(--red-11)]',
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* A short list of actions to choose between, on Radix `AlertDialog`: below `md` anchored to the
|
|
25
|
+
* bottom as a grouped list with a separated Cancel; at `md` and up a small centred dialog.
|
|
26
|
+
*
|
|
27
|
+
* AlertDialog rather than Dialog because the user is being asked to decide — it cannot be
|
|
28
|
+
* dismissed by clicking outside, and focus lands on Cancel, so Enter never fires a destructive
|
|
29
|
+
* action by accident. Escape and Cancel both close it without running anything.
|
|
30
|
+
*/
|
|
31
|
+
export function ActionSheet({ open, onOpenChange, actions, title, message, cancelLabel, container, }) {
|
|
32
|
+
const strings = useUiStrings();
|
|
33
|
+
const mdUp = useIsMdUp();
|
|
34
|
+
const { anchorRef, target, resolved } = useThemePortalTarget(container);
|
|
35
|
+
const cancel = cancelLabel ?? strings.cancel;
|
|
36
|
+
const hasHeader = title != null || message != null;
|
|
37
|
+
const header = hasHeader ? (_jsxs("div", { className: "px-4 py-3 text-center", children: [title != null ? (_jsx(AlertDialog.Title, { className: "m-0 text-sm font-medium text-ink", children: title })) : (_jsx(AlertDialog.Title, { className: "sr-only", children: cancel })), message != null ? (_jsx(AlertDialog.Description, { className: "m-0 mt-0.5 text-sm text-ink-muted", children: message })) : null] })) : null;
|
|
38
|
+
const rows = actions.map((a) => (_jsxs(AlertDialog.Action, { onClick: a.onSelect, "data-tone": a.tone ?? 'neutral', className: cn('flex min-h-[var(--tap-min,44px)] w-full items-center justify-center gap-2 border-t border-hairline px-4 text-base', 'outline-none hover:bg-[var(--gray-a3)] focus-visible:bg-[var(--gray-a3)] active:bg-[var(--gray-a4)]', TONE[a.tone ?? 'neutral']), children: [a.icon ? _jsx("span", { "aria-hidden": "true", children: a.icon }) : null, a.label] }, a.label)));
|
|
39
|
+
const cancelButton = (_jsx(AlertDialog.Cancel, { className: cn('flex min-h-[var(--tap-min,44px)] w-full items-center justify-center px-4 text-base font-medium text-ink', 'outline-none hover:bg-[var(--gray-a3)] focus-visible:bg-[var(--gray-a3)] active:bg-[var(--gray-a4)]'), children: cancel }));
|
|
40
|
+
const body = mdUp ? (_jsxs(AlertDialog.Content, { "data-presentation": "dialog", ...(message == null ? { 'aria-describedby': undefined } : {}), className: "ui-sheet-dialog fixed left-1/2 top-1/2 z-50 w-[calc(100%-2rem)] max-w-sm -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-2xl bg-sheet text-ink shadow-2xl outline-none ring-1 ring-hairline", children: [header, _jsx("div", { className: cn(!hasHeader && '[&>*:first-child]:border-t-0'), children: rows }), _jsx("div", { className: "border-t border-hairline", children: cancelButton })] })) : (_jsxs(AlertDialog.Content, { "data-presentation": "sheet", ...(message == null ? { 'aria-describedby': undefined } : {}), className: "ui-sheet fixed inset-x-0 bottom-0 z-50 mx-auto flex w-full max-w-lg flex-col gap-2 px-2 pb-[max(0.5rem,var(--safe-bottom,0px))] outline-none", children: [_jsxs("div", { className: "overflow-hidden rounded-xl bg-grouped", children: [header, _jsx("div", { className: cn(!hasHeader && '[&>*:first-child]:border-t-0'), children: rows })] }), _jsx("div", { className: "overflow-hidden rounded-xl bg-grouped", children: cancelButton })] }));
|
|
41
|
+
return (_jsxs(_Fragment, { children: [_jsx("span", { ref: anchorRef, hidden: true }), resolved ? (_jsx(AlertDialog.Root, { open: open, onOpenChange: onOpenChange, children: _jsx(AlertDialog.Portal, { container: target ?? undefined, children: _jsxs(Theme, { hasBackground: false, children: [_jsx(AlertDialog.Overlay, { className: "ui-sheet-scrim fixed inset-0 z-40 bg-scrim" }), body] }) }) })) : null] }));
|
|
42
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type ActionSheetTone } from './ActionSheet.js';
|
|
3
|
+
export interface ConfirmOptions {
|
|
4
|
+
title: ReactNode;
|
|
5
|
+
message?: ReactNode;
|
|
6
|
+
/** The accept button. Default: the `confirm` UI string. */
|
|
7
|
+
confirmLabel?: string;
|
|
8
|
+
/** The dismiss button. Default: the `cancel` UI string. */
|
|
9
|
+
cancelLabel?: string;
|
|
10
|
+
/** `danger` for destructive confirmations. Default `neutral`. */
|
|
11
|
+
tone?: ActionSheetTone;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Asks and resolves `true` on accept, `false` on Cancel, Escape or any other dismissal.
|
|
15
|
+
* Never rejects.
|
|
16
|
+
*
|
|
17
|
+
* if (await confirm({ title: 'Delete booking?', confirmLabel: 'Delete', tone: 'danger' })) …
|
|
18
|
+
*/
|
|
19
|
+
export declare function confirm(options: ConfirmOptions): Promise<boolean>;
|
|
20
|
+
/** Renders the pending confirmation. Mount once, inside `<Theme>`, next to `<Toaster>`. */
|
|
21
|
+
export declare function ConfirmProvider(): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useSyncExternalStore } from 'react';
|
|
3
|
+
import { useUiStrings } from '../../strings.js';
|
|
4
|
+
import { ActionSheet } from './ActionSheet.js';
|
|
5
|
+
/**
|
|
6
|
+
* A module-level queue, like `toast()`: `confirm()` can be called from any handler without
|
|
7
|
+
* threading a hook through. One `<ConfirmProvider>` per app; calls made while one is showing
|
|
8
|
+
* wait their turn rather than replacing it, so nobody's question is silently answered "no".
|
|
9
|
+
*/
|
|
10
|
+
let queue = [];
|
|
11
|
+
const listeners = new Set();
|
|
12
|
+
/** A fresh array per change, so `useSyncExternalStore` sees it. */
|
|
13
|
+
let snapshot;
|
|
14
|
+
function emit(next) {
|
|
15
|
+
queue = next;
|
|
16
|
+
snapshot = queue[0];
|
|
17
|
+
for (const l of listeners)
|
|
18
|
+
l();
|
|
19
|
+
}
|
|
20
|
+
function subscribe(l) {
|
|
21
|
+
listeners.add(l);
|
|
22
|
+
return () => listeners.delete(l);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Asks and resolves `true` on accept, `false` on Cancel, Escape or any other dismissal.
|
|
26
|
+
* Never rejects.
|
|
27
|
+
*
|
|
28
|
+
* if (await confirm({ title: 'Delete booking?', confirmLabel: 'Delete', tone: 'danger' })) …
|
|
29
|
+
*/
|
|
30
|
+
export function confirm(options) {
|
|
31
|
+
return new Promise((resolve) => emit([...queue, { ...options, resolve }]));
|
|
32
|
+
}
|
|
33
|
+
/** Renders the pending confirmation. Mount once, inside `<Theme>`, next to `<Toaster>`. */
|
|
34
|
+
export function ConfirmProvider() {
|
|
35
|
+
const current = useSyncExternalStore(subscribe, () => snapshot, () => undefined);
|
|
36
|
+
const strings = useUiStrings();
|
|
37
|
+
if (!current)
|
|
38
|
+
return null;
|
|
39
|
+
const settle = (ok) => {
|
|
40
|
+
if (queue[0] !== current)
|
|
41
|
+
return;
|
|
42
|
+
current.resolve(ok);
|
|
43
|
+
emit(queue.slice(1));
|
|
44
|
+
};
|
|
45
|
+
return (_jsx(ActionSheet, { open: true, onOpenChange: (open) => {
|
|
46
|
+
if (!open)
|
|
47
|
+
settle(false);
|
|
48
|
+
}, title: current.title, message: current.message, cancelLabel: current.cancelLabel, actions: [
|
|
49
|
+
{
|
|
50
|
+
label: current.confirmLabel ?? strings.confirm,
|
|
51
|
+
tone: current.tone,
|
|
52
|
+
onSelect: () => settle(true),
|
|
53
|
+
},
|
|
54
|
+
] }, queue.length === 0 ? 'none' : String(queueId(current))));
|
|
55
|
+
}
|
|
56
|
+
const ids = new WeakMap();
|
|
57
|
+
let nextId = 0;
|
|
58
|
+
function queueId(p) {
|
|
59
|
+
let id = ids.get(p);
|
|
60
|
+
if (id === undefined) {
|
|
61
|
+
id = ++nextId;
|
|
62
|
+
ids.set(p, id);
|
|
63
|
+
}
|
|
64
|
+
return id;
|
|
65
|
+
}
|
|
@@ -18,8 +18,8 @@ const VARIANTS = {
|
|
|
18
18
|
danger: { variant: 'solid', color: 'red' },
|
|
19
19
|
};
|
|
20
20
|
/** Themes sizes are 1–4; ours are the three a form actually needs. */
|
|
21
|
-
const SIZES = { sm: '2', md: '3', lg: '4' };
|
|
22
|
-
export const Button = forwardRef(function Button({ variant = 'primary', size = 'md', loading = false, fullWidth = false, asChild = false, href, className, disabled, children, ...props }, ref) {
|
|
21
|
+
const SIZES = { sm: '2', md: '3', lg: '4', touch: '3' };
|
|
22
|
+
export const Button = forwardRef(function Button({ variant = 'primary', size = 'md', loading = false, fullWidth = false, iconOnly = false, asChild = false, href, className, disabled, children, ...props }, ref) {
|
|
23
23
|
const intent = VARIANTS[variant];
|
|
24
24
|
// An anchor is `asChild` underneath, so everything below treats the two the same: neither a
|
|
25
25
|
// link nor a slotted child may be given `loading` or `disabled`, because those are button
|
|
@@ -29,6 +29,6 @@ export const Button = forwardRef(function Button({ variant = 'primary', size = '
|
|
|
29
29
|
// Themes renders its own spinner and keeps the label's width while it spins, so the
|
|
30
30
|
// button does not jump. It does not disable the button, which is why `disabled` still
|
|
31
31
|
// accounts for it.
|
|
32
|
-
loading: slotted ? undefined : loading, disabled: slotted ? undefined : disabled || loading, className: cn(fullWidth && 'w-full', className), ...props, children: href !== undefined ? _jsx("a", { href: href, children: children }) : children }));
|
|
32
|
+
loading: slotted ? undefined : loading, disabled: slotted ? undefined : disabled || loading, "data-variant": variant, "data-size": size, "data-icon-only": iconOnly || undefined, className: cn('ui-button', fullWidth && 'w-full', className), ...props, children: href !== undefined ? _jsx("a", { href: href, children: children }) : children }));
|
|
33
33
|
return themed;
|
|
34
34
|
});
|
|
@@ -12,7 +12,8 @@ import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
|
12
12
|
* panel. So `secondary` means exactly one thing everywhere and the other look has its own name.
|
|
13
13
|
*/
|
|
14
14
|
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
|
|
15
|
-
|
|
15
|
+
/** `touch` is `md` type with a 44px minimum height, for primary actions on touch screens. */
|
|
16
|
+
export type ButtonSize = 'sm' | 'md' | 'lg' | 'touch';
|
|
16
17
|
/**
|
|
17
18
|
* Derived from Themes' own props rather than from `ButtonHTMLAttributes`.
|
|
18
19
|
*
|
|
@@ -26,6 +27,11 @@ export interface ButtonProps extends Omit<ComponentPropsWithoutRef<typeof Themed
|
|
|
26
27
|
/** Renders a spinner and disables the button. */
|
|
27
28
|
loading?: boolean;
|
|
28
29
|
fullWidth?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The button holds only an icon. Its visual size is unchanged, but on coarse pointers the hit
|
|
32
|
+
* area grows to 44px with an invisible `::before`, so neighbours do not shrink.
|
|
33
|
+
*/
|
|
34
|
+
iconOnly?: boolean;
|
|
29
35
|
/** Renders the child element instead of a `button`, e.g. a router `Link`. */
|
|
30
36
|
asChild?: boolean;
|
|
31
37
|
/**
|
|
@@ -1,5 +1,19 @@
|
|
|
1
|
-
import type
|
|
2
|
-
export
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
export type CardVariant = 'outlined' | 'grouped' | 'plain';
|
|
3
|
+
export type CardDensity = 'comfortable' | 'compact';
|
|
4
|
+
/** Radix Themes' breakpoints, in the order they apply. `initial` is the unconditional value. */
|
|
5
|
+
export declare const CARD_BREAKPOINTS: readonly ["initial", "xs", "sm", "md", "lg", "xl"];
|
|
6
|
+
export type CardBreakpoint = (typeof CARD_BREAKPOINTS)[number];
|
|
7
|
+
/** A value, or one per breakpoint — Radix Themes' responsive prop convention. */
|
|
8
|
+
export type Responsive<T> = T | Partial<Record<CardBreakpoint, T>>;
|
|
9
|
+
interface CardStyleProps {
|
|
10
|
+
/** `outlined` is the bordered card; `grouped` a borderless rounded tile for tinted canvases;
|
|
11
|
+
* `plain` has no surface at all. Grouped and plain ignore `raised`. */
|
|
12
|
+
variant?: Responsive<CardVariant>;
|
|
13
|
+
/** `compact` tightens padding to the row spacing tokens and quietens the title. */
|
|
14
|
+
density?: Responsive<CardDensity>;
|
|
15
|
+
}
|
|
16
|
+
export interface CardProps extends HTMLAttributes<HTMLDivElement>, CardStyleProps {
|
|
3
17
|
/** Lifts the card off the ground — for dialogs and anything that floats. */
|
|
4
18
|
raised?: boolean;
|
|
5
19
|
}
|
|
@@ -8,9 +22,16 @@ export interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
8
22
|
*
|
|
9
23
|
* Themes pads its card for you; the house card is built from a header and a body with a
|
|
10
24
|
* rule between them, so the padding belongs to those rather than to the card. `p-0` is the
|
|
11
|
-
* whole of the customisation.
|
|
25
|
+
* whole of the customisation. Variant and density reach the header, title and body through
|
|
26
|
+
* context, and each can still override them.
|
|
12
27
|
*/
|
|
13
|
-
export declare function Card({ raised, className, ...props }: CardProps): import("react").JSX.Element;
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
export declare function
|
|
28
|
+
export declare function Card({ raised, variant, density, className, ...props }: CardProps): import("react").JSX.Element;
|
|
29
|
+
export interface CardPartProps extends HTMLAttributes<HTMLDivElement>, CardStyleProps {
|
|
30
|
+
}
|
|
31
|
+
export declare function CardHeader({ variant, density, className, ...props }: CardPartProps): import("react").JSX.Element;
|
|
32
|
+
export interface CardTitleProps extends HTMLAttributes<HTMLHeadingElement> {
|
|
33
|
+
density?: Responsive<CardDensity>;
|
|
34
|
+
}
|
|
35
|
+
export declare function CardTitle({ density, className, ...props }: CardTitleProps): import("react").JSX.Element;
|
|
36
|
+
export declare function CardBody({ variant, density, className, ...props }: CardPartProps): import("react").JSX.Element;
|
|
37
|
+
export {};
|