@sableui/react 0.14.0 → 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/CHANGELOG.md +58 -0
- package/DESIGN.md +3 -1
- package/dist/blocks/index.d.ts +12 -1
- package/dist/blocks/index.js +4 -0
- package/dist/blocks/message-reactions-block.js +87 -0
- package/dist/blocks/message-reactions-block.meta.js +23 -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/feedback/feedback.js +8 -1
- package/dist/components/feedback/feedback.meta.js +6 -0
- 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/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/toast/toast.js +25 -13
- package/dist/components/toast/toast.meta.js +7 -1
- package/dist/file-hashes.json +26 -17
- package/dist/foundations/motion.js +21 -1
- package/dist/icon-sets/reicon.js +11 -1
- package/dist/index.d.ts +295 -62
- package/dist/index.js +3 -1
- package/dist/lib/taxonomy.js +2 -0
- package/dist/motion/Burst.js +79 -0
- package/dist/motion/index.js +2 -0
- package/dist/styles.css +141 -0
- package/llms/blocks/message-reactions-block.md +116 -0
- package/llms/components/bubble.md +17 -7
- package/llms/components/feedback.md +1 -0
- package/llms/components/like-button.md +91 -0
- package/llms/components/reaction-picker.md +115 -0
- package/llms/components/toast.md +2 -1
- package/llms/foundations/motion.md +18 -2
- package/llms/reference/utilities.md +4 -1
- package/llms-full.txt +374 -12
- package/llms.txt +9 -6
- package/package.json +1 -1
- package/registry.json +573 -12
- package/targets/ios.json +18 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// src/components/like-button/like-button.meta.ts
|
|
2
|
+
var likeButtonMeta = {
|
|
3
|
+
name: "LikeButton",
|
|
4
|
+
summary: "A pressed-or-not toggle for a like, a star or a save, with its count. Pressing it on fills the glyph, which blooms while a ring and a spray of dots burst from it and the count rolls up; pressing it off is quiet.",
|
|
5
|
+
status: "stable",
|
|
6
|
+
keywords: [
|
|
7
|
+
"like",
|
|
8
|
+
"heart",
|
|
9
|
+
"favorite",
|
|
10
|
+
"favourite",
|
|
11
|
+
"love",
|
|
12
|
+
"star",
|
|
13
|
+
"bookmark",
|
|
14
|
+
"save",
|
|
15
|
+
"upvote",
|
|
16
|
+
"reaction",
|
|
17
|
+
"count",
|
|
18
|
+
"toggle"
|
|
19
|
+
],
|
|
20
|
+
usage: {
|
|
21
|
+
description: "Use LikeButton for a personal, reversible endorsement that other people can see the tally of: liking a post, starring a repository, saving a recipe. It is the one place in Sable that celebrates, because the person chose it and it costs nothing to undo.",
|
|
22
|
+
bestPractices: [
|
|
23
|
+
{
|
|
24
|
+
guidance: "do",
|
|
25
|
+
description: "Pass the server's total as `count` and update it when the save lands. The button shows the viewer's own like at once, before the save, and goes back to `count` exactly whenever it changes, so a failed save that leaves `count` alone can also turn `pressed` back off."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
guidance: "do",
|
|
29
|
+
description: 'Match the glyph, family and label to the meaning: `icon="star" tone="yellow" label="Star"` for a star. The label is the accessible name, so it must say what pressing does.'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
guidance: "do",
|
|
33
|
+
description: 'Use `format={{ notation: "compact" }}` once counts pass a few thousand, so a feed of buttons keeps one width.'
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
guidance: "dont",
|
|
37
|
+
description: "Do not use it for a setting or a view switch (bold, mute, grid or list). Those are tools, not endorsements, and a burst on every press of a formatting control is noise; use Toggle.",
|
|
38
|
+
instead: ["Toggle"]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
guidance: "dont",
|
|
42
|
+
description: "Do not use it for a rating or a verdict on an answer (helpful, not helpful). That is a question with two sides; use Feedback.",
|
|
43
|
+
instead: ["Feedback"]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
guidance: "dont",
|
|
47
|
+
description: "Do not press it for the user. A burst is a reply to a person's choice \u2014 liking something on their behalf, or on load, turns delight into a notification."
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
related: {
|
|
52
|
+
components: ["Toggle", "Feedback", "NumberTicker", "Button"]
|
|
53
|
+
},
|
|
54
|
+
props: [
|
|
55
|
+
{
|
|
56
|
+
name: "count",
|
|
57
|
+
type: "number",
|
|
58
|
+
description: "The total, rolled when it changes. Pressing adds or removes the viewer's like at once; the button shows `count` exactly again whenever `count` changes. Omit it for a bare glyph (an icon-size button)."
|
|
59
|
+
},
|
|
60
|
+
{ name: "pressed", type: "boolean", description: "Controlled pressed state." },
|
|
61
|
+
{ name: "defaultPressed", type: "boolean", default: "false", description: "Initial pressed state when uncontrolled." },
|
|
62
|
+
{
|
|
63
|
+
name: "onPressedChange",
|
|
64
|
+
type: "(pressed: boolean, event) => void",
|
|
65
|
+
description: "Called when the user presses it, with the new state."
|
|
66
|
+
},
|
|
67
|
+
{ name: "icon", type: "string", default: '"heart"', description: "The glyph, by icon name. Its `solid` variant is shown while pressed." },
|
|
68
|
+
{
|
|
69
|
+
name: "tone",
|
|
70
|
+
type: "PaletteColor",
|
|
71
|
+
default: '"red"',
|
|
72
|
+
description: "The palette family the glyph fills with while pressed, and the burst blooms from. A family, so it follows the theme."
|
|
73
|
+
},
|
|
74
|
+
{ name: "label", type: "string", default: '"Like"', description: "The accessible name, read before the count." },
|
|
75
|
+
{ name: "format", type: "Intl.NumberFormatOptions", description: 'How to write the count, e.g. `{ notation: "compact" }`.' },
|
|
76
|
+
{ name: "variant", type: '"ghost" | "outline" | "secondary"', default: '"ghost"', description: "The Button surface." },
|
|
77
|
+
{ name: "size", type: '"xs" | "sm" | "md"', default: '"sm"', description: "A Button size; without a count, the matching icon size." },
|
|
78
|
+
{ name: "disabled", type: "boolean", default: "false", description: "Disables it." },
|
|
79
|
+
{
|
|
80
|
+
name: "motion",
|
|
81
|
+
type: '"full" | "subtle" | "off"',
|
|
82
|
+
default: '"full"',
|
|
83
|
+
description: "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down \u2014 it never overrides a reader's reduce-motion setting. The bloom and the burst are the flourish; the fill and the count changing stay."
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
variants: {
|
|
87
|
+
variant: ["ghost", "outline", "secondary"],
|
|
88
|
+
size: ["xs", "sm", "md"],
|
|
89
|
+
tone: ["red", "orange", "yellow", "green", "teal", "blue", "purple", "pink"]
|
|
90
|
+
},
|
|
91
|
+
tokensUsed: ["palette-dot", "accent", "ring"],
|
|
92
|
+
motion: [
|
|
93
|
+
{
|
|
94
|
+
slot: "arrival",
|
|
95
|
+
recipe: "none",
|
|
96
|
+
part: "Root",
|
|
97
|
+
why: "It arrives with the post or row it belongs to. A page that loads with likes already given must be still: nothing blooms on mount."
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
slot: "response",
|
|
101
|
+
recipe: "motion-bloom",
|
|
102
|
+
part: "Glyph",
|
|
103
|
+
why: "Pressing it on is the moment. The glyph is 16px, so it needs real amplitude to read as an event: it grows from nothing past its size and settles, after a 90ms beat in which the ring leads. Inherits Button's motion-press for the press itself. Pressing off has no bloom \u2014 taking a like back is not a celebration."
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
slot: "transition",
|
|
107
|
+
recipe: "custom",
|
|
108
|
+
part: "Burst",
|
|
109
|
+
why: "The follow-through: <Burst> draws a disc that hollows into a ring and shifts colour, then dots thrown outward in the theme's palette families. Expressive only \u2014 under `subtle` or reduced motion it renders nothing and the fill alone answers the press. The count rolls with NumberTicker."
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
slot: "activity",
|
|
113
|
+
recipe: "none",
|
|
114
|
+
part: "Root",
|
|
115
|
+
why: "There is no in-between state to show: the like is shown at once, before it is saved."
|
|
116
|
+
}
|
|
117
|
+
],
|
|
118
|
+
a11y: [
|
|
119
|
+
"A native <button> through Button, with `aria-pressed`. Enter and Space press it.",
|
|
120
|
+
`The accessible name is the label then the count ("Like 1,301"); the burst is aria-hidden and the rolling digits are hidden behind the count's real text.`,
|
|
121
|
+
"The pressed state is carried by the filled glyph and `aria-pressed`, never by colour alone.",
|
|
122
|
+
"Under reduced motion the glyph fills and the count changes at once; the bloom and the burst are dropped."
|
|
123
|
+
],
|
|
124
|
+
examples: [
|
|
125
|
+
{ label: "A post's likes", code: "<LikeButton count={post.likes} defaultPressed={post.liked} onPressedChange={(on) => api.like(post.id, on)} />" },
|
|
126
|
+
{ label: "Star a repository", code: '<LikeButton icon="star" tone="yellow" label="Star" variant="outline" count={repo.stars} format={{ notation: "compact" }} />' },
|
|
127
|
+
{ label: "Bare glyph", code: '<LikeButton label="Save recipe" />' }
|
|
128
|
+
]
|
|
129
|
+
};
|
|
130
|
+
export {
|
|
131
|
+
likeButtonMeta
|
|
132
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/components/reaction-picker/index.ts
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_REACTIONS,
|
|
4
|
+
MenuReactions,
|
|
5
|
+
ReactionPicker
|
|
6
|
+
} from "./reaction-picker.js";
|
|
7
|
+
import { reactionPickerMeta } from "./reaction-picker.meta.js";
|
|
8
|
+
export {
|
|
9
|
+
DEFAULT_REACTIONS,
|
|
10
|
+
MenuReactions,
|
|
11
|
+
ReactionPicker,
|
|
12
|
+
reactionPickerMeta
|
|
13
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/components/reaction-picker/reaction-picker.tsx
|
|
4
|
+
import { Menu as BaseMenu } from "@base-ui/react/menu";
|
|
5
|
+
import { cn } from "../../lib/cn.js";
|
|
6
|
+
import { motionAttribute } from "../../motion/MotionProvider.js";
|
|
7
|
+
import { Toolbar, ToolbarButton } from "../toolbar/toolbar.js";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
var DEFAULT_REACTIONS = [
|
|
10
|
+
{ emoji: "\u{1F44D}", label: "Thumbs up" },
|
|
11
|
+
{ emoji: "\u2764\uFE0F", label: "Heart" },
|
|
12
|
+
{ emoji: "\u{1F602}", label: "Laughing" },
|
|
13
|
+
{ emoji: "\u{1F389}", label: "Party" },
|
|
14
|
+
{ emoji: "\u{1F62E}", label: "Surprised" },
|
|
15
|
+
{ emoji: "\u{1F622}", label: "Sad" },
|
|
16
|
+
{ emoji: "\u{1F64F}", label: "Thank you" }
|
|
17
|
+
];
|
|
18
|
+
var PRESSED = "aria-pressed:bg-primary/10 data-[checked]:bg-primary/10";
|
|
19
|
+
var MENU_CELL = `group/reaction grid size-(--control-md) shrink-0 cursor-pointer select-none place-items-center rounded-full outline-none sable-surface-interactive motion-interactive motion-press ${PRESSED}`;
|
|
20
|
+
function Glyph({ emoji, index }) {
|
|
21
|
+
return /* @__PURE__ */ jsx(
|
|
22
|
+
"span",
|
|
23
|
+
{
|
|
24
|
+
"aria-hidden": true,
|
|
25
|
+
className: "motion-bloom",
|
|
26
|
+
style: { "--motion-bloom-delay": `${index * 24}ms` },
|
|
27
|
+
children: /* @__PURE__ */ jsx("span", { className: "block text-[calc(var(--control-md)*0.6)] leading-none transition-[scale] duration-(--motion-duration-fast) ease-standard group-hover/reaction:scale-125 group-focus-visible/reaction:scale-125 group-data-[highlighted]/reaction:scale-125", children: emoji })
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
function ReactionPicker({
|
|
32
|
+
reactions = DEFAULT_REACTIONS,
|
|
33
|
+
selected = [],
|
|
34
|
+
onSelect,
|
|
35
|
+
motion,
|
|
36
|
+
className,
|
|
37
|
+
children,
|
|
38
|
+
"aria-label": ariaLabel = "Reactions",
|
|
39
|
+
...props
|
|
40
|
+
}) {
|
|
41
|
+
return /* @__PURE__ */ jsxs(
|
|
42
|
+
Toolbar,
|
|
43
|
+
{
|
|
44
|
+
"aria-label": ariaLabel,
|
|
45
|
+
"data-motion": motionAttribute(motion),
|
|
46
|
+
className: cn("gap-0.5", className),
|
|
47
|
+
...props,
|
|
48
|
+
children: [
|
|
49
|
+
reactions.map((reaction, index) => /* @__PURE__ */ jsx(
|
|
50
|
+
ToolbarButton,
|
|
51
|
+
{
|
|
52
|
+
size: "icon",
|
|
53
|
+
"aria-label": reaction.label,
|
|
54
|
+
"aria-pressed": selected.includes(reaction.emoji),
|
|
55
|
+
onClick: () => onSelect?.(reaction.emoji),
|
|
56
|
+
className: cn("group/reaction rounded-full", PRESSED),
|
|
57
|
+
children: /* @__PURE__ */ jsx(Glyph, { emoji: reaction.emoji, index })
|
|
58
|
+
},
|
|
59
|
+
reaction.emoji
|
|
60
|
+
)),
|
|
61
|
+
children
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
ReactionPicker.displayName = "ReactionPicker";
|
|
67
|
+
function MenuReactions({
|
|
68
|
+
reactions = DEFAULT_REACTIONS,
|
|
69
|
+
selected = [],
|
|
70
|
+
onSelect,
|
|
71
|
+
motion,
|
|
72
|
+
className,
|
|
73
|
+
"aria-label": ariaLabel = "Reactions",
|
|
74
|
+
...props
|
|
75
|
+
}) {
|
|
76
|
+
return /* @__PURE__ */ jsx(
|
|
77
|
+
BaseMenu.Group,
|
|
78
|
+
{
|
|
79
|
+
"aria-label": ariaLabel,
|
|
80
|
+
"data-motion": motionAttribute(motion),
|
|
81
|
+
className: cn("flex items-center gap-0.5", className),
|
|
82
|
+
...props,
|
|
83
|
+
children: reactions.map((reaction, index) => /* @__PURE__ */ jsx(
|
|
84
|
+
BaseMenu.CheckboxItem,
|
|
85
|
+
{
|
|
86
|
+
label: reaction.label,
|
|
87
|
+
"aria-label": reaction.label,
|
|
88
|
+
checked: selected.includes(reaction.emoji),
|
|
89
|
+
onCheckedChange: () => onSelect?.(reaction.emoji),
|
|
90
|
+
closeOnClick: true,
|
|
91
|
+
className: MENU_CELL,
|
|
92
|
+
children: /* @__PURE__ */ jsx(Glyph, { emoji: reaction.emoji, index })
|
|
93
|
+
},
|
|
94
|
+
reaction.emoji
|
|
95
|
+
))
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
MenuReactions.displayName = "MenuReactions";
|
|
100
|
+
export {
|
|
101
|
+
DEFAULT_REACTIONS,
|
|
102
|
+
MenuReactions,
|
|
103
|
+
ReactionPicker
|
|
104
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// src/components/reaction-picker/reaction-picker.meta.ts
|
|
2
|
+
var reactionPickerMeta = {
|
|
3
|
+
name: "ReactionPicker",
|
|
4
|
+
summary: "The quick row of emoji you react to a message with: a toolbar of large emoji toggles that bloom in one after another as it opens and magnify under the pointer, with MenuReactions as the same row inside a right-click menu.",
|
|
5
|
+
status: "stable",
|
|
6
|
+
keywords: [
|
|
7
|
+
"reaction",
|
|
8
|
+
"reactions",
|
|
9
|
+
"emoji",
|
|
10
|
+
"emoji picker",
|
|
11
|
+
"react",
|
|
12
|
+
"tapback",
|
|
13
|
+
"quick reactions",
|
|
14
|
+
"message actions",
|
|
15
|
+
"chat"
|
|
16
|
+
],
|
|
17
|
+
usage: {
|
|
18
|
+
description: "Use ReactionPicker wherever a reader chooses a reaction for something: in a Popover from an add-reaction control (BubbleReactionAdd is that, ready-made), or as a hover bar over a message. Use MenuReactions for the same row as the first group of the menu a message opens on right-click or long-press, above Reply and Copy. Both report which emoji the reader has already used, so choosing one again removes it.",
|
|
19
|
+
bestPractices: [
|
|
20
|
+
{
|
|
21
|
+
guidance: "do",
|
|
22
|
+
description: 'Keep the quick set short \u2014 six to eight. It is the reactions people actually use; a full emoji catalogue belongs behind a trailing "more" control passed as children.'
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
guidance: "do",
|
|
26
|
+
description: "Give every reaction a `label`. It is the accessible name and the menu's typeahead, and an emoji's own spoken name differs between platforms."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
guidance: "do",
|
|
30
|
+
description: "Pass `selected` so the reader's own reactions show pressed, and toggle in `onSelect`: choosing a reaction you already gave takes it back."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
guidance: "do",
|
|
34
|
+
description: "Offer the right-click menu and a visible add-reaction control together. A context menu is invisible until you know it is there; the pill is how a first-time reader finds reactions."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
guidance: "dont",
|
|
38
|
+
description: "Do not put a ReactionPicker inside a Menu or ContextMenu. A toolbar is not a menu item, so the menu's arrow keys would skip it; use MenuReactions there.",
|
|
39
|
+
instead: ["MenuReactions"]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
guidance: "dont",
|
|
43
|
+
description: "Do not use it to rate an answer or collect a verdict. Reactions are social and many-valued; a helpful / not-helpful question is Feedback.",
|
|
44
|
+
instead: ["Feedback"]
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
anatomy: [
|
|
48
|
+
{
|
|
49
|
+
name: "ReactionPicker",
|
|
50
|
+
required: true,
|
|
51
|
+
description: "The toolbar: one tab stop, arrow keys move between emoji, each a toggle with `aria-pressed`."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "MenuReactions",
|
|
55
|
+
required: false,
|
|
56
|
+
description: "The same row as a group of checkbox menu items, for MenuContent and ContextMenuContent. Choosing one closes the menu."
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
related: {
|
|
61
|
+
components: ["Bubble", "ContextMenu", "Popover", "Toolbar", "Feedback"]
|
|
62
|
+
},
|
|
63
|
+
props: [
|
|
64
|
+
{
|
|
65
|
+
name: "reactions",
|
|
66
|
+
type: "readonly { emoji: string; label: string }[]",
|
|
67
|
+
default: "DEFAULT_REACTIONS",
|
|
68
|
+
description: "The choices, in order. The default is \u{1F44D} \u2764\uFE0F \u{1F602} \u{1F389} \u{1F62E} \u{1F622} \u{1F64F}."
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "selected",
|
|
72
|
+
type: "readonly string[]",
|
|
73
|
+
default: "[]",
|
|
74
|
+
description: "The emoji the reader has already reacted with: shown pressed, and choosing one again removes it."
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "onSelect",
|
|
78
|
+
type: "(emoji: string) => void",
|
|
79
|
+
description: "Called with the emoji the reader chose. Toggle it in or out of `selected`."
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "children",
|
|
83
|
+
type: "ReactNode",
|
|
84
|
+
description: "ReactionPicker only: trailing controls after the emoji, such as a button that opens a full emoji picker."
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "aria-label",
|
|
88
|
+
type: "string",
|
|
89
|
+
default: '"Reactions"',
|
|
90
|
+
description: "The name of the toolbar or menu group."
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "motion",
|
|
94
|
+
type: '"full" | "subtle" | "off"',
|
|
95
|
+
default: '"full"',
|
|
96
|
+
description: "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps the press and drops the wave of emoji blooming in and the magnify, `off` stills it. It can only turn motion down."
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
subcomponents: [
|
|
100
|
+
{
|
|
101
|
+
name: "MenuReactions",
|
|
102
|
+
type: "{ reactions?; selected?; onSelect?; motion? } & ComponentProps<typeof Menu.Group>",
|
|
103
|
+
description: "The quick row as checkbox menu items, for the first group of a MenuContent or ContextMenuContent. Same props as ReactionPicker, without children."
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
tokensUsed: ["accent", "primary", "ring"],
|
|
107
|
+
motion: [
|
|
108
|
+
{
|
|
109
|
+
slot: "arrival",
|
|
110
|
+
recipe: "motion-bloom",
|
|
111
|
+
part: "Glyph",
|
|
112
|
+
why: "Opening the picker is the reader's own action, so the emoji answer it: each blooms in 24ms after the one before, a short wave from first to last. The surface it sits in (a Popover or a menu) arrives with its own motion-pop. Expressive only \u2014 under `subtle` the emoji are simply there."
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
slot: "response",
|
|
116
|
+
recipe: "custom",
|
|
117
|
+
part: "Glyph",
|
|
118
|
+
why: "The emoji under the pointer or the keyboard highlight magnifies to 1.25 on a fast standard ease, so you can see which one a release will choose; each cell also carries motion-interactive and motion-press. The magnify rides an inner layer so it never fights the bloom over `scale`."
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
slot: "transition",
|
|
122
|
+
recipe: "motion-interactive",
|
|
123
|
+
part: "Cell",
|
|
124
|
+
why: "Pressed and not pressed differ by a tint, eased by motion-interactive. The celebration belongs to the reaction that lands under the message (BubbleReaction), not to the picker that chose it."
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
slot: "activity",
|
|
128
|
+
recipe: "none",
|
|
129
|
+
part: "Root",
|
|
130
|
+
why: "Choosing is instant; there is no in-between state to show."
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
a11y: [
|
|
134
|
+
"ReactionPicker is a Base UI toolbar: one tab stop, Left/Right move between emoji, Home/End jump to the ends.",
|
|
135
|
+
"Each emoji is a button named by its `label` and reporting `aria-pressed`, so a screen-reader user hears whether choosing it adds or removes their reaction. The glyph itself is aria-hidden.",
|
|
136
|
+
"MenuReactions renders checkbox menu items (`menuitemcheckbox` with `aria-checked`), reached by the menu's arrow keys and typeahead like every other row.",
|
|
137
|
+
"Under reduced motion the emoji appear at once and do not magnify; pressed state is carried by `aria-pressed` and a tint, never motion."
|
|
138
|
+
],
|
|
139
|
+
examples: [
|
|
140
|
+
{
|
|
141
|
+
label: "In a popover",
|
|
142
|
+
code: '<Popover>\n <PopoverTrigger render={<Button variant="ghost" size="icon-sm" aria-label="Add reaction" />}>\n <Icon name="emoji" />\n </PopoverTrigger>\n <PopoverContent side="top" className="w-auto rounded-full p-1">\n <ReactionPicker selected={mine} onSelect={toggle} />\n </PopoverContent>\n</Popover>'
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
label: "In a right-click menu",
|
|
146
|
+
code: "<ContextMenu>\n <ContextMenuTrigger render={<Bubble />}>See you at 6.</ContextMenuTrigger>\n <ContextMenuContent>\n <MenuReactions selected={mine} onSelect={toggle} />\n <ContextMenuSeparator />\n <ContextMenuItem>Reply</ContextMenuItem>\n <ContextMenuItem>Copy text</ContextMenuItem>\n </ContextMenuContent>\n</ContextMenu>"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
label: "A custom set",
|
|
150
|
+
code: '<ReactionPicker\n reactions={[\n { emoji: "\u2705", label: "Done" },\n { emoji: "\u{1F440}", label: "Looking" },\n { emoji: "\u{1F680}", label: "Shipped" },\n ]}\n selected={mine}\n onSelect={toggle}\n/>'
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
};
|
|
154
|
+
export {
|
|
155
|
+
reactionPickerMeta
|
|
156
|
+
};
|
|
@@ -5,6 +5,7 @@ import { Icon } from "../../icon/index.js";
|
|
|
5
5
|
import { cn } from "../../lib/cn.js";
|
|
6
6
|
import { Button } from "../button/button.js";
|
|
7
7
|
import { hitTargetMin } from "../../lib/hit-target.js";
|
|
8
|
+
import { STATUS_TONE_ICON } from "../../lib/status-tone.js";
|
|
8
9
|
import { withPortalThemeScope } from "../../lib/portal-theme-scope.js";
|
|
9
10
|
import { Spinner } from "../spinner/spinner.js";
|
|
10
11
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -51,7 +52,11 @@ var STACKED_ROOT = [
|
|
|
51
52
|
// steps down to shadow-md and the cards behind to shadow-sm; fanned out,
|
|
52
53
|
// each is a toast again and takes the full step back.
|
|
53
54
|
"not-data-expanded:shadow-md not-data-expanded:has-[>[data-behind]]:shadow-sm",
|
|
54
|
-
|
|
55
|
+
// The deck springs: cards stepping back and fanning out overshoot a touch
|
|
56
|
+
// and settle, which is what makes them read as cards rather than rows. A
|
|
57
|
+
// leaving toast takes the standard curve (nothing overshoots on its way
|
|
58
|
+
// out), and under "subtle" the spring is a plain decelerate.
|
|
59
|
+
"transition-[transform,translate,opacity,height,box-shadow] duration-(--motion-duration-slow) ease-spring data-ending-style:ease-standard"
|
|
55
60
|
].join(" ");
|
|
56
61
|
var STACKED_EDGE = {
|
|
57
62
|
top: "top-0 origin-top [--toast-dir:1] after:top-full",
|
|
@@ -204,11 +209,11 @@ function ToastProvider({
|
|
|
204
209
|
)
|
|
205
210
|
] });
|
|
206
211
|
}
|
|
207
|
-
var
|
|
208
|
-
success: "
|
|
209
|
-
error: "
|
|
210
|
-
warning: "
|
|
211
|
-
info: "
|
|
212
|
+
var TYPE_TONE = {
|
|
213
|
+
success: "success",
|
|
214
|
+
error: "destructive",
|
|
215
|
+
warning: "warning",
|
|
216
|
+
info: "info"
|
|
212
217
|
};
|
|
213
218
|
function ToastList({
|
|
214
219
|
closeIcon,
|
|
@@ -218,7 +223,8 @@ function ToastList({
|
|
|
218
223
|
}) {
|
|
219
224
|
const { toasts } = useToast();
|
|
220
225
|
return /* @__PURE__ */ jsx(ToastViewport, { position, stacked, className: viewportClassName, children: toasts.map((item) => {
|
|
221
|
-
const
|
|
226
|
+
const tone = item.type ? TYPE_TONE[item.type] : void 0;
|
|
227
|
+
const status = tone ? STATUS_TONE_ICON[tone] : void 0;
|
|
222
228
|
return /* @__PURE__ */ jsx(ToastRoot, { toast: item, children: /* @__PURE__ */ jsxs(ToastContent, { children: [
|
|
223
229
|
item.type === "loading" ? /* @__PURE__ */ jsx(
|
|
224
230
|
Spinner,
|
|
@@ -226,12 +232,18 @@ function ToastList({
|
|
|
226
232
|
"aria-hidden": true,
|
|
227
233
|
className: "mt-0.5 size-4 shrink-0 text-muted-foreground"
|
|
228
234
|
}
|
|
229
|
-
) :
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
+
) : status ? (
|
|
236
|
+
// Blooms in once the card has landed, and again when a
|
|
237
|
+
// loading toast settles into success or failure: the
|
|
238
|
+
// glyph mounts in the spinner's place.
|
|
239
|
+
/* @__PURE__ */ jsx(
|
|
240
|
+
"span",
|
|
241
|
+
{
|
|
242
|
+
"aria-hidden": true,
|
|
243
|
+
className: "mt-0.5 flex shrink-0 motion-bloom [--motion-bloom-delay:120ms]",
|
|
244
|
+
children: /* @__PURE__ */ jsx(Icon, { name: status.name, className: cn("text-base", status.className) })
|
|
245
|
+
}
|
|
246
|
+
)
|
|
235
247
|
) : null,
|
|
236
248
|
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
|
|
237
249
|
item.title ? /* @__PURE__ */ jsx(ToastTitle, { children: item.title }) : null,
|
|
@@ -136,7 +136,7 @@ var toastMeta = {
|
|
|
136
136
|
name: "type (toast option)",
|
|
137
137
|
type: '"success" | "error" | "warning" | "info" | "loading"',
|
|
138
138
|
default: "\u2014",
|
|
139
|
-
description: "Set by the toast.* helpers. Drives a
|
|
139
|
+
description: "Set by the toast.* helpers. Drives a leading status glyph from the shared tone list, the same one Note and InlineStatus use (success\u2192success, error\u2192destructive, warning\u2192warning, info\u2192info), which blooms in as the toast lands; loading shows a spinner, and the glyph blooms in its place when the toast settles. Both loading and error persist until updated or dismissed unless the call supplies a timeout."
|
|
140
140
|
}
|
|
141
141
|
],
|
|
142
142
|
subcomponents: [
|
|
@@ -211,6 +211,12 @@ var toastMeta = {
|
|
|
211
211
|
part: "ToastRoot (stacked)",
|
|
212
212
|
why: "A stacked toast arrives from the anchored edge in front of the others, which step back and shrink to make room; hovering fans the deck out into the list. Moving every card, rather than snapping them, shows that the new one went on top and the old ones are still there."
|
|
213
213
|
},
|
|
214
|
+
{
|
|
215
|
+
slot: "response",
|
|
216
|
+
recipe: "motion-bloom",
|
|
217
|
+
part: "Status glyph",
|
|
218
|
+
why: "The glyph is what says how it went, so it lands as its own beat: it blooms in 120ms after the card, and blooms again in the spinner's place when a loading toast settles. A 16px glyph needs that amplitude to read as an answer; the card itself only slides."
|
|
219
|
+
},
|
|
214
220
|
{
|
|
215
221
|
slot: "activity",
|
|
216
222
|
recipe: "none",
|
package/dist/file-hashes.json
CHANGED
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"dist/blocks/goal-progress-block.meta.js": "3e5b376bf58bdb4b",
|
|
27
27
|
"dist/blocks/hero-block.js": "ac74e80d4d7d2ad9",
|
|
28
28
|
"dist/blocks/hero-block.meta.js": "2dd2e5be2472a563",
|
|
29
|
-
"dist/blocks/index.d.ts": "
|
|
30
|
-
"dist/blocks/index.js": "
|
|
29
|
+
"dist/blocks/index.d.ts": "940238f0164f6d03",
|
|
30
|
+
"dist/blocks/index.js": "690417c222dc4d2a",
|
|
31
31
|
"dist/blocks/invite-team-block.js": "be4df15518252535",
|
|
32
32
|
"dist/blocks/invite-team-block.meta.js": "fd57b632f0f32276",
|
|
33
33
|
"dist/blocks/invoice-history-block.js": "2befa5b9c6721d71",
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
"dist/blocks/kpi-sparkline-block.meta.js": "e72d7bd8f2b8c375",
|
|
37
37
|
"dist/blocks/media-player-block.js": "ac5091114576e539",
|
|
38
38
|
"dist/blocks/media-player-block.meta.js": "dd217cd0e93d9f5e",
|
|
39
|
+
"dist/blocks/message-reactions-block.js": "3ca1f91d9f53556e",
|
|
40
|
+
"dist/blocks/message-reactions-block.meta.js": "152314a67b45ade2",
|
|
39
41
|
"dist/blocks/newsletter-block.js": "047e5f22fcd3e5e3",
|
|
40
42
|
"dist/blocks/newsletter-block.meta.js": "2d9b133a96882482",
|
|
41
43
|
"dist/blocks/not-found-block.js": "bcc1f6b3c908777e",
|
|
@@ -126,10 +128,10 @@
|
|
|
126
128
|
"dist/components/browser/browser.js": "5d81502731837d2a",
|
|
127
129
|
"dist/components/browser/browser.meta.js": "c9307e7dbe925bf8",
|
|
128
130
|
"dist/components/browser/index.js": "b15737bb72715f84",
|
|
129
|
-
"dist/components/bubble/bubble.js": "
|
|
130
|
-
"dist/components/bubble/bubble.meta.js": "
|
|
131
|
-
"dist/components/bubble/bubble.variants.js": "
|
|
132
|
-
"dist/components/bubble/index.js": "
|
|
131
|
+
"dist/components/bubble/bubble.js": "59ca0e8ad01a481e",
|
|
132
|
+
"dist/components/bubble/bubble.meta.js": "69844a1e162679d0",
|
|
133
|
+
"dist/components/bubble/bubble.variants.js": "b5a50a9aee4ff17a",
|
|
134
|
+
"dist/components/bubble/index.js": "a45688c4023078b9",
|
|
133
135
|
"dist/components/button/button.js": "323b7f7e0cc3c6ab",
|
|
134
136
|
"dist/components/button/button.meta.js": "36d463ef6c205894",
|
|
135
137
|
"dist/components/button/button.variants.js": "137c201e0c01b56f",
|
|
@@ -216,8 +218,8 @@
|
|
|
216
218
|
"dist/components/entity/entity.meta.js": "2164fb36ab4d3962",
|
|
217
219
|
"dist/components/entity/entity.variants.js": "8838fb5cf6f304f1",
|
|
218
220
|
"dist/components/entity/index.js": "1397602aa7068da8",
|
|
219
|
-
"dist/components/feedback/feedback.js": "
|
|
220
|
-
"dist/components/feedback/feedback.meta.js": "
|
|
221
|
+
"dist/components/feedback/feedback.js": "6fe0ae8a638da70b",
|
|
222
|
+
"dist/components/feedback/feedback.meta.js": "6fd9f9b2c942bf09",
|
|
221
223
|
"dist/components/feedback/index.js": "fd1ab40c9d879286",
|
|
222
224
|
"dist/components/fieldset/fieldset.js": "08d02302b6866f8e",
|
|
223
225
|
"dist/components/fieldset/fieldset.meta.js": "64be6d633de27e04",
|
|
@@ -255,6 +257,9 @@
|
|
|
255
257
|
"dist/components/label/index.js": "413f64adef955fe5",
|
|
256
258
|
"dist/components/label/label.js": "51214a76795f6a51",
|
|
257
259
|
"dist/components/label/label.meta.js": "82a347c3c9dd2165",
|
|
260
|
+
"dist/components/like-button/index.js": "40539483ab512af5",
|
|
261
|
+
"dist/components/like-button/like-button.js": "d0fde69ec74a8b65",
|
|
262
|
+
"dist/components/like-button/like-button.meta.js": "0b4bd1cae3f7acf7",
|
|
258
263
|
"dist/components/line-chart/index.js": "6d7a54c8e9433fb1",
|
|
259
264
|
"dist/components/line-chart/line-chart.js": "217cf598be16698c",
|
|
260
265
|
"dist/components/line-chart/line-chart.meta.js": "debad75d571435f7",
|
|
@@ -361,6 +366,9 @@
|
|
|
361
366
|
"dist/components/radio-card/radio-card.js": "29c459d88339f035",
|
|
362
367
|
"dist/components/radio-card/radio-card.meta.js": "4b5ff5a38c6b8915",
|
|
363
368
|
"dist/components/radio-card/radio-card.variants.js": "e67661edc33062fb",
|
|
369
|
+
"dist/components/reaction-picker/index.js": "417147b602d14e9d",
|
|
370
|
+
"dist/components/reaction-picker/reaction-picker.js": "9eb295fdaf434742",
|
|
371
|
+
"dist/components/reaction-picker/reaction-picker.meta.js": "629b54a65d184975",
|
|
364
372
|
"dist/components/relative-time/index.js": "02e9b7b91ccad470",
|
|
365
373
|
"dist/components/relative-time/relative-time.js": "db5cd286b36dee74",
|
|
366
374
|
"dist/components/relative-time/relative-time.meta.js": "472cb014dc9b39fb",
|
|
@@ -453,8 +461,8 @@
|
|
|
453
461
|
"dist/components/thinking-orb/thinking-orb.meta.js": "d614e8ebfded8710",
|
|
454
462
|
"dist/components/thinking-orb/thinking-orb.render.js": "1d561ac1ffcd1e97",
|
|
455
463
|
"dist/components/toast/index.js": "d13be0a6c335931d",
|
|
456
|
-
"dist/components/toast/toast.js": "
|
|
457
|
-
"dist/components/toast/toast.meta.js": "
|
|
464
|
+
"dist/components/toast/toast.js": "6cc9beed93790af0",
|
|
465
|
+
"dist/components/toast/toast.meta.js": "1d444a25b34a7078",
|
|
458
466
|
"dist/components/toggle/index.js": "84a0a4bf596a1b7d",
|
|
459
467
|
"dist/components/toggle/toggle.js": "bf9d610b6d07ec88",
|
|
460
468
|
"dist/components/toggle/toggle.meta.js": "096b129f56b46e05",
|
|
@@ -481,7 +489,7 @@
|
|
|
481
489
|
"dist/foundations/accessibility.js": "e09d6f4063e58fd1",
|
|
482
490
|
"dist/foundations/index.d.ts": "34f9553827230412",
|
|
483
491
|
"dist/foundations/index.js": "37e83ca8063a4282",
|
|
484
|
-
"dist/foundations/motion.js": "
|
|
492
|
+
"dist/foundations/motion.js": "618ddec729326caf",
|
|
485
493
|
"dist/foundations/scrolling.js": "a5a537256ff04379",
|
|
486
494
|
"dist/foundations/shape.js": "d2cb3117f28d5063",
|
|
487
495
|
"dist/foundations/spacing.js": "49fd401256835447",
|
|
@@ -498,9 +506,9 @@
|
|
|
498
506
|
"dist/icon-sets/brands.generated.js": "901e198df0563b0b",
|
|
499
507
|
"dist/icon-sets/brands.js": "9a37d8739f14f2a9",
|
|
500
508
|
"dist/icon-sets/reicon.d.ts": "52f6e41dc8137b4e",
|
|
501
|
-
"dist/icon-sets/reicon.js": "
|
|
502
|
-
"dist/index.d.ts": "
|
|
503
|
-
"dist/index.js": "
|
|
509
|
+
"dist/icon-sets/reicon.js": "0f73074baac9b866",
|
|
510
|
+
"dist/index.d.ts": "bab442001128fe64",
|
|
511
|
+
"dist/index.js": "5d23113769c924de",
|
|
504
512
|
"dist/ios/chrome.css": "a7ca9e3735d8658e",
|
|
505
513
|
"dist/ios/chrome.js": "37091fef7b68bd28",
|
|
506
514
|
"dist/ios/frame.css": "dd2703c944ce575a",
|
|
@@ -565,7 +573,7 @@
|
|
|
565
573
|
"dist/lib/segmented.js": "43c21955f7f3ae33",
|
|
566
574
|
"dist/lib/selection-card.js": "43c49c8c51dde54c",
|
|
567
575
|
"dist/lib/status-tone.js": "61f0a8bb4fb9c7d6",
|
|
568
|
-
"dist/lib/taxonomy.js": "
|
|
576
|
+
"dist/lib/taxonomy.js": "4ecb616f502009c0",
|
|
569
577
|
"dist/lib/trim-label.js": "9998d45e03c95515",
|
|
570
578
|
"dist/lib/use-caret-trigger.js": "85839ca42e5540de",
|
|
571
579
|
"dist/lib/use-copy.js": "5cf22621e4d5e83b",
|
|
@@ -575,11 +583,12 @@
|
|
|
575
583
|
"dist/lib/use-scroll-activity.js": "793394a8c952e697",
|
|
576
584
|
"dist/lib/use-smooth-wheel.js": "9fd10f864eb40325",
|
|
577
585
|
"dist/meta-DX5r5wQL.d.ts": "b15f63123088709a",
|
|
586
|
+
"dist/motion/Burst.js": "cede90200e958908",
|
|
578
587
|
"dist/motion/MotionProvider.js": "624f5502828e7fd0",
|
|
579
588
|
"dist/motion/Presence.js": "21fc6f1c68207718",
|
|
580
589
|
"dist/motion/Reveal.js": "b42ceeee28320405",
|
|
581
590
|
"dist/motion/TextReveal.js": "ca61c6ae64917246",
|
|
582
|
-
"dist/motion/index.js": "
|
|
591
|
+
"dist/motion/index.js": "a638b7e0517ec687",
|
|
583
592
|
"dist/motion/useAutoSize.js": "b4b06290e1263c95",
|
|
584
593
|
"dist/motion/useInView.js": "6136ad61107db838",
|
|
585
594
|
"dist/motion/useReducedMotion.js": "ae7c093aac26e254",
|
|
@@ -607,7 +616,7 @@
|
|
|
607
616
|
"dist/presets/index.d.ts": "605f8edf5746ab3e",
|
|
608
617
|
"dist/presets/index.js": "a911222560939d60",
|
|
609
618
|
"dist/registry-CyL28OzJ.d.ts": "b068fc89571517d8",
|
|
610
|
-
"dist/styles.css": "
|
|
619
|
+
"dist/styles.css": "32bbb6860ff85c46",
|
|
611
620
|
"dist/theme-provider-CdSicAad.d.ts": "a37d38f7e3c64d31",
|
|
612
621
|
"dist/themes/_contract.js": "bc864a86d4cdbe1c",
|
|
613
622
|
"dist/themes/atelier/atelier.css": "cf7630ead6409242",
|