@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
package/registry.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://sable.design/registry.schema.json",
|
|
3
3
|
"name": "@sableui/react",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.15.0",
|
|
5
5
|
"description": "Themeable React components, semantic tokens, layout patterns, presets, and package-owned agent skills. Default theme: Sable.",
|
|
6
6
|
"engine": {
|
|
7
7
|
"behavior": "@base-ui/react",
|
|
@@ -954,6 +954,7 @@
|
|
|
954
954
|
"motion-tick",
|
|
955
955
|
"motion-appear",
|
|
956
956
|
"motion-arrive",
|
|
957
|
+
"motion-bloom",
|
|
957
958
|
"motion-size",
|
|
958
959
|
"motion-reveal",
|
|
959
960
|
"motion-draw",
|
|
@@ -1785,7 +1786,10 @@
|
|
|
1785
1786
|
"paperclip",
|
|
1786
1787
|
"chart",
|
|
1787
1788
|
"flag",
|
|
1788
|
-
"at-sign"
|
|
1789
|
+
"at-sign",
|
|
1790
|
+
"replay",
|
|
1791
|
+
"motion",
|
|
1792
|
+
"emoji"
|
|
1789
1793
|
]
|
|
1790
1794
|
},
|
|
1791
1795
|
"layoutPrimitives": {
|
|
@@ -4924,7 +4928,12 @@
|
|
|
4924
4928
|
{
|
|
4925
4929
|
"name": "BubbleReaction",
|
|
4926
4930
|
"required": false,
|
|
4927
|
-
"description": "One reaction pill — emoji, count (hidden at 1), and pressed state."
|
|
4931
|
+
"description": "One reaction pill — emoji, count (hidden at 1), and pressed state. Celebrates when it becomes the reader's own."
|
|
4932
|
+
},
|
|
4933
|
+
{
|
|
4934
|
+
"name": "BubbleReactionAdd",
|
|
4935
|
+
"required": false,
|
|
4936
|
+
"description": "The add-reaction pill at the end of the row: opens a ReactionPicker above it."
|
|
4928
4937
|
}
|
|
4929
4938
|
]
|
|
4930
4939
|
},
|
|
@@ -4933,7 +4942,8 @@
|
|
|
4933
4942
|
"Message",
|
|
4934
4943
|
"MessageScroller",
|
|
4935
4944
|
"Marker",
|
|
4936
|
-
"Attachment"
|
|
4945
|
+
"Attachment",
|
|
4946
|
+
"ReactionPicker"
|
|
4937
4947
|
]
|
|
4938
4948
|
},
|
|
4939
4949
|
"props": [
|
|
@@ -4969,8 +4979,13 @@
|
|
|
4969
4979
|
},
|
|
4970
4980
|
{
|
|
4971
4981
|
"name": "BubbleReaction",
|
|
4972
|
-
"type": "{ emoji: ReactNode; count?: number; active?: boolean } & ComponentPropsWithRef<\"button\">",
|
|
4973
|
-
"description": "One reaction pill. A toggle, not a label: `active` drives `aria-pressed` as well as the styling, and `count` is hidden at 1 because the pill already says \"one\"."
|
|
4982
|
+
"type": "{ emoji: ReactNode; label?: string; count?: number; active?: boolean; motion?: MotionLevel } & ComponentPropsWithRef<\"button\">",
|
|
4983
|
+
"description": "One reaction pill. A toggle, not a label: `active` drives `aria-pressed` as well as the styling, `label` names it (\"Party, 3\"), and `count` is hidden at 1 because the pill already says \"one\". When it turns on — pressed, or newly added already on — the emoji blooms, copies of it burst out and the count rolls; pills the page loads with stay still."
|
|
4984
|
+
},
|
|
4985
|
+
{
|
|
4986
|
+
"name": "BubbleReactionAdd",
|
|
4987
|
+
"type": "{ reactions?: Reaction[]; selected?: string[]; onSelect?: (emoji: string) => void; label?: string; motion?: MotionLevel } & ComponentPropsWithRef<\"button\">",
|
|
4988
|
+
"description": "The add-reaction pill: a smiley that opens a ReactionPicker above it. Choosing an emoji closes the picker and calls `onSelect`; add the emoji to the row and its new pill celebrates on arrival."
|
|
4974
4989
|
}
|
|
4975
4990
|
],
|
|
4976
4991
|
"variants": {
|
|
@@ -5002,9 +5017,35 @@
|
|
|
5002
5017
|
"muted-foreground",
|
|
5003
5018
|
"ring"
|
|
5004
5019
|
],
|
|
5020
|
+
"motion": [
|
|
5021
|
+
{
|
|
5022
|
+
"slot": "arrival",
|
|
5023
|
+
"recipe": "none",
|
|
5024
|
+
"part": "Bubble",
|
|
5025
|
+
"why": "A bubble arrives with its Message row, which owns how a new message enters the transcript. A reaction pill that the reader just added arrives with its own celebration (below)."
|
|
5026
|
+
},
|
|
5027
|
+
{
|
|
5028
|
+
"slot": "response",
|
|
5029
|
+
"recipe": "motion-bloom",
|
|
5030
|
+
"part": "BubbleReaction",
|
|
5031
|
+
"why": "Reacting is the reader's choice, so it is answered like a like: the emoji grows from nothing past its size and settles, while copies of it spray outward and fade (Burst with the emoji as its particle, the way chat apps answer a reaction). Plays when a pill turns on and when a pill mounts already on into a row that has rendered — never for the pills a page loads with. Pills inherit Pill's motion-interactive and motion-press."
|
|
5032
|
+
},
|
|
5033
|
+
{
|
|
5034
|
+
"slot": "transition",
|
|
5035
|
+
"recipe": "custom",
|
|
5036
|
+
"part": "BubbleReaction",
|
|
5037
|
+
"why": "The count rolls with NumberTicker as people react. Turning a reaction off is quiet: the tint eases out and the count rolls back, with no burst. Under `subtle` or reduced motion the bloom and the spray are dropped and the tint and count alone answer."
|
|
5038
|
+
},
|
|
5039
|
+
{
|
|
5040
|
+
"slot": "activity",
|
|
5041
|
+
"recipe": "none",
|
|
5042
|
+
"part": "Bubble",
|
|
5043
|
+
"why": "A bubble is static content; a message still being written is StreamingText or a Marker, not a state of the surface."
|
|
5044
|
+
}
|
|
5045
|
+
],
|
|
5005
5046
|
"a11y": [
|
|
5006
5047
|
"The surface is a plain container with no imposed role — a bubble is presentation, and announcing one as a region would put a landmark around every line of a conversation.",
|
|
5007
|
-
"Reaction pills are real buttons reporting `aria-pressed`, so the reader's own reaction is state rather than colour alone
|
|
5048
|
+
"Reaction pills are real buttons reporting `aria-pressed`, so the reader's own reaction is state rather than colour alone. Give each a `label`: it is named \"Party, 3\", with the emoji and its burst hidden from AT. The add-reaction pill is named \"Add reaction\" and opens a toolbar picker.",
|
|
5008
5049
|
"`whitespace-pre-wrap` and `break-words` keep pasted logs, long URLs, and hard-wrapped text inside the surface instead of overflowing the conversation.",
|
|
5009
5050
|
"Every variant meets AA against its own surface, including `primary`, which pairs the primary fill with its paired foreground token rather than a hardcoded white."
|
|
5010
5051
|
],
|
|
@@ -5021,8 +5062,8 @@
|
|
|
5021
5062
|
},
|
|
5022
5063
|
{
|
|
5023
5064
|
"label": "With reactions",
|
|
5024
|
-
"code": "<Message messageId=\"m3\">\n\t<Bubble>Shipped 0.45.0.</Bubble>\n\t<BubbleReactions>\n\t\t<BubbleReaction emoji=\"🎉\" count={3} active onClick={() => toggle(\"🎉\")} />\n\t\t<BubbleReaction emoji=\"👀\" count={1} onClick={() => toggle(\"👀\")} />\n\t</BubbleReactions>\n</Message>",
|
|
5025
|
-
"imports": "import { Bubble, BubbleReaction, BubbleReactions, Message } from \"@sableui/react\""
|
|
5065
|
+
"code": "<Message messageId=\"m3\">\n\t<Bubble>Shipped 0.45.0.</Bubble>\n\t<BubbleReactions>\n\t\t<BubbleReaction emoji=\"🎉\" label=\"Party\" count={3} active onClick={() => toggle(\"🎉\")} />\n\t\t<BubbleReaction emoji=\"👀\" label=\"Eyes\" count={1} onClick={() => toggle(\"👀\")} />\n\t\t<BubbleReactionAdd selected={mine} onSelect={toggle} />\n\t</BubbleReactions>\n</Message>",
|
|
5066
|
+
"imports": "import { Bubble, BubbleReaction, BubbleReactionAdd, BubbleReactions, Message } from \"@sableui/react\""
|
|
5026
5067
|
},
|
|
5027
5068
|
{
|
|
5028
5069
|
"label": "A grouped run drops the repeated tail",
|
|
@@ -10176,6 +10217,12 @@
|
|
|
10176
10217
|
"part": "Rating toggles",
|
|
10177
10218
|
"why": "The rating row is a choice among a few similar marks. The eased selected fill is what makes the pick read as made, not just hovered."
|
|
10178
10219
|
},
|
|
10220
|
+
{
|
|
10221
|
+
"slot": "response",
|
|
10222
|
+
"recipe": "motion-bloom",
|
|
10223
|
+
"part": "Chosen rating mark",
|
|
10224
|
+
"why": "The mark itself answers the pick: it blooms past its size and settles, so choosing reads as a small decision landing rather than a fill changing. Only the chosen mark, only on choosing — nothing blooms when the form opens or a pick is let go. Expressive: under subtle the fill alone answers."
|
|
10225
|
+
},
|
|
10179
10226
|
{
|
|
10180
10227
|
"slot": "transition",
|
|
10181
10228
|
"recipe": "motion-tick",
|
|
@@ -11879,6 +11926,217 @@
|
|
|
11879
11926
|
}
|
|
11880
11927
|
]
|
|
11881
11928
|
},
|
|
11929
|
+
{
|
|
11930
|
+
"name": "LikeButton",
|
|
11931
|
+
"status": "stable",
|
|
11932
|
+
"group": "Actions",
|
|
11933
|
+
"register": "either",
|
|
11934
|
+
"path": "components/like-button",
|
|
11935
|
+
"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.",
|
|
11936
|
+
"keywords": [
|
|
11937
|
+
"like",
|
|
11938
|
+
"heart",
|
|
11939
|
+
"favorite",
|
|
11940
|
+
"favourite",
|
|
11941
|
+
"love",
|
|
11942
|
+
"star",
|
|
11943
|
+
"bookmark",
|
|
11944
|
+
"save",
|
|
11945
|
+
"upvote",
|
|
11946
|
+
"reaction",
|
|
11947
|
+
"count",
|
|
11948
|
+
"toggle"
|
|
11949
|
+
],
|
|
11950
|
+
"usage": {
|
|
11951
|
+
"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.",
|
|
11952
|
+
"bestPractices": [
|
|
11953
|
+
{
|
|
11954
|
+
"guidance": "do",
|
|
11955
|
+
"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."
|
|
11956
|
+
},
|
|
11957
|
+
{
|
|
11958
|
+
"guidance": "do",
|
|
11959
|
+
"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."
|
|
11960
|
+
},
|
|
11961
|
+
{
|
|
11962
|
+
"guidance": "do",
|
|
11963
|
+
"description": "Use `format={{ notation: \"compact\" }}` once counts pass a few thousand, so a feed of buttons keeps one width."
|
|
11964
|
+
},
|
|
11965
|
+
{
|
|
11966
|
+
"guidance": "dont",
|
|
11967
|
+
"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.",
|
|
11968
|
+
"instead": [
|
|
11969
|
+
"Toggle"
|
|
11970
|
+
]
|
|
11971
|
+
},
|
|
11972
|
+
{
|
|
11973
|
+
"guidance": "dont",
|
|
11974
|
+
"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.",
|
|
11975
|
+
"instead": [
|
|
11976
|
+
"Feedback"
|
|
11977
|
+
]
|
|
11978
|
+
},
|
|
11979
|
+
{
|
|
11980
|
+
"guidance": "dont",
|
|
11981
|
+
"description": "Do not press it for the user. A burst is a reply to a person's choice — liking something on their behalf, or on load, turns delight into a notification."
|
|
11982
|
+
}
|
|
11983
|
+
]
|
|
11984
|
+
},
|
|
11985
|
+
"related": {
|
|
11986
|
+
"components": [
|
|
11987
|
+
"Toggle",
|
|
11988
|
+
"Feedback",
|
|
11989
|
+
"NumberTicker",
|
|
11990
|
+
"Button"
|
|
11991
|
+
]
|
|
11992
|
+
},
|
|
11993
|
+
"props": [
|
|
11994
|
+
{
|
|
11995
|
+
"name": "count",
|
|
11996
|
+
"type": "number",
|
|
11997
|
+
"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)."
|
|
11998
|
+
},
|
|
11999
|
+
{
|
|
12000
|
+
"name": "pressed",
|
|
12001
|
+
"type": "boolean",
|
|
12002
|
+
"description": "Controlled pressed state."
|
|
12003
|
+
},
|
|
12004
|
+
{
|
|
12005
|
+
"name": "defaultPressed",
|
|
12006
|
+
"type": "boolean",
|
|
12007
|
+
"default": "false",
|
|
12008
|
+
"description": "Initial pressed state when uncontrolled."
|
|
12009
|
+
},
|
|
12010
|
+
{
|
|
12011
|
+
"name": "onPressedChange",
|
|
12012
|
+
"type": "(pressed: boolean, event) => void",
|
|
12013
|
+
"description": "Called when the user presses it, with the new state."
|
|
12014
|
+
},
|
|
12015
|
+
{
|
|
12016
|
+
"name": "icon",
|
|
12017
|
+
"type": "string",
|
|
12018
|
+
"default": "\"heart\"",
|
|
12019
|
+
"description": "The glyph, by icon name. Its `solid` variant is shown while pressed."
|
|
12020
|
+
},
|
|
12021
|
+
{
|
|
12022
|
+
"name": "tone",
|
|
12023
|
+
"type": "PaletteColor",
|
|
12024
|
+
"default": "\"red\"",
|
|
12025
|
+
"description": "The palette family the glyph fills with while pressed, and the burst blooms from. A family, so it follows the theme."
|
|
12026
|
+
},
|
|
12027
|
+
{
|
|
12028
|
+
"name": "label",
|
|
12029
|
+
"type": "string",
|
|
12030
|
+
"default": "\"Like\"",
|
|
12031
|
+
"description": "The accessible name, read before the count."
|
|
12032
|
+
},
|
|
12033
|
+
{
|
|
12034
|
+
"name": "format",
|
|
12035
|
+
"type": "Intl.NumberFormatOptions",
|
|
12036
|
+
"description": "How to write the count, e.g. `{ notation: \"compact\" }`."
|
|
12037
|
+
},
|
|
12038
|
+
{
|
|
12039
|
+
"name": "variant",
|
|
12040
|
+
"type": "\"ghost\" | \"outline\" | \"secondary\"",
|
|
12041
|
+
"default": "\"ghost\"",
|
|
12042
|
+
"description": "The Button surface."
|
|
12043
|
+
},
|
|
12044
|
+
{
|
|
12045
|
+
"name": "size",
|
|
12046
|
+
"type": "\"xs\" | \"sm\" | \"md\"",
|
|
12047
|
+
"default": "\"sm\"",
|
|
12048
|
+
"description": "A Button size; without a count, the matching icon size."
|
|
12049
|
+
},
|
|
12050
|
+
{
|
|
12051
|
+
"name": "disabled",
|
|
12052
|
+
"type": "boolean",
|
|
12053
|
+
"default": "false",
|
|
12054
|
+
"description": "Disables it."
|
|
12055
|
+
},
|
|
12056
|
+
{
|
|
12057
|
+
"name": "motion",
|
|
12058
|
+
"type": "\"full\" | \"subtle\" | \"off\"",
|
|
12059
|
+
"default": "\"full\"",
|
|
12060
|
+
"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 — it never overrides a reader's reduce-motion setting. The bloom and the burst are the flourish; the fill and the count changing stay."
|
|
12061
|
+
}
|
|
12062
|
+
],
|
|
12063
|
+
"variants": {
|
|
12064
|
+
"variant": [
|
|
12065
|
+
"ghost",
|
|
12066
|
+
"outline",
|
|
12067
|
+
"secondary"
|
|
12068
|
+
],
|
|
12069
|
+
"size": [
|
|
12070
|
+
"xs",
|
|
12071
|
+
"sm",
|
|
12072
|
+
"md"
|
|
12073
|
+
],
|
|
12074
|
+
"tone": [
|
|
12075
|
+
"red",
|
|
12076
|
+
"orange",
|
|
12077
|
+
"yellow",
|
|
12078
|
+
"green",
|
|
12079
|
+
"teal",
|
|
12080
|
+
"blue",
|
|
12081
|
+
"purple",
|
|
12082
|
+
"pink"
|
|
12083
|
+
]
|
|
12084
|
+
},
|
|
12085
|
+
"tokensUsed": [
|
|
12086
|
+
"palette-dot",
|
|
12087
|
+
"accent",
|
|
12088
|
+
"ring"
|
|
12089
|
+
],
|
|
12090
|
+
"motion": [
|
|
12091
|
+
{
|
|
12092
|
+
"slot": "arrival",
|
|
12093
|
+
"recipe": "none",
|
|
12094
|
+
"part": "Root",
|
|
12095
|
+
"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."
|
|
12096
|
+
},
|
|
12097
|
+
{
|
|
12098
|
+
"slot": "response",
|
|
12099
|
+
"recipe": "motion-bloom",
|
|
12100
|
+
"part": "Glyph",
|
|
12101
|
+
"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 — taking a like back is not a celebration."
|
|
12102
|
+
},
|
|
12103
|
+
{
|
|
12104
|
+
"slot": "transition",
|
|
12105
|
+
"recipe": "custom",
|
|
12106
|
+
"part": "Burst",
|
|
12107
|
+
"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 — under `subtle` or reduced motion it renders nothing and the fill alone answers the press. The count rolls with NumberTicker."
|
|
12108
|
+
},
|
|
12109
|
+
{
|
|
12110
|
+
"slot": "activity",
|
|
12111
|
+
"recipe": "none",
|
|
12112
|
+
"part": "Root",
|
|
12113
|
+
"why": "There is no in-between state to show: the like is shown at once, before it is saved."
|
|
12114
|
+
}
|
|
12115
|
+
],
|
|
12116
|
+
"a11y": [
|
|
12117
|
+
"A native <button> through Button, with `aria-pressed`. Enter and Space press it.",
|
|
12118
|
+
"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.",
|
|
12119
|
+
"The pressed state is carried by the filled glyph and `aria-pressed`, never by colour alone.",
|
|
12120
|
+
"Under reduced motion the glyph fills and the count changes at once; the bloom and the burst are dropped."
|
|
12121
|
+
],
|
|
12122
|
+
"examples": [
|
|
12123
|
+
{
|
|
12124
|
+
"label": "A post's likes",
|
|
12125
|
+
"code": "<LikeButton count={post.likes} defaultPressed={post.liked} onPressedChange={(on) => api.like(post.id, on)} />",
|
|
12126
|
+
"imports": "import { LikeButton } from \"@sableui/react\""
|
|
12127
|
+
},
|
|
12128
|
+
{
|
|
12129
|
+
"label": "Star a repository",
|
|
12130
|
+
"code": "<LikeButton icon=\"star\" tone=\"yellow\" label=\"Star\" variant=\"outline\" count={repo.stars} format={{ notation: \"compact\" }} />",
|
|
12131
|
+
"imports": "import { LikeButton } from \"@sableui/react\""
|
|
12132
|
+
},
|
|
12133
|
+
{
|
|
12134
|
+
"label": "Bare glyph",
|
|
12135
|
+
"code": "<LikeButton label=\"Save recipe\" />",
|
|
12136
|
+
"imports": "import { LikeButton } from \"@sableui/react\""
|
|
12137
|
+
}
|
|
12138
|
+
]
|
|
12139
|
+
},
|
|
11882
12140
|
{
|
|
11883
12141
|
"name": "LineChart",
|
|
11884
12142
|
"status": "beta",
|
|
@@ -17235,6 +17493,178 @@
|
|
|
17235
17493
|
}
|
|
17236
17494
|
]
|
|
17237
17495
|
},
|
|
17496
|
+
{
|
|
17497
|
+
"name": "ReactionPicker",
|
|
17498
|
+
"status": "stable",
|
|
17499
|
+
"group": "Actions",
|
|
17500
|
+
"register": "either",
|
|
17501
|
+
"path": "components/reaction-picker",
|
|
17502
|
+
"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.",
|
|
17503
|
+
"keywords": [
|
|
17504
|
+
"reaction",
|
|
17505
|
+
"reactions",
|
|
17506
|
+
"emoji",
|
|
17507
|
+
"emoji picker",
|
|
17508
|
+
"react",
|
|
17509
|
+
"tapback",
|
|
17510
|
+
"quick reactions",
|
|
17511
|
+
"message actions",
|
|
17512
|
+
"chat"
|
|
17513
|
+
],
|
|
17514
|
+
"usage": {
|
|
17515
|
+
"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.",
|
|
17516
|
+
"bestPractices": [
|
|
17517
|
+
{
|
|
17518
|
+
"guidance": "do",
|
|
17519
|
+
"description": "Keep the quick set short — six to eight. It is the reactions people actually use; a full emoji catalogue belongs behind a trailing \"more\" control passed as children."
|
|
17520
|
+
},
|
|
17521
|
+
{
|
|
17522
|
+
"guidance": "do",
|
|
17523
|
+
"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."
|
|
17524
|
+
},
|
|
17525
|
+
{
|
|
17526
|
+
"guidance": "do",
|
|
17527
|
+
"description": "Pass `selected` so the reader's own reactions show pressed, and toggle in `onSelect`: choosing a reaction you already gave takes it back."
|
|
17528
|
+
},
|
|
17529
|
+
{
|
|
17530
|
+
"guidance": "do",
|
|
17531
|
+
"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."
|
|
17532
|
+
},
|
|
17533
|
+
{
|
|
17534
|
+
"guidance": "dont",
|
|
17535
|
+
"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.",
|
|
17536
|
+
"instead": [
|
|
17537
|
+
"MenuReactions"
|
|
17538
|
+
]
|
|
17539
|
+
},
|
|
17540
|
+
{
|
|
17541
|
+
"guidance": "dont",
|
|
17542
|
+
"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.",
|
|
17543
|
+
"instead": [
|
|
17544
|
+
"Feedback"
|
|
17545
|
+
]
|
|
17546
|
+
}
|
|
17547
|
+
],
|
|
17548
|
+
"anatomy": [
|
|
17549
|
+
{
|
|
17550
|
+
"name": "ReactionPicker",
|
|
17551
|
+
"required": true,
|
|
17552
|
+
"description": "The toolbar: one tab stop, arrow keys move between emoji, each a toggle with `aria-pressed`."
|
|
17553
|
+
},
|
|
17554
|
+
{
|
|
17555
|
+
"name": "MenuReactions",
|
|
17556
|
+
"required": false,
|
|
17557
|
+
"description": "The same row as a group of checkbox menu items, for MenuContent and ContextMenuContent. Choosing one closes the menu."
|
|
17558
|
+
}
|
|
17559
|
+
]
|
|
17560
|
+
},
|
|
17561
|
+
"related": {
|
|
17562
|
+
"components": [
|
|
17563
|
+
"Bubble",
|
|
17564
|
+
"ContextMenu",
|
|
17565
|
+
"Popover",
|
|
17566
|
+
"Toolbar",
|
|
17567
|
+
"Feedback"
|
|
17568
|
+
]
|
|
17569
|
+
},
|
|
17570
|
+
"props": [
|
|
17571
|
+
{
|
|
17572
|
+
"name": "reactions",
|
|
17573
|
+
"type": "readonly { emoji: string; label: string }[]",
|
|
17574
|
+
"default": "DEFAULT_REACTIONS",
|
|
17575
|
+
"description": "The choices, in order. The default is 👍 ❤️ 😂 🎉 😮 😢 🙏."
|
|
17576
|
+
},
|
|
17577
|
+
{
|
|
17578
|
+
"name": "selected",
|
|
17579
|
+
"type": "readonly string[]",
|
|
17580
|
+
"default": "[]",
|
|
17581
|
+
"description": "The emoji the reader has already reacted with: shown pressed, and choosing one again removes it."
|
|
17582
|
+
},
|
|
17583
|
+
{
|
|
17584
|
+
"name": "onSelect",
|
|
17585
|
+
"type": "(emoji: string) => void",
|
|
17586
|
+
"description": "Called with the emoji the reader chose. Toggle it in or out of `selected`."
|
|
17587
|
+
},
|
|
17588
|
+
{
|
|
17589
|
+
"name": "children",
|
|
17590
|
+
"type": "ReactNode",
|
|
17591
|
+
"description": "ReactionPicker only: trailing controls after the emoji, such as a button that opens a full emoji picker."
|
|
17592
|
+
},
|
|
17593
|
+
{
|
|
17594
|
+
"name": "aria-label",
|
|
17595
|
+
"type": "string",
|
|
17596
|
+
"default": "\"Reactions\"",
|
|
17597
|
+
"description": "The name of the toolbar or menu group."
|
|
17598
|
+
},
|
|
17599
|
+
{
|
|
17600
|
+
"name": "motion",
|
|
17601
|
+
"type": "\"full\" | \"subtle\" | \"off\"",
|
|
17602
|
+
"default": "\"full\"",
|
|
17603
|
+
"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."
|
|
17604
|
+
}
|
|
17605
|
+
],
|
|
17606
|
+
"subcomponents": [
|
|
17607
|
+
{
|
|
17608
|
+
"name": "MenuReactions",
|
|
17609
|
+
"type": "{ reactions?; selected?; onSelect?; motion? } & ComponentProps<typeof Menu.Group>",
|
|
17610
|
+
"description": "The quick row as checkbox menu items, for the first group of a MenuContent or ContextMenuContent. Same props as ReactionPicker, without children."
|
|
17611
|
+
}
|
|
17612
|
+
],
|
|
17613
|
+
"tokensUsed": [
|
|
17614
|
+
"accent",
|
|
17615
|
+
"primary",
|
|
17616
|
+
"ring"
|
|
17617
|
+
],
|
|
17618
|
+
"motion": [
|
|
17619
|
+
{
|
|
17620
|
+
"slot": "arrival",
|
|
17621
|
+
"recipe": "motion-bloom",
|
|
17622
|
+
"part": "Glyph",
|
|
17623
|
+
"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 — under `subtle` the emoji are simply there."
|
|
17624
|
+
},
|
|
17625
|
+
{
|
|
17626
|
+
"slot": "response",
|
|
17627
|
+
"recipe": "custom",
|
|
17628
|
+
"part": "Glyph",
|
|
17629
|
+
"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`."
|
|
17630
|
+
},
|
|
17631
|
+
{
|
|
17632
|
+
"slot": "transition",
|
|
17633
|
+
"recipe": "motion-interactive",
|
|
17634
|
+
"part": "Cell",
|
|
17635
|
+
"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."
|
|
17636
|
+
},
|
|
17637
|
+
{
|
|
17638
|
+
"slot": "activity",
|
|
17639
|
+
"recipe": "none",
|
|
17640
|
+
"part": "Root",
|
|
17641
|
+
"why": "Choosing is instant; there is no in-between state to show."
|
|
17642
|
+
}
|
|
17643
|
+
],
|
|
17644
|
+
"a11y": [
|
|
17645
|
+
"ReactionPicker is a Base UI toolbar: one tab stop, Left/Right move between emoji, Home/End jump to the ends.",
|
|
17646
|
+
"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.",
|
|
17647
|
+
"MenuReactions renders checkbox menu items (`menuitemcheckbox` with `aria-checked`), reached by the menu's arrow keys and typeahead like every other row.",
|
|
17648
|
+
"Under reduced motion the emoji appear at once and do not magnify; pressed state is carried by `aria-pressed` and a tint, never motion."
|
|
17649
|
+
],
|
|
17650
|
+
"examples": [
|
|
17651
|
+
{
|
|
17652
|
+
"label": "In a popover",
|
|
17653
|
+
"code": "<Popover>\n\t<PopoverTrigger render={<Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Add reaction\" />}>\n\t\t<Icon name=\"emoji\" />\n\t</PopoverTrigger>\n\t<PopoverContent side=\"top\" className=\"w-auto rounded-full p-1\">\n\t\t<ReactionPicker selected={mine} onSelect={toggle} />\n\t</PopoverContent>\n</Popover>",
|
|
17654
|
+
"imports": "import { Button, Icon, Popover, PopoverContent, PopoverTrigger, ReactionPicker } from \"@sableui/react\""
|
|
17655
|
+
},
|
|
17656
|
+
{
|
|
17657
|
+
"label": "In a right-click menu",
|
|
17658
|
+
"code": "<ContextMenu>\n\t<ContextMenuTrigger render={<Bubble />}>See you at 6.</ContextMenuTrigger>\n\t<ContextMenuContent>\n\t\t<MenuReactions selected={mine} onSelect={toggle} />\n\t\t<ContextMenuSeparator />\n\t\t<ContextMenuItem>Reply</ContextMenuItem>\n\t\t<ContextMenuItem>Copy text</ContextMenuItem>\n\t</ContextMenuContent>\n</ContextMenu>",
|
|
17659
|
+
"imports": "import { Bubble, ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, MenuReactions } from \"@sableui/react\""
|
|
17660
|
+
},
|
|
17661
|
+
{
|
|
17662
|
+
"label": "A custom set",
|
|
17663
|
+
"code": "<ReactionPicker\n\treactions={[\n\t\t{ emoji: \"✅\", label: \"Done\" },\n\t\t{ emoji: \"👀\", label: \"Looking\" },\n\t\t{ emoji: \"🚀\", label: \"Shipped\" },\n\t]}\n\tselected={mine}\n\tonSelect={toggle}\n/>",
|
|
17664
|
+
"imports": "import { ReactionPicker } from \"@sableui/react\""
|
|
17665
|
+
}
|
|
17666
|
+
]
|
|
17667
|
+
},
|
|
17238
17668
|
{
|
|
17239
17669
|
"name": "RelativeTime",
|
|
17240
17670
|
"status": "stable",
|
|
@@ -21953,7 +22383,7 @@
|
|
|
21953
22383
|
"name": "type (toast option)",
|
|
21954
22384
|
"type": "\"success\" | \"error\" | \"warning\" | \"info\" | \"loading\"",
|
|
21955
22385
|
"default": "—",
|
|
21956
|
-
"description": "Set by the toast.* helpers. Drives a
|
|
22386
|
+
"description": "Set by the toast.* helpers. Drives a leading status glyph from the shared tone list, the same one Note and InlineStatus use (success→success, error→destructive, warning→warning, info→info), 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."
|
|
21957
22387
|
}
|
|
21958
22388
|
],
|
|
21959
22389
|
"subcomponents": [
|
|
@@ -22028,6 +22458,12 @@
|
|
|
22028
22458
|
"part": "ToastRoot (stacked)",
|
|
22029
22459
|
"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."
|
|
22030
22460
|
},
|
|
22461
|
+
{
|
|
22462
|
+
"slot": "response",
|
|
22463
|
+
"recipe": "motion-bloom",
|
|
22464
|
+
"part": "Status glyph",
|
|
22465
|
+
"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."
|
|
22466
|
+
},
|
|
22031
22467
|
{
|
|
22032
22468
|
"slot": "activity",
|
|
22033
22469
|
"recipe": "none",
|
|
@@ -24111,6 +24547,31 @@
|
|
|
24111
24547
|
"imports": "import { useState } from \"react\"\nimport { Button, Card, CardContent, cn, Entity, Icon, MediaImage, Slider } from \"@sableui/react\"",
|
|
24112
24548
|
"code": "// `cover` is the artwork URL; left empty, the cover paints art seeded by the title.\nconst track = { title: \"Low tide, high noon\", artist: \"Juno Avery\", cover: \"\", duration: 220, position: 84 }\n\n/** Seconds → m:ss, the way the readout and the screen reader both say it. */\nfunction formatTime(seconds: number) {\n\treturn `${Math.floor(seconds / 60)}:${String(Math.floor(seconds % 60)).padStart(2, \"0\")}`\n}\n\n\nexport interface MediaPlayerBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a Sheet or muted section that already frames it. */\n\tframed?: boolean\n\t/** Called with the new playing state. */\n\tonPlayingChange?: (playing: boolean) => void\n\tonPrevious?: () => void\n\tonNext?: () => void\n}\n\nexport function MediaPlayerBlock({ className, framed = true, onPlayingChange, onPrevious, onNext }: MediaPlayerBlockProps) {\n\tconst [playing, setPlaying] = useState(true)\n\tconst [position, setPosition] = useState(track.position)\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-xs\", className)}>\n\t\t\t<CardContent className=\"flex flex-col gap-4\">\n\t\t\t\t{/* Entity's own row padding is for lists; inside a card's content the\n\t\t\t\t card already pads it. */}\n\t\t\t\t<Entity\n\t\t\t\t\tclassName=\"p-0\"\n\t\t\t\t\tleading={<MediaImage src={track.cover} seed={track.title} className=\"size-14 shrink-0 rounded-md\" />}\n\t\t\t\t\ttitle={track.title}\n\t\t\t\t\tsubtitle={track.artist}\n\t\t\t\t\taction={\n\t\t\t\t\t\t<Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Like\">\n\t\t\t\t\t\t\t<Icon name=\"heart\" />\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t\t<div className=\"flex flex-col gap-1.5\">\n\t\t\t\t\t<Slider\n\t\t\t\t\t\tvalue={position}\n\t\t\t\t\t\tmax={track.duration}\n\t\t\t\t\t\tonValueChange={setPosition}\n\t\t\t\t\t\taria-label=\"Position\"\n\t\t\t\t\t\t// Spoken as a time, not \"38\": the number alone means nothing.\n\t\t\t\t\t\tgetAriaValueText={(_, value) => `${formatTime(value)} of ${formatTime(track.duration)}`}\n\t\t\t\t\t/>\n\t\t\t\t\t<div className=\"flex justify-between text-mono-caption text-muted-foreground\">\n\t\t\t\t\t\t<span>{formatTime(position)}</span>\n\t\t\t\t\t\t<span>-{formatTime(track.duration - position)}</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"flex items-center justify-center gap-3\">\n\t\t\t\t\t<Button variant=\"ghost\" size=\"icon\" aria-label=\"Previous\" onClick={onPrevious}>\n\t\t\t\t\t\t<Icon name=\"previous\" variant=\"solid\" />\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\taria-label={playing ? \"Pause\" : \"Play\"}\n\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\tsetPlaying(!playing)\n\t\t\t\t\t\t\tonPlayingChange?.(!playing)\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Icon name={playing ? \"pause\" : \"play\"} variant=\"solid\" />\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button variant=\"ghost\" size=\"icon\" aria-label=\"Next\" onClick={onNext}>\n\t\t\t\t\t\t<Icon name=\"next\" variant=\"solid\" />\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t</CardContent>\n\t\t</Card>\n\t)\n}"
|
|
24113
24549
|
},
|
|
24550
|
+
{
|
|
24551
|
+
"name": "MessageReactionsBlock",
|
|
24552
|
+
"path": "blocks/message-reactions-block.tsx",
|
|
24553
|
+
"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.",
|
|
24554
|
+
"intent": "Reach for this when a conversation excerpt should be reactable — 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.",
|
|
24555
|
+
"family": "Social & messaging",
|
|
24556
|
+
"kind": "card",
|
|
24557
|
+
"width": {
|
|
24558
|
+
"min": "18rem",
|
|
24559
|
+
"max": "24rem"
|
|
24560
|
+
},
|
|
24561
|
+
"notes": [
|
|
24562
|
+
"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.",
|
|
24563
|
+
"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.",
|
|
24564
|
+
"Only the reader's own reaction celebrates. Counts from other people roll quietly, and the reactions a page loads with never play.",
|
|
24565
|
+
"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."
|
|
24566
|
+
],
|
|
24567
|
+
"a11y": [
|
|
24568
|
+
"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.",
|
|
24569
|
+
"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.",
|
|
24570
|
+
"Under reduced motion the reaction appears and the count changes at once; nothing else is lost."
|
|
24571
|
+
],
|
|
24572
|
+
"imports": "import { Bubble, BubbleReaction, BubbleReactionAdd, BubbleReactions, Card, CardContent, CardFooter, cn, ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, DEFAULT_REACTIONS, MenuReactions, Message } from \"@sableui/react\"",
|
|
24573
|
+
"code": "const messages: { id: string; text: string; mine: boolean; reactions: Record<string, number> }[] = [\n\t{ id: \"m1\", text: \"The new onboarding flow is live on staging 🚀\", mine: false, reactions: { \"🎉\": 2, \"👀\": 1 } },\n\t{ id: \"m2\", text: \"Clicked through it twice. The progress bar is so smooth.\", mine: true, reactions: {} },\n\t{ id: \"m3\", text: \"Ship it Friday?\", mine: false, reactions: { \"👍\": 1 } },\n]\nconst hint = \"Right-click or long-press a message to react.\"\n// Every emoji that can appear needs a name: it is the pill's accessible label.\nconst labels: Record<string, string> = {\n\t...Object.fromEntries(DEFAULT_REACTIONS.map((r) => [r.emoji, r.label])),\n\t\"👀\": \"Eyes\",\n}\n\nexport interface MessageReactionsBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border. Keep it framed on a muted page: incoming bubbles are the muted surface too. */\n\tframed?: boolean\n\t/** Called when the reader adds or removes a reaction, with the message, the emoji and whether it is now on. */\n\tonReact?: (messageId: string, emoji: string, on: boolean) => void\n}\n\nexport function MessageReactionsBlock({ className, framed = true, onReact }: MessageReactionsBlockProps) {\n\t// The reader's own reactions, per message. Everyone else's are the counts above.\n\tconst [mine, setMine] = useState<Record<string, string[]>>({ m1: [\"🎉\"] })\n\tconst toggle = (messageId: string, emoji: string) => {\n\t\tconst on = !(mine[messageId] ?? []).includes(emoji)\n\t\t// Decided again from `prev`, so two toggles in one batch cannot add an emoji twice.\n\t\tsetMine((prev) => {\n\t\t\tconst current = prev[messageId] ?? []\n\t\t\tconst next = current.includes(emoji) ? current.filter((e) => e !== emoji) : [...current, emoji]\n\t\t\treturn { ...prev, [messageId]: next }\n\t\t})\n\t\tonReact?.(messageId, emoji, on)\n\t}\n\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-sm\", className)}>\n\t\t\t<CardContent className=\"flex flex-col gap-3\">\n\t\t\t\t{messages.map((m) => {\n\t\t\t\t\tconst align = m.mine ? \"end\" : \"start\"\n\t\t\t\t\tconst chosen = mine[m.id] ?? []\n\t\t\t\t\t// Others' counts, plus the reader's own: a reaction only the reader gave\n\t\t\t\t\t// is a pill of its own, ordered after the ones already there.\n\t\t\t\t\tconst counts: Record<string, number> = { ...m.reactions }\n\t\t\t\t\tfor (const emoji of chosen) counts[emoji] = (counts[emoji] ?? 0)\n\t\t\t\t\tconst pills = Object.entries(counts).map(([emoji, others]) => ({\n\t\t\t\t\t\temoji,\n\t\t\t\t\t\tactive: chosen.includes(emoji),\n\t\t\t\t\t\tcount: others + (chosen.includes(emoji) ? 1 : 0),\n\t\t\t\t\t}))\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<Message key={m.id} messageId={m.id} align={m.mine ? \"end\" : undefined}>\n\t\t\t\t\t\t\t<ContextMenu>\n\t\t\t\t\t\t\t\t<ContextMenuTrigger\n\t\t\t\t\t\t\t\t\t// Focusable, so Shift+F10 or the Menu key opens the same menu.\n\t\t\t\t\t\t\t\t\trender={<Bubble tabIndex={0} variant={m.mine ? \"primary\" : undefined} align={align} />}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{m.text}\n\t\t\t\t\t\t\t\t</ContextMenuTrigger>\n\t\t\t\t\t\t\t\t<ContextMenuContent>\n\t\t\t\t\t\t\t\t\t<MenuReactions selected={chosen} onSelect={(emoji) => toggle(m.id, emoji)} />\n\t\t\t\t\t\t\t\t\t<ContextMenuSeparator />\n\t\t\t\t\t\t\t\t\t<ContextMenuItem>Reply</ContextMenuItem>\n\t\t\t\t\t\t\t\t\t<ContextMenuItem>Copy text</ContextMenuItem>\n\t\t\t\t\t\t\t\t</ContextMenuContent>\n\t\t\t\t\t\t\t</ContextMenu>\n\t\t\t\t\t\t\t{/* Rendered even while empty (it hides itself), so the first reaction still celebrates. */}\n\t\t\t\t\t\t\t<BubbleReactions align={align}>\n\t\t\t\t\t\t\t\t{pills.map((p) => (\n\t\t\t\t\t\t\t\t\t<BubbleReaction\n\t\t\t\t\t\t\t\t\t\tkey={p.emoji}\n\t\t\t\t\t\t\t\t\t\temoji={p.emoji}\n\t\t\t\t\t\t\t\t\t\tlabel={labels[p.emoji]}\n\t\t\t\t\t\t\t\t\t\tcount={p.count}\n\t\t\t\t\t\t\t\t\t\tactive={p.active}\n\t\t\t\t\t\t\t\t\t\tonClick={() => toggle(m.id, p.emoji)}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t{pills.length > 0 ? (\n\t\t\t\t\t\t\t\t\t<BubbleReactionAdd selected={chosen} onSelect={(emoji) => toggle(m.id, emoji)} />\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t</BubbleReactions>\n\t\t\t\t\t\t</Message>\n\t\t\t\t\t)\n\t\t\t\t})}\n\t\t\t</CardContent>\n\t\t\t<CardFooter className=\"text-caption text-muted-foreground\">{hint}</CardFooter>\n\t\t</Card>\n\t)\n}"
|
|
24574
|
+
},
|
|
24114
24575
|
{
|
|
24115
24576
|
"name": "NewsletterBlock",
|
|
24116
24577
|
"path": "blocks/newsletter-block.tsx",
|
|
@@ -25363,6 +25824,16 @@
|
|
|
25363
25824
|
"value": "base · emphasized · expressive",
|
|
25364
25825
|
"use": "An element easing between intrinsic sizes when its label changes: a two-step confirm, Copy → Copied."
|
|
25365
25826
|
},
|
|
25827
|
+
{
|
|
25828
|
+
"name": "motion-bloom",
|
|
25829
|
+
"value": "slower · 0 → 1.2 → 1 · expressive",
|
|
25830
|
+
"use": "A small glyph landing in its on state: a liked heart, a picked rating, a toast's status icon. Remount it (key) to replay; never on the resting state."
|
|
25831
|
+
},
|
|
25832
|
+
{
|
|
25833
|
+
"name": "<Burst>",
|
|
25834
|
+
"value": "slower ring + thrown dots · expressive",
|
|
25835
|
+
"use": "The follow-through behind a bloom, in the theme's palette families. Only on a choice a person made: a like, a star, a finished task."
|
|
25836
|
+
},
|
|
25366
25837
|
{
|
|
25367
25838
|
"name": "motion-draw",
|
|
25368
25839
|
"value": "slower · decelerate",
|
|
@@ -25417,7 +25888,7 @@
|
|
|
25417
25888
|
{
|
|
25418
25889
|
"name": "expressive",
|
|
25419
25890
|
"value": "full only",
|
|
25420
|
-
"use": "Glides, rolling numbers (NumberTicker), drawn ticks, shakes, staggers, chart arrival and morph, scroll reveals, spring overshoot."
|
|
25891
|
+
"use": "Glides, rolling numbers (NumberTicker), drawn ticks, blooms and bursts, shakes, staggers, chart arrival and morph, scroll reveals, spring overshoot."
|
|
25421
25892
|
},
|
|
25422
25893
|
{
|
|
25423
25894
|
"name": "full",
|
|
@@ -25512,6 +25983,18 @@
|
|
|
25512
25983
|
"rule": "A state change finishes its story: refused shakes and says why, success confirms in place, armed asks again.",
|
|
25513
25984
|
"why": "Motion that starts a change and leaves the outcome to a snap reads as unfinished. ConfirmButton, OtpField's invalid shake and the copy-to-check swap are the reference cases."
|
|
25514
25985
|
},
|
|
25986
|
+
{
|
|
25987
|
+
"rule": "Amplitude scales with smallness. A 16px glyph that turns on grows from nothing past its size; a surface travels from 0.96 and a press settles to 0.97.",
|
|
25988
|
+
"why": "The same 4% that is right for a dialog is invisible on an icon, which is why a state change on a small control used to read as a colour swap with no life in it. A dialog that overshot 20% would lurch. Size, not importance, sets how far a thing moves."
|
|
25989
|
+
},
|
|
25990
|
+
{
|
|
25991
|
+
"rule": "Stage a moment: the main object lands first, the follow-through after it.",
|
|
25992
|
+
"why": "A ring that opens, a glyph that blooms 90ms later, dots thrown 60ms after that read as one event with a cause and an effect. Everything starting on the same frame reads as a flash. LikeButton and the toast's status glyph are the reference cases."
|
|
25993
|
+
},
|
|
25994
|
+
{
|
|
25995
|
+
"rule": "Celebrate only what a person chose, and only at full motion.",
|
|
25996
|
+
"why": "A burst is a reply. On a like it is delight; on a system event, on load, or on every row of a list it is a notification wearing confetti. Under subtle the fill alone answers."
|
|
25997
|
+
},
|
|
25515
25998
|
{
|
|
25516
25999
|
"rule": "Component motion props and MotionScope only turn motion down.",
|
|
25517
26000
|
"why": "`full` on a component means inherit. Nothing below the provider can switch motion back on for a reader who asked for it off."
|
|
@@ -26692,6 +27175,29 @@
|
|
|
26692
27175
|
"motion-interactive"
|
|
26693
27176
|
]
|
|
26694
27177
|
},
|
|
27178
|
+
{
|
|
27179
|
+
"name": "motion-bloom",
|
|
27180
|
+
"kind": "utility",
|
|
27181
|
+
"family": "motion",
|
|
27182
|
+
"intent": "A small glyph landing in its \"on\" state grows from nothing past its size (1.2) and settles. The amplitude a 16px glyph needs to read as an event; surfaces never bloom. A keyframe that plays on mount, so remount the glyph (a changing `key`) to replay it. `--motion-bloom-delay` holds it at nothing first (raw ms; the recipe scales it). Expressive.",
|
|
27183
|
+
"whenToUse": "A glyph that a person's choice turns on: a liked heart, a starred item, a status icon arriving in a toast. LikeButton and Toast already use it. Pair with <Burst> for the follow-through on a celebratory toggle.",
|
|
27184
|
+
"insteadOf": "A bigger motion-press or motion-pop on the whole control (a button or card that overshoots 20% lurches), or a hand-written scale keyframe with literal durations. Never on the resting state: a page that loads with a post already liked must not bloom every heart.",
|
|
27185
|
+
"example": "<span key={likes} className={liked ? \"motion-bloom\" : undefined}><Icon name=\"heart\" variant={liked ? \"solid\" : \"regular\"} /></span>",
|
|
27186
|
+
"keywords": [
|
|
27187
|
+
"pop",
|
|
27188
|
+
"overshoot",
|
|
27189
|
+
"like",
|
|
27190
|
+
"heart",
|
|
27191
|
+
"star",
|
|
27192
|
+
"celebrate",
|
|
27193
|
+
"glyph",
|
|
27194
|
+
"icon pop",
|
|
27195
|
+
"delight"
|
|
27196
|
+
],
|
|
27197
|
+
"pairsWith": [
|
|
27198
|
+
"motion-interactive"
|
|
27199
|
+
]
|
|
27200
|
+
},
|
|
26695
27201
|
{
|
|
26696
27202
|
"name": "motion-size",
|
|
26697
27203
|
"kind": "utility",
|
|
@@ -27339,11 +27845,29 @@
|
|
|
27339
27845
|
"BubbleReaction": [
|
|
27340
27846
|
"active",
|
|
27341
27847
|
"count",
|
|
27342
|
-
"emoji"
|
|
27848
|
+
"emoji",
|
|
27849
|
+
"label",
|
|
27850
|
+
"motion"
|
|
27851
|
+
],
|
|
27852
|
+
"BubbleReactionAdd": [
|
|
27853
|
+
"label",
|
|
27854
|
+
"motion",
|
|
27855
|
+
"onSelect",
|
|
27856
|
+
"reactions",
|
|
27857
|
+
"selected"
|
|
27343
27858
|
],
|
|
27344
27859
|
"BubbleReactions": [
|
|
27345
27860
|
"align"
|
|
27346
27861
|
],
|
|
27862
|
+
"Burst": [
|
|
27863
|
+
"className",
|
|
27864
|
+
"count",
|
|
27865
|
+
"motion",
|
|
27866
|
+
"palette",
|
|
27867
|
+
"particle",
|
|
27868
|
+
"playOnMount",
|
|
27869
|
+
"trigger"
|
|
27870
|
+
],
|
|
27347
27871
|
"Button": [
|
|
27348
27872
|
"badge",
|
|
27349
27873
|
"bleed",
|
|
@@ -28261,6 +28785,21 @@
|
|
|
28261
28785
|
"hint",
|
|
28262
28786
|
"size"
|
|
28263
28787
|
],
|
|
28788
|
+
"LikeButton": [
|
|
28789
|
+
"bleed",
|
|
28790
|
+
"count",
|
|
28791
|
+
"defaultPressed",
|
|
28792
|
+
"format",
|
|
28793
|
+
"icon",
|
|
28794
|
+
"label",
|
|
28795
|
+
"motion",
|
|
28796
|
+
"onClick",
|
|
28797
|
+
"onPressedChange",
|
|
28798
|
+
"pressed",
|
|
28799
|
+
"size",
|
|
28800
|
+
"tone",
|
|
28801
|
+
"variant"
|
|
28802
|
+
],
|
|
28264
28803
|
"LineChart": [
|
|
28265
28804
|
"activeIndex",
|
|
28266
28805
|
"animate",
|
|
@@ -28462,6 +29001,15 @@
|
|
|
28462
29001
|
"style",
|
|
28463
29002
|
"value"
|
|
28464
29003
|
],
|
|
29004
|
+
"MenuReactions": [
|
|
29005
|
+
"className",
|
|
29006
|
+
"motion",
|
|
29007
|
+
"onSelect",
|
|
29008
|
+
"reactions",
|
|
29009
|
+
"render",
|
|
29010
|
+
"selected",
|
|
29011
|
+
"style"
|
|
29012
|
+
],
|
|
28465
29013
|
"MenuSeparator": [
|
|
28466
29014
|
"className",
|
|
28467
29015
|
"orientation",
|
|
@@ -29015,6 +29563,19 @@
|
|
|
29015
29563
|
"style",
|
|
29016
29564
|
"value"
|
|
29017
29565
|
],
|
|
29566
|
+
"ReactionPicker": [
|
|
29567
|
+
"children",
|
|
29568
|
+
"className",
|
|
29569
|
+
"disabled",
|
|
29570
|
+
"loopFocus",
|
|
29571
|
+
"motion",
|
|
29572
|
+
"onSelect",
|
|
29573
|
+
"orientation",
|
|
29574
|
+
"reactions",
|
|
29575
|
+
"render",
|
|
29576
|
+
"selected",
|
|
29577
|
+
"style"
|
|
29578
|
+
],
|
|
29018
29579
|
"Region": [
|
|
29019
29580
|
"as",
|
|
29020
29581
|
"bleed",
|