@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
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/components/feedback/feedback.tsx
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
useEffect,
|
|
6
|
+
useId,
|
|
7
|
+
useLayoutEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useState
|
|
10
|
+
} from "react";
|
|
11
|
+
import { Icon } from "../../icon/index.js";
|
|
5
12
|
import { cn } from "../../lib/cn.js";
|
|
13
|
+
import { motionAttribute } from "../../motion/MotionProvider.js";
|
|
6
14
|
import { Button } from "../button/index.js";
|
|
7
15
|
import { Popover, PopoverContent, PopoverTrigger } from "../popover/index.js";
|
|
8
16
|
import { Textarea } from "../textarea/index.js";
|
|
@@ -13,79 +21,144 @@ var DEFAULT_RATINGS = [
|
|
|
13
21
|
{ value: "ok", label: "\u{1F610}" },
|
|
14
22
|
{ value: "good", label: "\u{1F642}" }
|
|
15
23
|
];
|
|
24
|
+
var SUCCESS_DWELL_MS = 1200;
|
|
16
25
|
function Feedback({
|
|
17
26
|
label = "Feedback",
|
|
18
27
|
heading = "Send feedback",
|
|
19
28
|
placeholder = "What can we improve?",
|
|
20
29
|
submitLabel = "Send",
|
|
21
30
|
ratings = DEFAULT_RATINGS,
|
|
31
|
+
successMessage = "Thanks for the feedback.",
|
|
22
32
|
onSubmit,
|
|
33
|
+
motion,
|
|
23
34
|
className,
|
|
24
35
|
...props
|
|
25
36
|
}) {
|
|
26
37
|
const [open, setOpen] = useState(false);
|
|
27
38
|
const [rating, setRating] = useState(void 0);
|
|
28
39
|
const [message, setMessage] = useState("");
|
|
40
|
+
const [sent, setSent] = useState(false);
|
|
41
|
+
const [drawn, setDrawn] = useState(false);
|
|
42
|
+
const thanksRef = useRef(null);
|
|
29
43
|
const headingId = useId();
|
|
30
44
|
function reset() {
|
|
31
45
|
setRating(void 0);
|
|
32
46
|
setMessage("");
|
|
47
|
+
setSent(false);
|
|
48
|
+
setDrawn(false);
|
|
33
49
|
}
|
|
34
50
|
function handleSubmit(event) {
|
|
35
51
|
event.preventDefault();
|
|
36
|
-
if (!rating && message.trim() === "") return;
|
|
52
|
+
if (sent || !rating && message.trim() === "") return;
|
|
37
53
|
onSubmit?.({ rating, message });
|
|
38
|
-
|
|
39
|
-
setOpen(false);
|
|
54
|
+
setSent(true);
|
|
40
55
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
useLayoutEffect(() => {
|
|
57
|
+
if (sent) thanksRef.current?.focus({ preventScroll: true });
|
|
58
|
+
}, [sent]);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!sent) return;
|
|
61
|
+
let frame = requestAnimationFrame(() => {
|
|
62
|
+
frame = requestAnimationFrame(() => setDrawn(true));
|
|
63
|
+
});
|
|
64
|
+
const timer = setTimeout(() => setOpen(false), SUCCESS_DWELL_MS);
|
|
65
|
+
return () => {
|
|
66
|
+
cancelAnimationFrame(frame);
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
};
|
|
69
|
+
}, [sent]);
|
|
70
|
+
return /* @__PURE__ */ jsxs(
|
|
71
|
+
Popover,
|
|
72
|
+
{
|
|
73
|
+
open,
|
|
74
|
+
onOpenChange: setOpen,
|
|
75
|
+
onOpenChangeComplete: (isOpen) => {
|
|
76
|
+
if (!isOpen) reset();
|
|
77
|
+
},
|
|
78
|
+
children: [
|
|
79
|
+
/* @__PURE__ */ jsx(
|
|
80
|
+
PopoverTrigger,
|
|
81
|
+
{
|
|
82
|
+
render: /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", children: label })
|
|
83
|
+
}
|
|
84
|
+
),
|
|
85
|
+
/* @__PURE__ */ jsxs(PopoverContent, { className: "grid w-80", "data-motion": motionAttribute(motion), children: [
|
|
86
|
+
/* @__PURE__ */ jsxs(
|
|
87
|
+
"form",
|
|
59
88
|
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
|
|
89
|
+
className: cn(
|
|
90
|
+
"col-start-1 row-start-1 flex flex-col gap-3 transition-opacity duration-(--motion-duration-fast) ease-standard",
|
|
91
|
+
sent && "opacity-0",
|
|
92
|
+
className
|
|
93
|
+
),
|
|
94
|
+
"aria-labelledby": headingId,
|
|
95
|
+
inert: sent,
|
|
96
|
+
onSubmit: handleSubmit,
|
|
97
|
+
...props,
|
|
98
|
+
children: [
|
|
99
|
+
/* @__PURE__ */ jsx("p", { id: headingId, className: "text-title-sm text-popover-foreground", children: heading }),
|
|
100
|
+
/* @__PURE__ */ jsx(
|
|
101
|
+
ToggleGroup,
|
|
102
|
+
{
|
|
103
|
+
value: rating ? [rating] : [],
|
|
104
|
+
onValueChange: (value) => setRating(value[0]),
|
|
105
|
+
"aria-label": "Rating",
|
|
106
|
+
children: ratings.map((option) => /* @__PURE__ */ jsx(
|
|
107
|
+
Toggle,
|
|
108
|
+
{
|
|
109
|
+
value: option.value,
|
|
110
|
+
"aria-label": option.value,
|
|
111
|
+
className: "flex-1 text-base",
|
|
112
|
+
children: /* @__PURE__ */ jsx(
|
|
113
|
+
"span",
|
|
114
|
+
{
|
|
115
|
+
className: cn("inline-flex", rating === option.value && "motion-bloom"),
|
|
116
|
+
children: option.label
|
|
117
|
+
},
|
|
118
|
+
rating === option.value ? "chosen" : "rest"
|
|
119
|
+
)
|
|
120
|
+
},
|
|
121
|
+
option.value
|
|
122
|
+
))
|
|
123
|
+
}
|
|
124
|
+
),
|
|
125
|
+
/* @__PURE__ */ jsx(
|
|
126
|
+
Textarea,
|
|
127
|
+
{
|
|
128
|
+
value: message,
|
|
129
|
+
onChange: (event) => setMessage(event.target.value),
|
|
130
|
+
placeholder,
|
|
131
|
+
"aria-label": placeholder
|
|
132
|
+
}
|
|
133
|
+
),
|
|
134
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center justify-end gap-2", children: /* @__PURE__ */ jsx(Button, { type: "submit", size: "sm", disabled: !rating && message.trim() === "", children: submitLabel }) })
|
|
135
|
+
]
|
|
73
136
|
}
|
|
74
137
|
),
|
|
75
|
-
/* @__PURE__ */
|
|
76
|
-
|
|
138
|
+
sent ? /* @__PURE__ */ jsxs(
|
|
139
|
+
"div",
|
|
77
140
|
{
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"
|
|
141
|
+
ref: thanksRef,
|
|
142
|
+
tabIndex: -1,
|
|
143
|
+
"data-slot": "feedback-thanks",
|
|
144
|
+
className: "col-start-1 row-start-1 flex flex-col items-center justify-center gap-2 text-center outline-none transition-opacity duration-(--motion-duration-base) ease-standard starting:opacity-0",
|
|
145
|
+
children: [
|
|
146
|
+
/* @__PURE__ */ jsx(
|
|
147
|
+
"span",
|
|
148
|
+
{
|
|
149
|
+
"aria-hidden": "true",
|
|
150
|
+
className: "inline-flex size-10 items-center justify-center rounded-full bg-success/15 text-2xl text-success",
|
|
151
|
+
children: /* @__PURE__ */ jsx("span", { "data-starting-style": drawn ? void 0 : "", className: "inline-flex motion-tick", children: /* @__PURE__ */ jsx(Icon, { name: "check" }) })
|
|
152
|
+
}
|
|
153
|
+
),
|
|
154
|
+
/* @__PURE__ */ jsx("p", { className: "text-body-md text-popover-foreground", children: successMessage })
|
|
155
|
+
]
|
|
82
156
|
}
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
] });
|
|
157
|
+
) : null
|
|
158
|
+
] })
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
);
|
|
89
162
|
}
|
|
90
163
|
Feedback.displayName = "Feedback";
|
|
91
164
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/components/feedback/feedback.meta.ts
|
|
2
2
|
var feedbackMeta = {
|
|
3
3
|
name: "Feedback",
|
|
4
|
-
summary: "A compact feedback popover. Composes Popover + Textarea + Button + Toggle into a rating row plus a message field; submitting reports the rating + message and closes the popover.",
|
|
4
|
+
summary: "A compact feedback popover. Composes Popover + Textarea + Button + Toggle into a rating row plus a message field; submitting reports the rating + message, shows a brief drawn-check thanks, and closes the popover.",
|
|
5
5
|
status: "stable",
|
|
6
6
|
composition: "Popover, Textarea, ToggleGroup, Button",
|
|
7
7
|
keywords: [
|
|
@@ -72,7 +72,19 @@ var feedbackMeta = {
|
|
|
72
72
|
{
|
|
73
73
|
name: "onSubmit",
|
|
74
74
|
type: "(data: { rating?: string; message: string }) => void",
|
|
75
|
-
description: "Called with the selected rating + message when the form is submitted."
|
|
75
|
+
description: "Called with the selected rating + message when the form is submitted. The thanks shows right away; send the report in the background."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "successMessage",
|
|
79
|
+
type: "string",
|
|
80
|
+
default: '"Thanks for the feedback."',
|
|
81
|
+
description: "The line shown, and read out, for about 1.2s after sending, before the popover closes on its own."
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "motion",
|
|
85
|
+
type: '"full" | "subtle" | "off"',
|
|
86
|
+
default: '"full"',
|
|
87
|
+
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 drawn check is the flourish; the thanks line and its dwell stay."
|
|
76
88
|
}
|
|
77
89
|
],
|
|
78
90
|
tokensUsed: [
|
|
@@ -85,14 +97,49 @@ var feedbackMeta = {
|
|
|
85
97
|
"muted-foreground",
|
|
86
98
|
"accent",
|
|
87
99
|
"accent-foreground",
|
|
88
|
-
"ring"
|
|
100
|
+
"ring",
|
|
101
|
+
"success"
|
|
102
|
+
],
|
|
103
|
+
motion: [
|
|
104
|
+
{
|
|
105
|
+
slot: "arrival",
|
|
106
|
+
recipe: "motion-pop",
|
|
107
|
+
part: "Popover",
|
|
108
|
+
why: "The form opens from its trigger, so the eye goes from the button it pressed to the field it should type in. Popover owns the recipe."
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
slot: "response",
|
|
112
|
+
recipe: "motion-interactive",
|
|
113
|
+
part: "Rating toggles",
|
|
114
|
+
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."
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
slot: "response",
|
|
118
|
+
recipe: "motion-bloom",
|
|
119
|
+
part: "Chosen rating mark",
|
|
120
|
+
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 \u2014 nothing blooms when the form opens or a pick is let go. Expressive: under subtle the fill alone answers."
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
slot: "transition",
|
|
124
|
+
recipe: "motion-tick",
|
|
125
|
+
part: "Thanks",
|
|
126
|
+
why: "Sending is the one moment the person needs to know their words arrived. The form fades out in place and a check draws itself in over it, then the popover closes. Without it the popover just vanished, which looks the same as the form being dismissed or the message lost."
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
slot: "activity",
|
|
130
|
+
recipe: "none",
|
|
131
|
+
part: "Root",
|
|
132
|
+
why: "The thanks shows as soon as the form is sent; delivery is the app's background work. A spinner for a one-line report would make a quick, optional gesture feel heavy."
|
|
133
|
+
}
|
|
89
134
|
],
|
|
90
135
|
a11y: [
|
|
91
136
|
"Trigger opens the popover on press; Base UI manages focus into the form and restores it to the trigger on close.",
|
|
92
137
|
"The rating row is a Base UI ToggleGroup with arrow-key roving focus; each toggle carries an aria-label.",
|
|
93
138
|
'The form is named by its heading (aria-labelledby), so it is announced as "Send feedback" rather than an unnamed form.',
|
|
94
139
|
"The message field carries an aria-label matching its placeholder.",
|
|
95
|
-
"Submit is disabled until there is a rating or a non-whitespace message. Either alone is a complete answer."
|
|
140
|
+
"Submit is disabled until there is a rating or a non-whitespace message. Either alone is a complete answer.",
|
|
141
|
+
"On send, focus moves to the thanks line (tabIndex=-1), which reads it out, and the form becomes inert so focus cannot land in invisible fields. The line is not also a live region, which read it twice. When the popover closes, Base UI returns focus to the trigger.",
|
|
142
|
+
"The thanks dwell (about 1.2s) is a reading time, not motion, so it is the same under reduced motion."
|
|
96
143
|
],
|
|
97
144
|
examples: [
|
|
98
145
|
{
|
|
@@ -102,6 +149,10 @@ var feedbackMeta = {
|
|
|
102
149
|
{
|
|
103
150
|
label: "Custom labels",
|
|
104
151
|
code: '<Feedback label="Tell us" submitLabel="Send it" placeholder="Describe the issue\u2026" />'
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
label: "Custom thanks",
|
|
155
|
+
code: '<Feedback successMessage="Got it \u2014 thank you." onSubmit={({ rating, message }) => report(rating, message)} />'
|
|
105
156
|
}
|
|
106
157
|
]
|
|
107
158
|
};
|
|
@@ -76,6 +76,32 @@ var inputGroupMeta = {
|
|
|
76
76
|
}
|
|
77
77
|
],
|
|
78
78
|
tokensUsed: ["input", "card", "ring", "destructive", "muted-foreground", "foreground"],
|
|
79
|
+
motion: [
|
|
80
|
+
{
|
|
81
|
+
slot: "arrival",
|
|
82
|
+
recipe: "none",
|
|
83
|
+
part: "Root",
|
|
84
|
+
why: "It arrives with the form it belongs to. A field that animated in on its own would be briefly unusable for no stated reason."
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
slot: "response",
|
|
88
|
+
recipe: "motion-interactive",
|
|
89
|
+
part: "Root",
|
|
90
|
+
why: "The shell owns one focus ring for the input and its addons and eases it in, so focusing the text, or tabbing to a copy button inside the field, reads as the one field taking focus."
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
slot: "transition",
|
|
94
|
+
recipe: "motion-interactive",
|
|
95
|
+
part: "Root",
|
|
96
|
+
why: "An invalid value turns the shell's edge destructive on the same ease, so the error lands as the field changing state rather than a red box swapping in."
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
slot: "activity",
|
|
100
|
+
recipe: "none",
|
|
101
|
+
part: "Root",
|
|
102
|
+
why: "A field does no work of its own. A pending check (an availability lookup) puts a Spinner in `trailing`, which is where its answer will appear."
|
|
103
|
+
}
|
|
104
|
+
],
|
|
79
105
|
a11y: [
|
|
80
106
|
"The Input stays the focusable control; the group draws the focus ring around the whole field while it is focused.",
|
|
81
107
|
"Inside a Field, the label, description and error are wired to the Input itself, not the group.",
|
|
@@ -93,7 +119,7 @@ var inputGroupMeta = {
|
|
|
93
119
|
},
|
|
94
120
|
{
|
|
95
121
|
label: "Copy button",
|
|
96
|
-
code: '<Field label="API key"><InputGroup trailing={<Button variant="ghost" size="icon-sm" aria-label="Copy key" onClick={copy}><
|
|
122
|
+
code: '<Field label="API key"><InputGroup trailing={<Button variant="ghost" size="icon-sm" aria-label="Copy key" onClick={copy}><span aria-hidden="true" className="motion-icon-swap"><span data-active={!copied} className="inline-flex"><Icon name="copy" /></span><span data-active={copied} className="inline-flex"><Icon name="check" /></span></span></Button>}><Input readOnly value={key} /></InputGroup></Field>'
|
|
97
123
|
},
|
|
98
124
|
{
|
|
99
125
|
label: "Password with reveal",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/components/like-button/like-button.tsx
|
|
4
|
+
import { useMemo, useState } from "react";
|
|
5
|
+
import { Icon } from "../../icon/index.js";
|
|
6
|
+
import { cn } from "../../lib/cn.js";
|
|
7
|
+
import { Burst } from "../../motion/Burst.js";
|
|
8
|
+
import { motionAttribute } from "../../motion/MotionProvider.js";
|
|
9
|
+
import { Button } from "../button/button.js";
|
|
10
|
+
import { NumberTicker } from "../number-ticker/number-ticker.js";
|
|
11
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
var ICON_SIZE = { xs: "icon-xs", sm: "icon-sm", md: "icon" };
|
|
13
|
+
function LikeButton({
|
|
14
|
+
className,
|
|
15
|
+
count,
|
|
16
|
+
format,
|
|
17
|
+
icon = "heart",
|
|
18
|
+
tone = "red",
|
|
19
|
+
label = "Like",
|
|
20
|
+
variant = "ghost",
|
|
21
|
+
size = "sm",
|
|
22
|
+
motion,
|
|
23
|
+
pressed: pressedProp,
|
|
24
|
+
defaultPressed = false,
|
|
25
|
+
onPressedChange,
|
|
26
|
+
onClick,
|
|
27
|
+
...props
|
|
28
|
+
}) {
|
|
29
|
+
const [innerPressed, setInnerPressed] = useState(defaultPressed);
|
|
30
|
+
const pressed = pressedProp ?? innerPressed;
|
|
31
|
+
const [baseline, setBaseline] = useState({ count, pressed });
|
|
32
|
+
if (!Object.is(count, baseline.count)) setBaseline({ count, pressed });
|
|
33
|
+
const shown = count === void 0 ? void 0 : count + Number(pressed) - Number(baseline.pressed);
|
|
34
|
+
const [likes, setLikes] = useState(0);
|
|
35
|
+
const [armed, setArmed] = useState(false);
|
|
36
|
+
if (armed) {
|
|
37
|
+
setArmed(false);
|
|
38
|
+
if (pressed) setLikes((n) => n + 1);
|
|
39
|
+
}
|
|
40
|
+
const formatter = useMemo(() => new Intl.NumberFormat(void 0, format), [format]);
|
|
41
|
+
return /* @__PURE__ */ jsxs(
|
|
42
|
+
Button,
|
|
43
|
+
{
|
|
44
|
+
...props,
|
|
45
|
+
variant,
|
|
46
|
+
size: shown === void 0 ? ICON_SIZE[size] : size,
|
|
47
|
+
className: cn("tabular-nums", className),
|
|
48
|
+
"aria-pressed": pressed,
|
|
49
|
+
"data-pressed": pressed ? "" : void 0,
|
|
50
|
+
onClick: (event) => {
|
|
51
|
+
onClick?.(event);
|
|
52
|
+
if (event.defaultPrevented) return;
|
|
53
|
+
const next = !pressed;
|
|
54
|
+
if (pressedProp === void 0) setInnerPressed(next);
|
|
55
|
+
if (next) setArmed(true);
|
|
56
|
+
onPressedChange?.(next, event);
|
|
57
|
+
},
|
|
58
|
+
"aria-label": props["aria-label"] ?? (shown === void 0 ? label : `${label} ${formatter.format(shown)}`),
|
|
59
|
+
"data-motion": motionAttribute(motion),
|
|
60
|
+
children: [
|
|
61
|
+
/* @__PURE__ */ jsxs("span", { className: "relative inline-flex", children: [
|
|
62
|
+
/* @__PURE__ */ jsx(
|
|
63
|
+
"span",
|
|
64
|
+
{
|
|
65
|
+
style: { "--motion-bloom-delay": "90ms" },
|
|
66
|
+
className: cn("inline-flex", pressed && likes > 0 && "motion-bloom"),
|
|
67
|
+
"data-palette": pressed ? tone : void 0,
|
|
68
|
+
children: /* @__PURE__ */ jsx(
|
|
69
|
+
Icon,
|
|
70
|
+
{
|
|
71
|
+
name: icon,
|
|
72
|
+
variant: pressed ? "solid" : "regular",
|
|
73
|
+
className: cn("size-(--button-icon)", pressed && "text-palette-dot")
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
},
|
|
77
|
+
likes
|
|
78
|
+
),
|
|
79
|
+
/* @__PURE__ */ jsx("span", { "data-palette": tone, className: "contents text-palette-dot", children: /* @__PURE__ */ jsx(Burst, { trigger: likes, motion, className: "[--burst-size:calc(var(--button-icon)*2.25)]" }) })
|
|
80
|
+
] }),
|
|
81
|
+
shown === void 0 ? null : /* @__PURE__ */ jsx(NumberTicker, { value: shown, format, motion })
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
LikeButton.displayName = "LikeButton";
|
|
87
|
+
export {
|
|
88
|
+
LikeButton
|
|
89
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -115,11 +115,18 @@ var lineChartMeta = {
|
|
|
115
115
|
default: "false for columns; true for horizontal bars",
|
|
116
116
|
description: "The vertical axis."
|
|
117
117
|
},
|
|
118
|
+
{
|
|
119
|
+
name: "motion",
|
|
120
|
+
type: '"full" | "subtle" | "off"',
|
|
121
|
+
default: '"full"',
|
|
122
|
+
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 arrival (lines drawing on) and the data-change morph are the flourish; the gliding cursor, dots and tooltip are the feedback `subtle` keeps."
|
|
123
|
+
},
|
|
118
124
|
{
|
|
119
125
|
name: "animate",
|
|
120
126
|
type: "boolean",
|
|
121
127
|
default: "true",
|
|
122
|
-
description:
|
|
128
|
+
description: 'Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart. `animate={false}` is `motion="subtle"`.',
|
|
129
|
+
deprecated: 'motion="subtle" (what animate={false} now maps to: no arrival, hover kept) or motion="off"'
|
|
123
130
|
},
|
|
124
131
|
{
|
|
125
132
|
name: "activeIndex",
|
|
@@ -180,7 +187,7 @@ var lineChartMeta = {
|
|
|
180
187
|
slot: "arrival",
|
|
181
188
|
recipe: "custom",
|
|
182
189
|
part: "Line",
|
|
183
|
-
why:
|
|
190
|
+
why: 'Once on screen each line draws on from the first point (stroke-dashoffset), series staggered, then the dots and labels fade in. On the motion tokens; reduced motion shows the finished chart. The arrival is expressive: it plays at the full motion level only, so `motion="subtle"` (or a subtle app policy) shows the finished chart while hover feedback stays.'
|
|
184
191
|
},
|
|
185
192
|
{
|
|
186
193
|
slot: "response",
|
|
@@ -192,7 +199,7 @@ var lineChartMeta = {
|
|
|
192
199
|
slot: "transition",
|
|
193
200
|
recipe: "custom",
|
|
194
201
|
part: "Line + dots",
|
|
195
|
-
why: "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place."
|
|
202
|
+
why: "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place. Expressive, like the arrival: under `subtle` new data swaps in place."
|
|
196
203
|
},
|
|
197
204
|
{
|
|
198
205
|
slot: "activity",
|
|
@@ -68,7 +68,7 @@ var middleTruncateMeta = {
|
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
label: "Copyable value",
|
|
71
|
-
code: 'const { copy, copied } = useCopy(commitHash)\n<Row gap={1}><MiddleTruncate text={commitHash} /><Button variant="ghost" size="icon-xs" aria-label="Copy commit hash" onClick={copy}><Icon name={copied
|
|
71
|
+
code: 'const { copy, copied } = useCopy(commitHash)\n<Row gap={1}><MiddleTruncate text={commitHash} /><Button variant="ghost" size="icon-xs" aria-label="Copy commit hash" onClick={copy}><span aria-hidden="true" className="motion-icon-swap"><span data-active={!copied} className="inline-flex"><Icon name="copy" /></span><span data-active={copied} className="inline-flex"><Icon name="check" /></span></span></Button><span role="status" className="sr-only">{copied ? "Copied" : ""}</span></Row>'
|
|
72
72
|
}
|
|
73
73
|
]
|
|
74
74
|
};
|