@sableui/react 0.13.2 → 0.15.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/AGENTS.md +2 -2
- package/CHANGELOG.md +172 -0
- package/CONTRIBUTING.md +6 -4
- package/DESIGN.md +10 -3
- package/dist/blocks/approval-block.js +93 -0
- package/dist/blocks/approval-block.meta.js +23 -0
- package/dist/blocks/index.d.ts +27 -1
- package/dist/blocks/index.js +8 -0
- package/dist/blocks/message-reactions-block.js +87 -0
- package/dist/blocks/message-reactions-block.meta.js +23 -0
- package/dist/components/accordion/accordion.meta.js +8 -2
- package/dist/components/area-chart/area-chart.meta.js +10 -3
- package/dist/components/attachment/attachment.meta.js +26 -0
- package/dist/components/bar-chart/bar-chart.meta.js +11 -4
- package/dist/components/bottom-nav/bottom-nav.js +3 -1
- package/dist/components/bottom-nav/bottom-nav.meta.js +32 -0
- package/dist/components/breadcrumbs/breadcrumbs.meta.js +26 -0
- package/dist/components/bubble/bubble.js +105 -5
- package/dist/components/bubble/bubble.meta.js +42 -6
- package/dist/components/bubble/bubble.variants.js +1 -1
- package/dist/components/bubble/index.js +2 -0
- package/dist/components/button/button.js +21 -4
- package/dist/components/button/button.meta.js +7 -7
- package/dist/components/calendar/calendar.js +18 -1
- package/dist/components/calendar/calendar.meta.js +38 -0
- package/dist/components/carousel/carousel.js +6 -4
- package/dist/components/carousel/carousel.meta.js +42 -0
- package/dist/components/chat-composer/chat-composer.js +21 -21
- package/dist/components/chat-composer/chat-composer.meta.js +38 -0
- package/dist/components/chat-composer/use-eased-growth.js +70 -0
- package/dist/components/checkbox/checkbox-mark.js +8 -1
- package/dist/components/checkbox/checkbox.js +3 -1
- package/dist/components/checkbox/checkbox.meta.js +32 -0
- package/dist/components/checkbox-card/checkbox-card.js +3 -0
- package/dist/components/checkbox-card/checkbox-card.meta.js +32 -0
- package/dist/components/code-block/code-block.js +14 -3
- package/dist/components/code-block/code-block.meta.js +28 -1
- package/dist/components/collapsible/collapsible.meta.js +32 -0
- package/dist/components/color-field/color-field.meta.js +26 -0
- package/dist/components/combobox/combobox.meta.js +44 -0
- package/dist/components/command-menu/command-menu.js +8 -3
- package/dist/components/command-menu/command-menu.meta.js +38 -0
- package/dist/components/confirm-button/confirm-button.js +179 -0
- package/dist/components/confirm-button/confirm-button.meta.js +143 -0
- package/dist/components/confirm-button/index.js +7 -0
- package/dist/components/data-table/data-table.js +6 -2
- package/dist/components/data-table/data-table.meta.js +32 -0
- package/dist/components/entity/entity.meta.js +26 -0
- package/dist/components/feedback/feedback.js +120 -47
- package/dist/components/feedback/feedback.meta.js +55 -4
- package/dist/components/input-group/input-group.meta.js +27 -1
- package/dist/components/like-button/index.js +7 -0
- package/dist/components/like-button/like-button.js +89 -0
- package/dist/components/like-button/like-button.meta.js +132 -0
- package/dist/components/line-chart/line-chart.meta.js +10 -3
- package/dist/components/middle-truncate/middle-truncate.meta.js +1 -1
- package/dist/components/note/note.meta.js +32 -0
- package/dist/components/notification-badge/notification-badge.js +17 -5
- package/dist/components/notification-badge/notification-badge.meta.js +34 -2
- package/dist/components/number-ticker/index.js +7 -0
- package/dist/components/number-ticker/number-ticker.js +246 -0
- package/dist/components/number-ticker/number-ticker.meta.js +159 -0
- package/dist/components/otp-field/otp-field.js +23 -3
- package/dist/components/otp-field/otp-field.meta.js +23 -2
- package/dist/components/panel-group/panel-group.meta.js +26 -0
- package/dist/components/panel-section/panel-section.js +37 -10
- package/dist/components/panel-section/panel-section.meta.js +32 -0
- package/dist/components/pie-chart/pie-chart.js +20 -11
- package/dist/components/pie-chart/pie-chart.meta.js +17 -4
- package/dist/components/pill/pill.meta.js +32 -0
- package/dist/components/radar-chart/radar-chart.js +4 -3
- package/dist/components/radar-chart/radar-chart.meta.js +10 -3
- package/dist/components/radial-chart/radial-chart.js +22 -22
- package/dist/components/radial-chart/radial-chart.meta.js +17 -4
- package/dist/components/radio/radio.meta.js +26 -0
- package/dist/components/radio-card/radio-card.meta.js +38 -0
- package/dist/components/reaction-picker/index.js +13 -0
- package/dist/components/reaction-picker/reaction-picker.js +104 -0
- package/dist/components/reaction-picker/reaction-picker.meta.js +156 -0
- package/dist/components/relative-time/relative-time.meta.js +32 -0
- package/dist/components/scroller/scroller.meta.js +32 -0
- package/dist/components/scrub-field/scrub-field.meta.js +32 -0
- package/dist/components/search-input/search-input.js +9 -7
- package/dist/components/search-input/search-input.meta.js +26 -0
- package/dist/components/show-more/show-more.js +1 -4
- package/dist/components/show-more/show-more.meta.js +38 -0
- package/dist/components/side-nav/side-nav.js +21 -1
- package/dist/components/side-nav/side-nav.meta.js +32 -0
- package/dist/components/side-nav/side-nav.variants.js +3 -1
- package/dist/components/sidebar/sidebar.js +31 -8
- package/dist/components/sidebar/sidebar.meta.js +38 -0
- package/dist/components/slider/slider.meta.js +26 -0
- package/dist/components/sparkline/sparkline.js +7 -4
- package/dist/components/sparkline/sparkline.meta.js +9 -2
- package/dist/components/table/table.meta.js +26 -0
- package/dist/components/tabs/tabs.js +27 -8
- package/dist/components/tabs/tabs.meta.js +46 -3
- package/dist/components/theme-selector/theme-selector.meta.js +32 -0
- package/dist/components/theme-switcher/theme-switcher.js +4 -1
- package/dist/components/theme-switcher/theme-switcher.meta.js +42 -0
- package/dist/components/toast/toast.js +25 -13
- package/dist/components/toast/toast.meta.js +7 -1
- package/dist/components/toggle/toggle.js +28 -5
- package/dist/components/toggle/toggle.meta.js +44 -2
- package/dist/components/tool-call/index.js +8 -0
- package/dist/components/tool-call/tool-call.js +117 -0
- package/dist/components/tool-call/tool-call.meta.js +147 -0
- package/dist/components/tool-call/tool-call.variants.js +33 -0
- package/dist/components/tooltip/index.js +3 -1
- package/dist/components/tooltip/tooltip.js +84 -0
- package/dist/components/tooltip/tooltip.meta.js +16 -1
- package/dist/components/tree/tree.js +71 -6
- package/dist/components/tree/tree.meta.js +32 -0
- package/dist/components/video/video.meta.js +32 -0
- package/dist/file-hashes.json +130 -104
- package/dist/foundations/motion.js +69 -3
- package/dist/icon-sets/reicon.js +11 -1
- package/dist/index.d.ts +907 -104
- package/dist/index.js +6 -1
- package/dist/lib/chart/cartesian.js +9 -10
- package/dist/lib/chart/chart-center.js +30 -0
- package/dist/lib/chart/chart-frame.js +15 -8
- package/dist/lib/chart/dim.js +13 -0
- package/dist/lib/chart/format.js +7 -1
- package/dist/lib/chart/motion.js +6 -4
- package/dist/lib/highlight-glide.js +68 -10
- package/dist/lib/taxonomy.js +5 -0
- package/dist/motion/Burst.js +79 -0
- package/dist/motion/MotionProvider.js +45 -3
- package/dist/motion/Reveal.js +1 -1
- package/dist/motion/TextReveal.js +1 -1
- package/dist/motion/index.js +17 -2
- package/dist/motion/useAutoSize.js +42 -0
- package/dist/motion/useShake.js +25 -0
- package/dist/styles.css +439 -5
- package/llms/blocks/approval-block.md +128 -0
- package/llms/blocks/message-reactions-block.md +116 -0
- package/llms/components/accordion.md +3 -2
- package/llms/components/area-chart.md +4 -3
- package/llms/components/attachment.md +7 -0
- package/llms/components/bar-chart.md +5 -4
- package/llms/components/bottom-nav.md +8 -0
- package/llms/components/breadcrumbs.md +7 -0
- package/llms/components/bubble.md +17 -7
- package/llms/components/button.md +6 -6
- package/llms/components/calendar.md +9 -0
- package/llms/components/carousel.md +17 -0
- package/llms/components/chat-composer.md +9 -0
- package/llms/components/checkbox-card.md +8 -0
- package/llms/components/checkbox.md +8 -0
- package/llms/components/code-block.md +8 -0
- package/llms/components/collapsible.md +8 -0
- package/llms/components/color-field.md +7 -0
- package/llms/components/combobox.md +10 -0
- package/llms/components/command-menu.md +9 -0
- package/llms/components/confirm-button.md +92 -0
- package/llms/components/data-table.md +8 -0
- package/llms/components/entity.md +7 -0
- package/llms/components/feedback.md +23 -3
- package/llms/components/input-group.md +8 -1
- package/llms/components/like-button.md +91 -0
- package/llms/components/line-chart.md +4 -3
- package/llms/components/middle-truncate.md +1 -1
- package/llms/components/note.md +8 -0
- package/llms/components/notification-badge.md +10 -2
- package/llms/components/number-ticker.md +86 -0
- package/llms/components/otp-field.md +15 -1
- package/llms/components/panel-group.md +7 -0
- package/llms/components/panel-section.md +8 -0
- package/llms/components/pie-chart.md +6 -4
- package/llms/components/pill.md +8 -0
- package/llms/components/radar-chart.md +4 -3
- package/llms/components/radial-chart.md +6 -4
- package/llms/components/radio-card.md +9 -0
- package/llms/components/radio.md +7 -0
- package/llms/components/reaction-picker.md +115 -0
- package/llms/components/relative-time.md +8 -0
- package/llms/components/scroller.md +8 -0
- package/llms/components/scrub-field.md +8 -0
- package/llms/components/search-input.md +7 -0
- package/llms/components/show-more.md +9 -0
- package/llms/components/side-nav.md +8 -0
- package/llms/components/sidebar.md +9 -0
- package/llms/components/slider.md +7 -0
- package/llms/components/sparkline.md +3 -2
- package/llms/components/table.md +7 -0
- package/llms/components/tabs.md +20 -2
- package/llms/components/theme-selector.md +8 -0
- package/llms/components/theme-switcher.md +17 -0
- package/llms/components/toast.md +2 -1
- package/llms/components/toggle.md +19 -2
- package/llms/components/tool-call.md +115 -0
- package/llms/components/tooltip.md +11 -1
- package/llms/components/tree.md +8 -0
- package/llms/components/video.md +8 -0
- package/llms/foundations/motion.md +64 -3
- package/llms/reference/utilities.md +26 -1
- package/llms/tasks/reviewing-motion.md +14 -0
- package/llms-full.txt +1337 -69
- package/llms.txt +19 -11
- package/package.json +1 -1
- package/registry.json +3040 -124
- package/targets/ios.json +45 -0
package/AGENTS.md
CHANGED
|
@@ -42,7 +42,7 @@ These are short on purpose. Each links to a foundations doc with the full vocabu
|
|
|
42
42
|
3. **Space on the 4px scale, by role.** Inset, stack, inline; related things closer than unrelated things. No off-scale `p-[15px]`. Padding, gaps and insets stay on the scale (a step, or `--spacing(n)` inside an arbitrary value) so the density control moves them; fixed `rem`/`px` only for what must not scale, like a hairline. Single-column content in a card is a flex column, not a grid. → `sable docs spacing`
|
|
43
43
|
4. **Controls read the ladder.** Heights come from `--control-*`, so `<Density>` can retune a region at once. Never `h-[40px]`. → `sable docs spacing`
|
|
44
44
|
5. **Shape and depth are roles.** Radii from the `rounded-*` ladder, concentric when nested (inner = outer − inset; `rounded-concentric-*`, `sable-frame-inner`); elevation by transience (resting → raised → overlay → modal → toast), one shadow per surface. → `sable docs shape` · `sable docs surfaces`
|
|
45
|
-
6. **Motion has a reason and a recipe.** Apply a named `motion-*` recipe; drop to `duration-(--motion-duration-*)` + `ease-*` tokens only when none fits. Never Tailwind's `animate-*`, never a raw `duration-150`. Exits are faster than entrances and never overshoot. → `sable docs motion`
|
|
45
|
+
6. **Motion has a reason and a recipe.** Apply a named `motion-*` recipe; drop to `duration-(--motion-duration-*)` + `ease-*` tokens only when none fits. Never Tailwind's `animate-*`, never a raw `duration-150`. Exits are faster than entrances and never overshoot. Components ship their craft (gliding indicators, rolling numbers, drawn ticks) on by default; to calm a dense view use `motion="subtle"` on the component or `<MotionScope level="subtle">` around a region, never a CSS override. → `sable docs motion`
|
|
46
46
|
7. **Icons by semantic name.** `<Icon name="check" />`; size with `text-{size}`, never type utilities. A mark attached to a line of type (dot, bullet, leading icon) sits in an `h-[1lh] items-center` box and is sized in em, never nudged with px. → `sable docs typography`
|
|
47
47
|
8. **Every scroll container declares a policy:** `scrollbar-quiet` (vertical) or `scrollbar-hidden` (horizontal, with an edge fade carrying the affordance).
|
|
48
48
|
9. **Accessible by construction.** WCAG AA contrast (and a label on a brand or status fill also clears APCA Lc 60 — `label-contrast`), 44px hit areas, visible focus, logical properties, live regions that exist before they speak. → `sable docs accessibility`
|
|
@@ -65,7 +65,7 @@ The machine-readable version of this section is the `consume` block in `registry
|
|
|
65
65
|
|
|
66
66
|
`styles.css` carries **Sable only** — the default theme, and the layout floor every other theme inherits (`--page-inset`, `--region-*`, `--rhythm-unit` on a bare `:root`). The bundled **alternates are opt-in**, because they are `[data-sable-theme]`-scoped and an app that never sets the attribute ships CSS that can never match: 5.5 KB gzip of permanently-dead weight, which is exactly how a real app's build audit found this. Using one? `@import "@sableui/react/theme/prism.css";`. Offering the whole set through a `<ThemeSelector>`? `@import "@sableui/react/themes.css";`. **Symptom of forgetting:** `data-sable-theme` is set, nothing matches, and the page silently renders as the Sable theme — no error, just the wrong theme.
|
|
67
67
|
4. **Fonts:** load Figtree (sans) + DM Mono (mono) — the theme declares the stacks, the host provides the files. Apply `antialiased` (`-webkit-font-smoothing: antialiased` / `-moz-osx-font-smoothing: grayscale`) **once on the app root**: Sable's type scale was drawn against smoothed text, and without it macOS renders every weight heavier than designed. The package deliberately doesn't set it — a library has no business styling the host's whole page — so it has to happen here.
|
|
68
|
-
5. **Providers:** `<IconProvider>` is **required** — wrap the app once in `<IconProvider set={reiconIconSet}>` with `import { reiconIconSet } from "@sableui/react/icons/reicon"` (without it every `<Icon>` renders nothing; development builds warn once). Pass another icon package's set instead to replace Reicon, or extend it using `composeIconSets(appExtras, reiconIconSet)` (list the added names in `.sable/icons.json` as `{ "names": [...] }` so `sable grade` accepts them); `<MotionProvider>` is optional too. Use `<SableThemeProvider>` + `useSableTheme()` when the app should expose user-selectable themes; pair `themeId` / `setThemeId` with `<ThemeSelector>` and `preference` / `setPreference` with `<ThemeSwitcher>`. The provider stores `light` / `dark` / `system`, toggles the `dark` class on `<html>`, stores the active design theme, and writes `data-sable-theme` for non-default themes. Apps with their own state layer can call `applySableColorMode()` / `applySableTheme()` instead. You only need a Tailwind `@custom-variant dark` if **your** app code uses `dark:` utilities (Sable ships none).
|
|
68
|
+
5. **Providers:** `<IconProvider>` is **required** — wrap the app once in `<IconProvider set={reiconIconSet}>` with `import { reiconIconSet } from "@sableui/react/icons/reicon"` (without it every `<Icon>` renders nothing; development builds warn once). Pass another icon package's set instead to replace Reicon, or extend it using `composeIconSets(appExtras, reiconIconSet)` (list the added names in `.sable/icons.json` as `{ "names": [...] }` so `sable grade` accepts them); `<MotionProvider>` is optional too (`policy="subtle"` keeps feedback and drops flourishes app-wide). Use `<SableThemeProvider>` + `useSableTheme()` when the app should expose user-selectable themes; pair `themeId` / `setThemeId` with `<ThemeSelector>` and `preference` / `setPreference` with `<ThemeSwitcher>`. The provider stores `light` / `dark` / `system`, toggles the `dark` class on `<html>`, stores the active design theme, and writes `data-sable-theme` for non-default themes. Apps with their own state layer can call `applySableColorMode()` / `applySableTheme()` instead. You only need a Tailwind `@custom-variant dark` if **your** app code uses `dark:` utilities (Sable ships none).
|
|
69
69
|
6. Import from the root barrel: `import { Button } from "@sableui/react"`. Page shells live on the `patterns` subpath (`import { SettingsPage } from "@sableui/react/patterns"`) — preview one, then copy its source from `registry.json → patterns` when the route needs to diverge. Blocks — section-sized compositions such as a sign-in card or a pricing card — live on the `blocks` subpath (`import { SignInBlock } from "@sableui/react/blocks"`) and ship the same way (`registry.json → blocks`); copy one and edit its content constants. A block's props are only `className`, `framed` and its primary action's callback, never content.
|
|
70
70
|
|
|
71
71
|
7. **Check the wiring:** `npx sable doctor`. Every check it runs corresponds to a way Sable fails *silently* — a second React (blank page, "invalid hook call"), a missing `@source` (Tailwind emits none of the package's classes, everything renders unstyled), or `data-sable-theme` set with no theme CSS imported (every theme renders as the Sable theme and the switcher looks broken). Exits 1 on any failure; `--json` for the structured report.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,177 @@
|
|
|
1
1
|
# What's new
|
|
2
2
|
|
|
3
|
+
## 0.15.0
|
|
4
|
+
|
|
5
|
+
Motion with some life in it: small things that turn on now land like events,
|
|
6
|
+
told through the theme rather than one-off styles, and chat reactions that
|
|
7
|
+
answer you the way a messaging app does.
|
|
8
|
+
|
|
9
|
+
- **New: `LikeButton`.** A pressed-or-not toggle for a like, a star or a
|
|
10
|
+
save, with its count. Pressing it on fills the glyph, which blooms past
|
|
11
|
+
its size and settles while a ring opens and dots spray out in the theme's
|
|
12
|
+
palette families, and the count rolls up a digit; pressing it off is quiet.
|
|
13
|
+
It shows the viewer's like at once and goes back to `count` exactly
|
|
14
|
+
whenever `count` changes, so pass the server's total. `icon`, `tone` (a
|
|
15
|
+
palette family) and `label` make it a star or a save; `motion="subtle"`
|
|
16
|
+
keeps the fill and drops the flourish.
|
|
17
|
+
- **New recipe: `motion-bloom`.** A small glyph landing in its on state grows
|
|
18
|
+
from nothing to 1.2 and settles. Remount the glyph (a changing `key`) to
|
|
19
|
+
replay it; `--motion-bloom-delay` lets a ring lead. Expressive.
|
|
20
|
+
- **New: `<Burst>`.** The follow-through behind a bloom — a disc that hollows
|
|
21
|
+
into a ring, then dots thrown outward — coloured by palette families, so it
|
|
22
|
+
re-themes. Plays when its `trigger` changes, never on mount, and renders
|
|
23
|
+
nothing below full motion.
|
|
24
|
+
- **Toast: a status glyph instead of an accent bar.** A typed toast now leads
|
|
25
|
+
with the same glyph and ink as Note and InlineStatus (success, info,
|
|
26
|
+
warning, and the destructive glyph for errors), and it blooms in once the
|
|
27
|
+
card has landed — and again in the spinner's place when a loading toast
|
|
28
|
+
settles. The coloured bar down the leading edge told the type by colour
|
|
29
|
+
alone. A stacked deck now springs as cards step back and fan out; leaving
|
|
30
|
+
toasts still take the standard curve.
|
|
31
|
+
- **Feedback:** the chosen rating mark blooms as it is picked.
|
|
32
|
+
- **Icons: `replay` and `motion`.** A single circular arrow for watching
|
|
33
|
+
something again from its start (`refresh` reads as fetching again), and
|
|
34
|
+
a play mark in a dashed ring for a motion or playback setting (`sparkles`
|
|
35
|
+
reads as AI).
|
|
36
|
+
- **New: `ReactionPicker` and `MenuReactions`.** The quick row of emoji you
|
|
37
|
+
react to a message with. `ReactionPicker` is a toolbar for a popover or a
|
|
38
|
+
hover bar; `MenuReactions` is the same row as checkbox menu items, for the
|
|
39
|
+
top of the menu a message opens on right click or long press. The emoji
|
|
40
|
+
bloom in one after another as it opens and magnify under the pointer.
|
|
41
|
+
- **Bubble reactions celebrate.** When a `BubbleReaction` becomes the
|
|
42
|
+
reader's own — pressed, or added already on to a row that has rendered —
|
|
43
|
+
the emoji blooms and copies of it spray out, and the count rolls. Taking
|
|
44
|
+
a reaction back is quiet, and the reactions a page loads with stay still.
|
|
45
|
+
New `label` names a pill ("Party, 3"); new `BubbleReactionAdd` is the
|
|
46
|
+
smiley pill that opens a picker. `BubbleReactions` now hides itself while
|
|
47
|
+
empty, so render it for every message: staying mounted is how the first
|
|
48
|
+
reaction still celebrates. A Bubble shows a focus ring when it is made
|
|
49
|
+
focusable as a menu trigger.
|
|
50
|
+
- **New block: `MessageReactionsBlock`.** A thread you can react to from a
|
|
51
|
+
right-click / long-press menu or the add-reaction pill.
|
|
52
|
+
- **`<Burst>`: `particle` and `playOnMount`.** Throw copies of a glyph
|
|
53
|
+
instead of dots (no ring, travelling upright), and play once on mount for
|
|
54
|
+
a glyph that exists because of the action.
|
|
55
|
+
- **Icon: `emoji`.** A smiley, for an add-reaction control.
|
|
56
|
+
- **Motion guidance:** amplitude scales with smallness (a glyph travels far, a
|
|
57
|
+
surface barely), a moment is staged (main object first, follow-through
|
|
58
|
+
after), and only a person's own choice is celebrated. In `sable docs
|
|
59
|
+
motion` and DESIGN.md.
|
|
60
|
+
|
|
61
|
+
## 0.14.0
|
|
62
|
+
|
|
63
|
+
Motion that is crafted by default and calm on request, and the first pieces for agent products.
|
|
64
|
+
|
|
65
|
+
- **New: two motion levels, and `subtle`.** Motion is now *essential*
|
|
66
|
+
(colour and focus feedback, press, overlay arrival, disclosure, loaders,
|
|
67
|
+
chart hover) or *expressive* (glides, rolling numbers, drawn ticks,
|
|
68
|
+
shakes, staggers, chart arrival and morph, scroll reveals, spring
|
|
69
|
+
overshoot). Expressive durations also multiply by `--motion-expressive`,
|
|
70
|
+
which the new `subtle` policy sets to 0, and `subtle` takes the overshoot
|
|
71
|
+
out of `ease-spring` and `ease-bounce`. So `full` (the default) plays both,
|
|
72
|
+
`subtle` keeps every answer and drops every flourish, and `off` stills
|
|
73
|
+
everything. Set it app-wide with `<MotionProvider policy="subtle">`, for a
|
|
74
|
+
region with the new `<MotionScope level="subtle">`, or per component with a
|
|
75
|
+
new `motion` prop. Scopes and props can only turn motion **down**: `full`
|
|
76
|
+
on a component means "inherit", so nothing can override a reader's
|
|
77
|
+
reduce-motion setting. `useMotion()` gains `expressive`, and
|
|
78
|
+
`useMotionLevel(motion)` resolves a component's prop against everything
|
|
79
|
+
around it. `Reveal` and `TextReveal` are expressive: under `subtle` the
|
|
80
|
+
content is simply there.
|
|
81
|
+
- **New recipes:** `motion-glide` (one indicator travelling to the selected
|
|
82
|
+
option), `motion-shake` (a refused answer, driven by the new `useShake()`),
|
|
83
|
+
`motion-tick` (a mark that draws itself in), `motion-appear` (a small
|
|
84
|
+
control mounting in place, through `@starting-style`), `motion-arrive`
|
|
85
|
+
(items a user action adds to a list, staggered) and `motion-size` (easing
|
|
86
|
+
between sizes, with the new `useAutoSize()` measuring a label change).
|
|
87
|
+
`sable utility <name>` has each one.
|
|
88
|
+
- **New: `NumberTicker`.** A number that rolls to its new value, digit by
|
|
89
|
+
digit, keyed by place value, so 999 → 1,000 rolls the columns it keeps and
|
|
90
|
+
grows the one it gains. It formats through `Intl.NumberFormat`, reads as
|
|
91
|
+
plain text to assistive tech and copy, and lands without a roll under
|
|
92
|
+
`subtle`. `NotificationBadge` counts and the `PieChart` / `RadialChart`
|
|
93
|
+
centre values now roll through it.
|
|
94
|
+
- **New: `ConfirmButton`.** An inline two-step for low-stakes destructive
|
|
95
|
+
actions. The first press arms it: the label changes, the button eases to
|
|
96
|
+
the new width, a thin line drains while it waits, and the armed state is
|
|
97
|
+
announced. A second press within `timeout` confirms; Escape, a click
|
|
98
|
+
elsewhere, blur or the timeout disarms it, and a double-click cannot
|
|
99
|
+
confirm. `DestructiveActionModal` stays the answer for high stakes.
|
|
100
|
+
- **New: `TooltipGroup`.** Wrap a row of `SimpleTooltip`s and they share one
|
|
101
|
+
tooltip: moving along a toolbar, it glides to the next button and its label
|
|
102
|
+
slides across instead of closing and reopening.
|
|
103
|
+
- **Selections travel.** `Tabs` renders a gliding indicator in both variants
|
|
104
|
+
(the underline and the pill), and a changed panel fades in. Single-select
|
|
105
|
+
`ToggleGroup appearance="segmented"` has one fill that glides and resizes
|
|
106
|
+
to the pressed option. `ThemeSwitcher` uses the same recipe, and
|
|
107
|
+
`Carousel`'s active dot eases its width. `SideNav` and `SidebarNav` draw
|
|
108
|
+
the current page as one fill that glides to the new item when the page
|
|
109
|
+
changes inside a persistent layout. Menu, Select and Combobox highlights
|
|
110
|
+
keep their glide, which is now expressive.
|
|
111
|
+
- **Calendar pages with direction.** Going to the next or previous month
|
|
112
|
+
slides the old grid out toward the edge it leaves by and the new one in
|
|
113
|
+
from the other, over a short travel with a fade (react-day-picker's
|
|
114
|
+
`animate` mode on Sable keyframes, mirrored in RTL). It is on at full
|
|
115
|
+
motion only (a `DatePicker`'s calendar follows the app or scope policy);
|
|
116
|
+
`Calendar` takes `motion`, and an explicit `animate` still wins.
|
|
117
|
+
- **Sidebar collapse eases.** Collapsing to the icon rail eases the width
|
|
118
|
+
instead of jumping 192px in one frame, so the content beside it visibly
|
|
119
|
+
takes the room. Icons stay on one column throughout and labels fade back
|
|
120
|
+
in on reopen. `Sidebar` takes `motion`; `subtle` snaps.
|
|
121
|
+
- **Focus outlines ease in.** `motion-interactive` now also transitions
|
|
122
|
+
`outline-color`, `outline-width` and `outline-offset`, so a keyboard focus
|
|
123
|
+
ring drawn with `outline-*` grows from the element's edge over the fast
|
|
124
|
+
step instead of appearing in one frame. Instant under reduced motion.
|
|
125
|
+
- **States finish their story.** `OtpField` shakes once when it turns
|
|
126
|
+
invalid, and its digits take the destructive colour. `CodeBlock`'s copy
|
|
127
|
+
button swaps to a check in place and announces "Copied". `Feedback` shows a
|
|
128
|
+
drawn check and a thank-you before it closes. A loading `Button` keeps its
|
|
129
|
+
width: the spinner lays over the label instead of replacing it. The
|
|
130
|
+
`ChatComposer` grows with an ease, and its send and stop swap in one
|
|
131
|
+
button. `SearchInput` and `CommandMenu`'s clear buttons appear rather than
|
|
132
|
+
pop. Expanding a `Tree` folder settles its rows in, staggered. The
|
|
133
|
+
`Checkbox` tick draws in from its leading stroke.
|
|
134
|
+
- **Fixed: disclosure chevrons snapped.** Tailwind v4's `rotate-*` writes the
|
|
135
|
+
standalone `rotate` property, which `motion-interactive` did not list, so
|
|
136
|
+
the chevrons on `Accordion`, `Collapsible`, `Tree` and `ShowMore` jumped
|
|
137
|
+
180° in one frame while the panel beside them eased. The recipe now lists
|
|
138
|
+
`rotate`. `DataTable`'s sort chevron rotates instead of swapping glyphs.
|
|
139
|
+
- **Chart hover dims mix toward the surface.** The bars, slices and rings
|
|
140
|
+
that are not under the pointer now step back by mixing toward the surface
|
|
141
|
+
colour instead of fading, so gridlines no longer strike through a dimmed
|
|
142
|
+
bar and a dimmed ring stays solid over its track.
|
|
143
|
+
- **Deprecated: `animate` on charts.** Every chart takes `motion` instead.
|
|
144
|
+
`animate={false}` maps to `motion="subtle"` (no arrival or morph, hover
|
|
145
|
+
feedback kept); use `motion="off"` to still everything.
|
|
146
|
+
- **Motion declaration is a build gate.** A component that reacts to the
|
|
147
|
+
pointer or carries state must declare its four motion slots (arrival,
|
|
148
|
+
response, transition, activity) in its meta, or decline each with a reason.
|
|
149
|
+
It used to be a report, and 36 components said nothing; all of them now
|
|
150
|
+
answer.
|
|
151
|
+
- **Docs:** a new **Interactions** page shows the catalog by task (inspect,
|
|
152
|
+
select, confirm, recover, succeed, finish loading, type, point, notify).
|
|
153
|
+
Every preview has its own motion level and Replay. Playgrounds gain a Motion
|
|
154
|
+
row and **Copy for agent**, which copies the exact JSX, imports, install
|
|
155
|
+
commands, the active theme and `sable component <Name>`. The Motion page
|
|
156
|
+
sets the policy to Full, Subtle or Off and shows the two levels side by
|
|
157
|
+
side.
|
|
158
|
+
|
|
159
|
+
- **New: `ToolCall`.** One tool an agent called, as a quiet row inside its
|
|
160
|
+
turn: a status glyph, what it did in plain words, the tool's name in mono,
|
|
161
|
+
and one word for where it is (`running`, `awaiting`, `complete`, `error`,
|
|
162
|
+
`denied`). Input and output sit behind a disclosure, as formatted JSON or
|
|
163
|
+
as a rendered result passed as `children`. A failure's message shows under
|
|
164
|
+
the row without opening anything. When a call settles, its spinner swaps
|
|
165
|
+
for the outcome in place (`motion-icon-swap`), and that's the only motion
|
|
166
|
+
in the row. The change is announced once, and a thread restored from
|
|
167
|
+
history announces nothing.
|
|
168
|
+
- **New: `ApprovalBlock`**, in a new "AI & agents" block family. An agent
|
|
169
|
+
asking permission for one call: the question in plain words, what can't be
|
|
170
|
+
undone, the call's details, an "allow for the rest of this chat" Checkbox,
|
|
171
|
+
and Deny / Allow. Neither button is focused on arrival. After a decision,
|
|
172
|
+
one status line replaces the buttons, focus moves to it, and it's
|
|
173
|
+
announced. `onApprove({ remember })` and `onDeny()` report the answer.
|
|
174
|
+
|
|
3
175
|
## 0.13.2
|
|
4
176
|
|
|
5
177
|
What to use instead, wherever a component is the wrong pick.
|
package/CONTRIBUTING.md
CHANGED
|
@@ -168,7 +168,9 @@ artwork, and nothing in the core graph imports `src/icon-sets/`.
|
|
|
168
168
|
|
|
169
169
|
## Motion
|
|
170
170
|
|
|
171
|
-
Motion is a first-class layer — **
|
|
171
|
+
Motion is a first-class layer — **crafted by default, calm on request, zero runtime dependency** (CSS tokens/recipes + tiny browser-native JS). When a component has any interaction, wire it to the system; don't leave it static or hand-roll a transition. When its selection, value or mark changes in place, move the one element that represents it (`motion-glide`, `NumberTicker`, `motion-tick`, `motion-icon-swap`) rather than switching two things on and off.
|
|
172
|
+
|
|
173
|
+
**Two levels.** Essential motion (feedback, press, overlay arrival, disclosure, loaders) runs on the plain tokens. Expressive motion (glides, rolling numbers, drawn ticks, shakes, staggers, arrival choreography, overshoot) multiplies its durations by `var(--motion-expressive, 1)` as well — the shipped expressive recipes already do — and JS-driven flourishes gate on `useMotionLevel(motion).expressive`. A component that owns a flourish takes `motion?: MotionLevel` (`full` | `subtle` | `off`) and spreads `data-motion={motionAttribute(motion)}` on its root; the prop only turns motion down.
|
|
172
174
|
|
|
173
175
|
**Four slots.** Every component answers all four — by filling the slot, or by declining it in `meta.motion` with `recipe: "none"` and a reason:
|
|
174
176
|
|
|
@@ -179,13 +181,13 @@ Motion is a first-class layer — **subtle by default, tasteful spring welcome,
|
|
|
179
181
|
| `transition` | how does it move between its own states? | `motion-disclosure`, or ease the transform |
|
|
180
182
|
| `activity` | how do you tell "working" from "frozen"? | `motion-spin` · `motion-pulse` · `motion-shimmer` |
|
|
181
183
|
|
|
182
|
-
This paragraph said "don't leave it static" for many releases while **nothing checked it**. The four motion rules below are all prohibitive — they fire on motion that is present and wrong, and none can fire on motion that is absent — so a component that snapped between every state passed the whole contract, and motion stayed a thing a human had to request by name. Two gates now close that: `static-interactive` (lint, fires on an element styled for pointer states with no recipe on it) and `meta.motion`
|
|
184
|
+
This paragraph said "don't leave it static" for many releases while **nothing checked it**. The four motion rules below are all prohibitive — they fire on motion that is present and wrong, and none can fire on motion that is absent — so a component that snapped between every state passed the whole contract, and motion stayed a thing a human had to request by name. Two gates now close that: `static-interactive` (lint, fires on an element styled for pointer states with no recipe on it) and `meta.motion` — `build-registry` **fails** when a component that reacts to the pointer or carries state declares no `motion` array (`pnpm build:registry -- --motion-report` prints the slot-by-slot coverage).
|
|
183
185
|
|
|
184
|
-
1. **Reach for a recipe first** — apply one named class instead of a bespoke transition string: `motion-interactive` (default hover/focus/press chrome — **replaces** `transition-colors`), `motion-press` (tactile press; always pair with `motion-interactive`), `motion-fade`/`motion-pop`/`motion-slide-{up,down,left,right}` (overlay enter/exit, on Base UI `data-[starting-style]`/`data-[ending-style]`), `motion-disclosure` (height), and loaders `motion-{spin,shimmer,pulse,dot-bounce,ping}`.
|
|
186
|
+
1. **Reach for a recipe first** — apply one named class instead of a bespoke transition string: `motion-interactive` (default hover/focus/press chrome — **replaces** `transition-colors`), `motion-press` (tactile press; always pair with `motion-interactive`), `motion-fade`/`motion-pop`/`motion-slide-{up,down,left,right}` (overlay enter/exit, on Base UI `data-[starting-style]`/`data-[ending-style]`), `motion-disclosure` (height), `motion-icon-swap` (a glyph that changes with state), the expressive `motion-{glide,shake,tick,appear,arrive,size}`, and loaders `motion-{spin,shimmer,pulse,dot-bounce,ping}`.
|
|
185
187
|
2. **Drop to tokens only when a recipe doesn't fit.** Durations via `duration-(--motion-duration-{instant|fast|base|slow|slower})` — **never** a raw `duration-150` (it bypasses the kill-switch). Easings via `ease-{standard|decelerate|emphasized|spring|bounce}`. Spring/bounce overshoot is **enter-only**; exits stay clean.
|
|
186
188
|
3. **Never** use Tailwind's built-in `animate-spin`/`animate-pulse`/`animate-ping`/`animate-bounce` — they ignore `--motion-scale`, so the on/off policy can't freeze them. Use the scale-aware `motion-*` loader recipes.
|
|
187
189
|
4. **Compositor-first & subtle:** animate opacity + transform (scale/translate) only; short durations; no exit overshoot; no gaudy motion.
|
|
188
|
-
5. **Policy:** all token-driven motion collapses to static under `prefers-reduced-motion` or `<MotionProvider>` off (one `--motion-scale: 0`). JS primitives read `useMotion().enabled` and must no-op when motion is off
|
|
190
|
+
5. **Policy:** all token-driven motion collapses to static under `prefers-reduced-motion` or `<MotionProvider>` off (one `--motion-scale: 0`); `subtle` zeroes `--motion-expressive` only. JS primitives read `useMotion().enabled` and must no-op when motion is off; JS flourishes read `expressive`.
|
|
189
191
|
6. **JS motion lives in `src/motion/`** (zero-dep, browser-native: WAAPI / IntersectionObserver / View Transitions). Do **not** add the `motion` library to core — drag/gesture, FLIP layout, and velocity springs are the future optional `@sableui/motion` adapter (peer-dep `motion`).
|
|
190
192
|
|
|
191
193
|
Full token table + rationale: `DESIGN.md → Motion`. Live demo: docs → Foundations → Motion (also the on/off toggle).
|
package/DESIGN.md
CHANGED
|
@@ -477,7 +477,13 @@ Reference via `z-(--z-overlay)`, never a raw `z-[9999]`. Base UI overlays portal
|
|
|
477
477
|
|
|
478
478
|
## Motion
|
|
479
479
|
|
|
480
|
-
Motion is a first-class token layer, not an afterthought. **
|
|
480
|
+
Motion is a first-class token layer, not an afterthought. **Crafted by default, calm on request** — quick, deliberate, skippable, and never gaudy. Sable components ship the small continuity that makes an interface feel made rather than assembled: one indicator travels to the selected tab instead of two fills blinking, a total rolls to its new value, a check draws itself in, a wrong code shakes and clears. Built compositor-first (opacity + transform, with height and an absolutely positioned glider's size as the sanctioned exceptions).
|
|
481
|
+
|
|
482
|
+
**The principle behind the craft: one thing travels; two things do not blink.** When a selection, a value or a mark changes in place, move the element that represents it. And finish the story — a refused answer shakes *and* says why, a success confirms where it happened, an armed action asks again before it acts.
|
|
483
|
+
|
|
484
|
+
**Amplitude scales with smallness.** Motion felt lifeless when one amplitude served everything: the 4% a dialog needs is invisible on a 16px icon, so a heart turning on read as a colour swap. Small glyphs that a person turns on now travel far — grow from nothing past their size and settle — while surfaces keep their 0.96 → 1 and presses their 0.97. A moment is staged rather than flashed: the main object lands, then its follow-through (LikeButton's ring opens, the heart blooms 90ms later, dots are thrown after it). And only a person's own choice is celebrated, only at full motion: a burst on a system event or on load is a notification wearing confetti. The look of all of it still comes from the theme — palette families, tone glyphs, the radius scale — never from one-off values.
|
|
485
|
+
|
|
486
|
+
**Two levels.** *Essential* motion answers the user — colour and focus feedback, press, overlay arrival, disclosure height, loaders, chart hover. *Expressive* motion narrates — glides, rolling numbers, drawn ticks, shakes, staggers, chart arrival and morph, scroll reveals, spring overshoot. Expressive durations are multiplied by a second factor, `--motion-expressive`, which `data-motion="subtle"` sets to 0 (and which takes the overshoot out of `ease-spring`). So the policy ladder is `full` (default: both) → `subtle` (essential only) → `off` (nothing moves; every state still changes). Set it app-wide on `<MotionProvider policy>`, for a region with `<MotionScope level>`, or per component with the `motion` prop that every component owning a flourish takes. Scopes and props can only turn motion **down** — `full` on a component means "inherit", so nothing below the provider can override a reader's reduce-motion setting.
|
|
481
487
|
|
|
482
488
|
**Three parts:**
|
|
483
489
|
|
|
@@ -487,9 +493,10 @@ Motion is a first-class token layer, not an afterthought. **Subtle by default, w
|
|
|
487
493
|
- Consume easings via the `ease-*` utilities; durations via `duration-(--motion-duration-*)` (so the kill-switch always applies).
|
|
488
494
|
|
|
489
495
|
2. **Recipes** (`styles.css`) — named, reusable behaviours a component opts into with one class instead of a bespoke transition string:
|
|
490
|
-
- `motion-interactive` (hover/focus colour + transform transitions) · `motion-press` (tactile press settle) · `motion-fade` (scrims) · `motion-pop` (anchored/centred surfaces — gentle spring on open) · `motion-slide-{up,down,left,right}` (directional fade-slide) · `motion-disclosure` (height) · `motion-draw` (SVG line-art draw-on — a `pathLength="1"` path inside a `<Reveal>` draws in when scrolled into view; stagger siblings via `--motion-draw-delay`) · loaders `motion-spin`/`motion-pulse`/`motion-shimmer`/`motion-dot-bounce`. Enter/exit recipes drive off Base UI's `data-[starting-style]`/`data-[ending-style]`.
|
|
496
|
+
- `motion-interactive` (hover/focus colour + transform transitions, including `rotate`) · `motion-press` (tactile press settle) · `motion-fade` (scrims) · `motion-pop` (anchored/centred surfaces — gentle spring on open) · `motion-slide-{up,down,left,right}` (directional fade-slide) · `motion-disclosure` (height) · `motion-draw` (SVG line-art draw-on — a `pathLength="1"` path inside a `<Reveal>` draws in when scrolled into view; stagger siblings via `--motion-draw-delay`) · loaders `motion-spin`/`motion-pulse`/`motion-shimmer`/`motion-dot-bounce`. Enter/exit recipes drive off Base UI's `data-[starting-style]`/`data-[ending-style]`.
|
|
497
|
+
- Expressive: `motion-glide` (one indicator travelling to the selection) · `motion-shake` (a refused answer, driven by `useShake()`) · `motion-tick` (a mark that draws itself in) · `motion-appear` (a small control mounting via `@starting-style`) · `motion-arrive` (items a user action adds to a list, staggered) · `motion-size` (easing between intrinsic sizes via `interpolate-size`) · `motion-bloom` (a small glyph landing in its on state, 0 → 1.2 → 1). `NumberTicker` is the rolling-number primitive; `<Burst>` is the follow-through behind a bloom (a ring and thrown dots in the theme's palette families), used by `LikeButton`.
|
|
491
498
|
|
|
492
|
-
3. **Policy** — `prefers-reduced-motion` and `<MotionProvider>` collapse `--motion-scale` to 0 in one place: transitions resolve instantly, loaders hold their first frame.
|
|
499
|
+
3. **Policy** — `prefers-reduced-motion` and `<MotionProvider>` collapse `--motion-scale` to 0 in one place: transitions resolve instantly, loaders hold their first frame. `subtle` zeroes `--motion-expressive` instead, so essential motion keeps playing. In-app, `useMotion()` reads and sets the policy; `useMotionLevel(motion)` resolves a component's own prop against everything around it, and JS-driven flourishes gate on its `expressive`.
|
|
493
500
|
|
|
494
501
|
Richer physics (interruptible velocity-springs, FLIP layout, drag/gesture) is intentionally **out of core** — it's the future optional `@sableui/motion` adapter (peer-dep `motion`), reached for only when CSS genuinely can't express a surface.
|
|
495
502
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// src/blocks/approval-block.tsx
|
|
2
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
3
|
+
import { Button } from "../components/button/index.js";
|
|
4
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../components/card/index.js";
|
|
5
|
+
import { Checkbox } from "../components/checkbox/index.js";
|
|
6
|
+
import { Code } from "../components/code/index.js";
|
|
7
|
+
import { InlineStatus } from "../components/inline-status/index.js";
|
|
8
|
+
import { Label } from "../components/label/index.js";
|
|
9
|
+
import { cn } from "../lib/cn.js";
|
|
10
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
var request = {
|
|
12
|
+
tool: "send_email",
|
|
13
|
+
title: "Send the invoice to Ada?",
|
|
14
|
+
description: "The agent wants to email invoice INV-1042. An email can't be unsent.",
|
|
15
|
+
details: [
|
|
16
|
+
{ label: "To", value: "ada@example.com" },
|
|
17
|
+
{ label: "Subject", value: "INV-1042 for October" },
|
|
18
|
+
{ label: "Attached", value: "INV-1042.pdf" }
|
|
19
|
+
],
|
|
20
|
+
allowed: "Allowed. Sending the email.",
|
|
21
|
+
denied: "Denied. The agent won't send it."
|
|
22
|
+
};
|
|
23
|
+
function ApprovalBlock({ className, framed = true, onApprove, onDeny }) {
|
|
24
|
+
const [decision, setDecision] = useState(null);
|
|
25
|
+
const [remember, setRemember] = useState(false);
|
|
26
|
+
const id = useId();
|
|
27
|
+
const outcome = useRef(null);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (decision) outcome.current?.focus();
|
|
30
|
+
}, [decision]);
|
|
31
|
+
return /* @__PURE__ */ jsxs(Card, { variant: framed ? "default" : "flat", className: cn("w-full max-w-md", className), children: [
|
|
32
|
+
/* @__PURE__ */ jsxs(CardHeader, { children: [
|
|
33
|
+
/* @__PURE__ */ jsx(CardTitle, { children: request.title }),
|
|
34
|
+
/* @__PURE__ */ jsx(CardDescription, { children: request.description })
|
|
35
|
+
] }),
|
|
36
|
+
/* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col gap-4", children: [
|
|
37
|
+
/* @__PURE__ */ jsxs("dl", { className: "grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 text-body-sm", children: [
|
|
38
|
+
request.details.map((d) => /* @__PURE__ */ jsxs("div", { className: "contents", children: [
|
|
39
|
+
/* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: d.label }),
|
|
40
|
+
/* @__PURE__ */ jsx("dd", { className: "truncate text-foreground", children: d.value })
|
|
41
|
+
] }, d.label)),
|
|
42
|
+
/* @__PURE__ */ jsxs("div", { className: "contents", children: [
|
|
43
|
+
/* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "Tool" }),
|
|
44
|
+
/* @__PURE__ */ jsx("dd", { className: "min-w-0", children: /* @__PURE__ */ jsx(Code, { children: request.tool }) })
|
|
45
|
+
] })
|
|
46
|
+
] }),
|
|
47
|
+
decision ? null : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
48
|
+
/* @__PURE__ */ jsx(Checkbox, { id: `${id}-remember`, checked: remember, onCheckedChange: (v) => setRemember(v) }),
|
|
49
|
+
/* @__PURE__ */ jsxs(Label, { htmlFor: `${id}-remember`, className: "text-body-sm", children: [
|
|
50
|
+
"Allow ",
|
|
51
|
+
/* @__PURE__ */ jsx(Code, { children: request.tool }),
|
|
52
|
+
" for the rest of this chat"
|
|
53
|
+
] })
|
|
54
|
+
] })
|
|
55
|
+
] }),
|
|
56
|
+
/* @__PURE__ */ jsx(CardFooter, { actions: true, className: "justify-end", children: decision ? /* @__PURE__ */ jsx(
|
|
57
|
+
InlineStatus,
|
|
58
|
+
{
|
|
59
|
+
ref: outcome,
|
|
60
|
+
tabIndex: -1,
|
|
61
|
+
role: "status",
|
|
62
|
+
tone: decision === "allowed" ? "success" : "neutral",
|
|
63
|
+
className: "me-auto outline-none",
|
|
64
|
+
children: decision === "allowed" ? request.allowed : request.denied
|
|
65
|
+
}
|
|
66
|
+
) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
67
|
+
/* @__PURE__ */ jsx(
|
|
68
|
+
Button,
|
|
69
|
+
{
|
|
70
|
+
variant: "outline",
|
|
71
|
+
onClick: () => {
|
|
72
|
+
setDecision("denied");
|
|
73
|
+
onDeny?.();
|
|
74
|
+
},
|
|
75
|
+
children: "Deny"
|
|
76
|
+
}
|
|
77
|
+
),
|
|
78
|
+
/* @__PURE__ */ jsx(
|
|
79
|
+
Button,
|
|
80
|
+
{
|
|
81
|
+
onClick: () => {
|
|
82
|
+
setDecision("allowed");
|
|
83
|
+
onApprove?.({ remember });
|
|
84
|
+
},
|
|
85
|
+
children: "Allow"
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
] }) })
|
|
89
|
+
] });
|
|
90
|
+
}
|
|
91
|
+
export {
|
|
92
|
+
ApprovalBlock
|
|
93
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/blocks/approval-block.meta.ts
|
|
2
|
+
var approvalBlockMeta = {
|
|
3
|
+
name: "ApprovalBlock",
|
|
4
|
+
family: "AI & agents",
|
|
5
|
+
kind: "card",
|
|
6
|
+
width: { min: "18rem", max: "28rem" },
|
|
7
|
+
summary: `An agent asking permission for one tool call: the question, what will happen and what can't be undone, the call's details, an "allow for the rest of this chat" Checkbox, and Deny / Allow.`,
|
|
8
|
+
intent: 'Reach for this in a chat or agent run when a tool call waits on the person \u2014 sending, paying, deleting, publishing. The call itself, before and after the decision, is a ToolCall with status="awaiting", then "complete" or "denied". A destructive action outside an agent is DestructiveActionModal.',
|
|
9
|
+
notes: [
|
|
10
|
+
`The title is the question in plain words ("Send the invoice to Ada?"), not the tool's name; the tool's name is one row of the details, for the person who wants to know exactly what runs.`,
|
|
11
|
+
"The description says what can't be undone. That sentence is what the person is deciding on.",
|
|
12
|
+
"Deny comes first and Allow is the primary on the end, the same order as every Sable confirmation. Neither is focused on arrival: an approval that a stray Enter can grant is not an approval.",
|
|
13
|
+
"After a decision the buttons give way to one InlineStatus line saying what happened, so the thread keeps a record of the answer."
|
|
14
|
+
],
|
|
15
|
+
a11y: [
|
|
16
|
+
"Focus moves to the outcome line when the buttons leave, instead of dropping to the page; the line is a role=status region, so the decision is announced.",
|
|
17
|
+
"The details are a dl, so each value is read with its label.",
|
|
18
|
+
"The remember Checkbox has its own Label, so the tool name inside it is part of its accessible name."
|
|
19
|
+
]
|
|
20
|
+
};
|
|
21
|
+
export {
|
|
22
|
+
approvalBlockMeta
|
|
23
|
+
};
|
package/dist/blocks/index.d.ts
CHANGED
|
@@ -13,6 +13,21 @@ declare function ApiKeysBlock({ className, framed, onCreate, onKeyAction }: ApiK
|
|
|
13
13
|
|
|
14
14
|
declare const apiKeysBlockMeta: BlockMeta;
|
|
15
15
|
|
|
16
|
+
interface ApprovalBlockProps {
|
|
17
|
+
className?: string;
|
|
18
|
+
/** false drops the card's surface and border — for a chat column that already frames it. */
|
|
19
|
+
framed?: boolean;
|
|
20
|
+
/** The person allowed the call. `remember` is the "for the rest of this chat" choice. */
|
|
21
|
+
onApprove?: (options: {
|
|
22
|
+
remember: boolean;
|
|
23
|
+
}) => void;
|
|
24
|
+
/** The person declined the call. */
|
|
25
|
+
onDeny?: () => void;
|
|
26
|
+
}
|
|
27
|
+
declare function ApprovalBlock({ className, framed, onApprove, onDeny }: ApprovalBlockProps): react.JSX.Element;
|
|
28
|
+
|
|
29
|
+
declare const approvalBlockMeta: BlockMeta;
|
|
30
|
+
|
|
16
31
|
interface ChatThreadBlockProps {
|
|
17
32
|
className?: string;
|
|
18
33
|
/** false drops the card's surface and border. Keep it framed on a muted page: incoming bubbles are the muted surface too. */
|
|
@@ -155,6 +170,17 @@ declare function MediaPlayerBlock({ className, framed, onPlayingChange, onPrevio
|
|
|
155
170
|
|
|
156
171
|
declare const mediaPlayerBlockMeta: BlockMeta;
|
|
157
172
|
|
|
173
|
+
interface MessageReactionsBlockProps {
|
|
174
|
+
className?: string;
|
|
175
|
+
/** false drops the card's surface and border. Keep it framed on a muted page: incoming bubbles are the muted surface too. */
|
|
176
|
+
framed?: boolean;
|
|
177
|
+
/** Called when the reader adds or removes a reaction, with the message, the emoji and whether it is now on. */
|
|
178
|
+
onReact?: (messageId: string, emoji: string, on: boolean) => void;
|
|
179
|
+
}
|
|
180
|
+
declare function MessageReactionsBlock({ className, framed, onReact }: MessageReactionsBlockProps): react.JSX.Element;
|
|
181
|
+
|
|
182
|
+
declare const messageReactionsBlockMeta: BlockMeta;
|
|
183
|
+
|
|
158
184
|
interface NewsletterBlockProps {
|
|
159
185
|
className?: string;
|
|
160
186
|
/** false drops the card's surface and border — for a footer or muted section that already frames it. */
|
|
@@ -393,4 +419,4 @@ declare function WalletBlock({ className, framed, onSend, onReceive }: WalletBlo
|
|
|
393
419
|
|
|
394
420
|
declare const walletBlockMeta: BlockMeta;
|
|
395
421
|
|
|
396
|
-
export { ApiKeysBlock, ChatThreadBlock, CommandPaletteBlock, CtaBandBlock, DangerZoneBlock, DeviceComparisonBlock, EventCardBlock, FaqBlock, FeatureGridBlock, FeedbackFormBlock, GoalProgressBlock, HeroBlock, InviteTeamBlock, InvoiceHistoryBlock, KpiSparklineBlock, MediaPlayerBlock, NewsletterBlock, NotFoundBlock, NotificationListBlock, NotificationSettingsBlock, OrderTrackingBlock, PricingCardBlock, PricingTableBlock, ProductCardBlock, ProfileCardBlock, ProfileFormBlock, RevenueBreakdownBlock, SignInBlock, SignUpBlock, SiteFooterBlock, StatRowBlock, StepsBlock, TaskListBlock, TeamMembersBlock, TestimonialBlock, TrafficSourcesBlock, TrafficTrendBlock, UploadListBlock, VerifyCodeBlock, WalletBlock, apiKeysBlockMeta, chatThreadBlockMeta, commandPaletteBlockMeta, ctaBandBlockMeta, dangerZoneBlockMeta, deviceComparisonBlockMeta, eventCardBlockMeta, faqBlockMeta, featureGridBlockMeta, feedbackFormBlockMeta, goalProgressBlockMeta, heroBlockMeta, inviteTeamBlockMeta, invoiceHistoryBlockMeta, kpiSparklineBlockMeta, mediaPlayerBlockMeta, newsletterBlockMeta, notFoundBlockMeta, notificationListBlockMeta, notificationSettingsBlockMeta, orderTrackingBlockMeta, pricingCardBlockMeta, pricingTableBlockMeta, productCardBlockMeta, profileCardBlockMeta, profileFormBlockMeta, revenueBreakdownBlockMeta, signInBlockMeta, signUpBlockMeta, siteFooterBlockMeta, statRowBlockMeta, stepsBlockMeta, taskListBlockMeta, teamMembersBlockMeta, testimonialBlockMeta, trafficSourcesBlockMeta, trafficTrendBlockMeta, uploadListBlockMeta, verifyCodeBlockMeta, walletBlockMeta };
|
|
422
|
+
export { ApiKeysBlock, ApprovalBlock, ChatThreadBlock, CommandPaletteBlock, CtaBandBlock, DangerZoneBlock, DeviceComparisonBlock, EventCardBlock, FaqBlock, FeatureGridBlock, FeedbackFormBlock, GoalProgressBlock, HeroBlock, InviteTeamBlock, InvoiceHistoryBlock, KpiSparklineBlock, MediaPlayerBlock, MessageReactionsBlock, NewsletterBlock, NotFoundBlock, NotificationListBlock, NotificationSettingsBlock, OrderTrackingBlock, PricingCardBlock, PricingTableBlock, ProductCardBlock, ProfileCardBlock, ProfileFormBlock, RevenueBreakdownBlock, SignInBlock, SignUpBlock, SiteFooterBlock, StatRowBlock, StepsBlock, TaskListBlock, TeamMembersBlock, TestimonialBlock, TrafficSourcesBlock, TrafficTrendBlock, UploadListBlock, VerifyCodeBlock, WalletBlock, apiKeysBlockMeta, approvalBlockMeta, chatThreadBlockMeta, commandPaletteBlockMeta, ctaBandBlockMeta, dangerZoneBlockMeta, deviceComparisonBlockMeta, eventCardBlockMeta, faqBlockMeta, featureGridBlockMeta, feedbackFormBlockMeta, goalProgressBlockMeta, heroBlockMeta, inviteTeamBlockMeta, invoiceHistoryBlockMeta, kpiSparklineBlockMeta, mediaPlayerBlockMeta, messageReactionsBlockMeta, newsletterBlockMeta, notFoundBlockMeta, notificationListBlockMeta, notificationSettingsBlockMeta, orderTrackingBlockMeta, pricingCardBlockMeta, pricingTableBlockMeta, productCardBlockMeta, profileCardBlockMeta, profileFormBlockMeta, revenueBreakdownBlockMeta, signInBlockMeta, signUpBlockMeta, siteFooterBlockMeta, statRowBlockMeta, stepsBlockMeta, taskListBlockMeta, teamMembersBlockMeta, testimonialBlockMeta, trafficSourcesBlockMeta, trafficTrendBlockMeta, uploadListBlockMeta, verifyCodeBlockMeta, walletBlockMeta };
|
package/dist/blocks/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// src/blocks/index.ts
|
|
2
2
|
import { ApiKeysBlock } from "./api-keys-block.js";
|
|
3
3
|
import { apiKeysBlockMeta } from "./api-keys-block.meta.js";
|
|
4
|
+
import { ApprovalBlock } from "./approval-block.js";
|
|
5
|
+
import { approvalBlockMeta } from "./approval-block.meta.js";
|
|
4
6
|
import { ChatThreadBlock } from "./chat-thread-block.js";
|
|
5
7
|
import { chatThreadBlockMeta } from "./chat-thread-block.meta.js";
|
|
6
8
|
import { CommandPaletteBlock } from "./command-palette-block.js";
|
|
@@ -31,6 +33,8 @@ import { KpiSparklineBlock } from "./kpi-sparkline-block.js";
|
|
|
31
33
|
import { kpiSparklineBlockMeta } from "./kpi-sparkline-block.meta.js";
|
|
32
34
|
import { MediaPlayerBlock } from "./media-player-block.js";
|
|
33
35
|
import { mediaPlayerBlockMeta } from "./media-player-block.meta.js";
|
|
36
|
+
import { MessageReactionsBlock } from "./message-reactions-block.js";
|
|
37
|
+
import { messageReactionsBlockMeta } from "./message-reactions-block.meta.js";
|
|
34
38
|
import { NewsletterBlock } from "./newsletter-block.js";
|
|
35
39
|
import { newsletterBlockMeta } from "./newsletter-block.meta.js";
|
|
36
40
|
import { NotFoundBlock } from "./not-found-block.js";
|
|
@@ -81,6 +85,7 @@ import { WalletBlock } from "./wallet-block.js";
|
|
|
81
85
|
import { walletBlockMeta } from "./wallet-block.meta.js";
|
|
82
86
|
export {
|
|
83
87
|
ApiKeysBlock,
|
|
88
|
+
ApprovalBlock,
|
|
84
89
|
ChatThreadBlock,
|
|
85
90
|
CommandPaletteBlock,
|
|
86
91
|
CtaBandBlock,
|
|
@@ -96,6 +101,7 @@ export {
|
|
|
96
101
|
InvoiceHistoryBlock,
|
|
97
102
|
KpiSparklineBlock,
|
|
98
103
|
MediaPlayerBlock,
|
|
104
|
+
MessageReactionsBlock,
|
|
99
105
|
NewsletterBlock,
|
|
100
106
|
NotFoundBlock,
|
|
101
107
|
NotificationListBlock,
|
|
@@ -121,6 +127,7 @@ export {
|
|
|
121
127
|
VerifyCodeBlock,
|
|
122
128
|
WalletBlock,
|
|
123
129
|
apiKeysBlockMeta,
|
|
130
|
+
approvalBlockMeta,
|
|
124
131
|
chatThreadBlockMeta,
|
|
125
132
|
commandPaletteBlockMeta,
|
|
126
133
|
ctaBandBlockMeta,
|
|
@@ -136,6 +143,7 @@ export {
|
|
|
136
143
|
invoiceHistoryBlockMeta,
|
|
137
144
|
kpiSparklineBlockMeta,
|
|
138
145
|
mediaPlayerBlockMeta,
|
|
146
|
+
messageReactionsBlockMeta,
|
|
139
147
|
newsletterBlockMeta,
|
|
140
148
|
notFoundBlockMeta,
|
|
141
149
|
notificationListBlockMeta,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/blocks/message-reactions-block.tsx
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { Bubble, BubbleReaction, BubbleReactionAdd, BubbleReactions } from "../components/bubble/index.js";
|
|
6
|
+
import { Card, CardContent, CardFooter } from "../components/card/index.js";
|
|
7
|
+
import {
|
|
8
|
+
ContextMenu,
|
|
9
|
+
ContextMenuContent,
|
|
10
|
+
ContextMenuItem,
|
|
11
|
+
ContextMenuSeparator,
|
|
12
|
+
ContextMenuTrigger
|
|
13
|
+
} from "../components/context-menu/index.js";
|
|
14
|
+
import { Message } from "../components/message/index.js";
|
|
15
|
+
import { DEFAULT_REACTIONS, MenuReactions } from "../components/reaction-picker/index.js";
|
|
16
|
+
import { cn } from "../lib/cn.js";
|
|
17
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
18
|
+
var messages = [
|
|
19
|
+
{ id: "m1", text: "The new onboarding flow is live on staging \u{1F680}", mine: false, reactions: { "\u{1F389}": 2, "\u{1F440}": 1 } },
|
|
20
|
+
{ id: "m2", text: "Clicked through it twice. The progress bar is so smooth.", mine: true, reactions: {} },
|
|
21
|
+
{ id: "m3", text: "Ship it Friday?", mine: false, reactions: { "\u{1F44D}": 1 } }
|
|
22
|
+
];
|
|
23
|
+
var hint = "Right-click or long-press a message to react.";
|
|
24
|
+
var labels = {
|
|
25
|
+
...Object.fromEntries(DEFAULT_REACTIONS.map((r) => [r.emoji, r.label])),
|
|
26
|
+
"\u{1F440}": "Eyes"
|
|
27
|
+
};
|
|
28
|
+
function MessageReactionsBlock({ className, framed = true, onReact }) {
|
|
29
|
+
const [mine, setMine] = useState({ m1: ["\u{1F389}"] });
|
|
30
|
+
const toggle = (messageId, emoji) => {
|
|
31
|
+
const on = !(mine[messageId] ?? []).includes(emoji);
|
|
32
|
+
setMine((prev) => {
|
|
33
|
+
const current = prev[messageId] ?? [];
|
|
34
|
+
const next = current.includes(emoji) ? current.filter((e) => e !== emoji) : [...current, emoji];
|
|
35
|
+
return { ...prev, [messageId]: next };
|
|
36
|
+
});
|
|
37
|
+
onReact?.(messageId, emoji, on);
|
|
38
|
+
};
|
|
39
|
+
return /* @__PURE__ */ jsxs(Card, { variant: framed ? "default" : "flat", className: cn("w-full max-w-sm", className), children: [
|
|
40
|
+
/* @__PURE__ */ jsx(CardContent, { className: "flex flex-col gap-3", children: messages.map((m) => {
|
|
41
|
+
const align = m.mine ? "end" : "start";
|
|
42
|
+
const chosen = mine[m.id] ?? [];
|
|
43
|
+
const counts = { ...m.reactions };
|
|
44
|
+
for (const emoji of chosen) counts[emoji] = counts[emoji] ?? 0;
|
|
45
|
+
const pills = Object.entries(counts).map(([emoji, others]) => ({
|
|
46
|
+
emoji,
|
|
47
|
+
active: chosen.includes(emoji),
|
|
48
|
+
count: others + (chosen.includes(emoji) ? 1 : 0)
|
|
49
|
+
}));
|
|
50
|
+
return /* @__PURE__ */ jsxs(Message, { messageId: m.id, align: m.mine ? "end" : void 0, children: [
|
|
51
|
+
/* @__PURE__ */ jsxs(ContextMenu, { children: [
|
|
52
|
+
/* @__PURE__ */ jsx(
|
|
53
|
+
ContextMenuTrigger,
|
|
54
|
+
{
|
|
55
|
+
render: /* @__PURE__ */ jsx(Bubble, { tabIndex: 0, variant: m.mine ? "primary" : void 0, align }),
|
|
56
|
+
children: m.text
|
|
57
|
+
}
|
|
58
|
+
),
|
|
59
|
+
/* @__PURE__ */ jsxs(ContextMenuContent, { children: [
|
|
60
|
+
/* @__PURE__ */ jsx(MenuReactions, { selected: chosen, onSelect: (emoji) => toggle(m.id, emoji) }),
|
|
61
|
+
/* @__PURE__ */ jsx(ContextMenuSeparator, {}),
|
|
62
|
+
/* @__PURE__ */ jsx(ContextMenuItem, { children: "Reply" }),
|
|
63
|
+
/* @__PURE__ */ jsx(ContextMenuItem, { children: "Copy text" })
|
|
64
|
+
] })
|
|
65
|
+
] }),
|
|
66
|
+
/* @__PURE__ */ jsxs(BubbleReactions, { align, children: [
|
|
67
|
+
pills.map((p) => /* @__PURE__ */ jsx(
|
|
68
|
+
BubbleReaction,
|
|
69
|
+
{
|
|
70
|
+
emoji: p.emoji,
|
|
71
|
+
label: labels[p.emoji],
|
|
72
|
+
count: p.count,
|
|
73
|
+
active: p.active,
|
|
74
|
+
onClick: () => toggle(m.id, p.emoji)
|
|
75
|
+
},
|
|
76
|
+
p.emoji
|
|
77
|
+
)),
|
|
78
|
+
pills.length > 0 ? /* @__PURE__ */ jsx(BubbleReactionAdd, { selected: chosen, onSelect: (emoji) => toggle(m.id, emoji) }) : null
|
|
79
|
+
] })
|
|
80
|
+
] }, m.id);
|
|
81
|
+
}) }),
|
|
82
|
+
/* @__PURE__ */ jsx(CardFooter, { className: "text-caption text-muted-foreground", children: hint })
|
|
83
|
+
] });
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
MessageReactionsBlock
|
|
87
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/blocks/message-reactions-block.meta.ts
|
|
2
|
+
var messageReactionsBlockMeta = {
|
|
3
|
+
name: "MessageReactionsBlock",
|
|
4
|
+
family: "Social & messaging",
|
|
5
|
+
kind: "card",
|
|
6
|
+
width: { min: "18rem", max: "24rem" },
|
|
7
|
+
summary: "A short thread you can react to: right-click or long-press a bubble for a menu with a row of quick reactions above Reply and Copy, or use the add-reaction pill under a message. A new reaction blooms in and sprays copies of its emoji.",
|
|
8
|
+
intent: "Reach for this when a conversation excerpt should be reactable \u2014 a team thread, a comment card, a support transcript. A read-only excerpt is ChatThreadBlock; a working chat with a composer and history is the ChatPanel page shell.",
|
|
9
|
+
notes: [
|
|
10
|
+
"Two ways in, on purpose: the right-click / long-press menu is fast once you know it is there, and the add-reaction pill is how a first-time reader finds reactions at all.",
|
|
11
|
+
"The quick row in the menu is MenuReactions (checkbox menu items) and the one in the pill's popover is ReactionPicker (a toolbar), so each is reachable by the keyboard model of the surface it sits in.",
|
|
12
|
+
"Only the reader's own reaction celebrates. Counts from other people roll quietly, and the reactions a page loads with never play.",
|
|
13
|
+
"The add-reaction pill appears only on messages that already have reactions; a message with none is reacted to from its menu, which keeps quiet rows quiet."
|
|
14
|
+
],
|
|
15
|
+
a11y: [
|
|
16
|
+
"Each bubble is focusable, and its menu opens on right click, long press, Shift+F10 or the Menu key; its reactions are checkbox items reporting whether the reader has used each one.",
|
|
17
|
+
'Each reaction pill is a toggle named by its meaning and count ("Party, 3") with `aria-pressed`; the emoji and the burst are hidden from assistive tech.',
|
|
18
|
+
"Under reduced motion the reaction appears and the count changes at once; nothing else is lost."
|
|
19
|
+
]
|
|
20
|
+
};
|
|
21
|
+
export {
|
|
22
|
+
messageReactionsBlockMeta
|
|
23
|
+
};
|