@zyncat/ui 0.8.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/LICENSE +21 -0
- package/README.md +133 -0
- package/dist/alert.css +132 -0
- package/dist/alert.d.ts +46 -0
- package/dist/alert.js +80 -0
- package/dist/alert.js.map +1 -0
- package/dist/avatar-group.d.ts +24 -0
- package/dist/avatar-group.js +23 -0
- package/dist/avatar-group.js.map +1 -0
- package/dist/avatar.css +200 -0
- package/dist/avatar.d.ts +35 -0
- package/dist/avatar.js +67 -0
- package/dist/avatar.js.map +1 -0
- package/dist/badge.css +182 -0
- package/dist/badge.d.ts +34 -0
- package/dist/badge.js +4 -0
- package/dist/badge.js.map +1 -0
- package/dist/button.css +269 -0
- package/dist/button.d.ts +44 -0
- package/dist/button.js +4 -0
- package/dist/button.js.map +1 -0
- package/dist/checkbox.css +177 -0
- package/dist/checkbox.d.ts +35 -0
- package/dist/checkbox.js +5 -0
- package/dist/checkbox.js.map +1 -0
- package/dist/chunk-2ERBBFZS.js +12 -0
- package/dist/chunk-2ERBBFZS.js.map +1 -0
- package/dist/chunk-2HORSLXJ.js +191 -0
- package/dist/chunk-2HORSLXJ.js.map +1 -0
- package/dist/chunk-36WVAQKL.js +40 -0
- package/dist/chunk-36WVAQKL.js.map +1 -0
- package/dist/chunk-3SBCBYB7.js +575 -0
- package/dist/chunk-3SBCBYB7.js.map +1 -0
- package/dist/chunk-4R2GQO43.js +125 -0
- package/dist/chunk-4R2GQO43.js.map +1 -0
- package/dist/chunk-64UU4LGU.js +258 -0
- package/dist/chunk-64UU4LGU.js.map +1 -0
- package/dist/chunk-7SZCJ7WI.js +35 -0
- package/dist/chunk-7SZCJ7WI.js.map +1 -0
- package/dist/chunk-BIDVNLJO.js +38 -0
- package/dist/chunk-BIDVNLJO.js.map +1 -0
- package/dist/chunk-CONSWNIV.js +17 -0
- package/dist/chunk-CONSWNIV.js.map +1 -0
- package/dist/chunk-EQ32F2QJ.js +75 -0
- package/dist/chunk-EQ32F2QJ.js.map +1 -0
- package/dist/chunk-GY7JOPUQ.js +111 -0
- package/dist/chunk-GY7JOPUQ.js.map +1 -0
- package/dist/chunk-I4XQ4PV2.js +30 -0
- package/dist/chunk-I4XQ4PV2.js.map +1 -0
- package/dist/chunk-JSKXZMLH.js +67 -0
- package/dist/chunk-JSKXZMLH.js.map +1 -0
- package/dist/chunk-L3Q3VDIW.js +295 -0
- package/dist/chunk-L3Q3VDIW.js.map +1 -0
- package/dist/chunk-LDC3RIYA.js +62 -0
- package/dist/chunk-LDC3RIYA.js.map +1 -0
- package/dist/chunk-MKIQFK7S.js +43 -0
- package/dist/chunk-MKIQFK7S.js.map +1 -0
- package/dist/chunk-MMRIQNEN.js +20 -0
- package/dist/chunk-MMRIQNEN.js.map +1 -0
- package/dist/chunk-MXOPCFWR.js +59 -0
- package/dist/chunk-MXOPCFWR.js.map +1 -0
- package/dist/chunk-NEF5XAHG.js +17 -0
- package/dist/chunk-NEF5XAHG.js.map +1 -0
- package/dist/chunk-NNXJCLTV.js +22 -0
- package/dist/chunk-NNXJCLTV.js.map +1 -0
- package/dist/chunk-O4N2OBTR.js +94 -0
- package/dist/chunk-O4N2OBTR.js.map +1 -0
- package/dist/chunk-PMECHR3A.js +38 -0
- package/dist/chunk-PMECHR3A.js.map +1 -0
- package/dist/chunk-QDK66S4G.js +44 -0
- package/dist/chunk-QDK66S4G.js.map +1 -0
- package/dist/chunk-QGNC7LH5.js +8 -0
- package/dist/chunk-QGNC7LH5.js.map +1 -0
- package/dist/chunk-QMSJZCK3.js +398 -0
- package/dist/chunk-QMSJZCK3.js.map +1 -0
- package/dist/chunk-S5ZTZANS.js +57 -0
- package/dist/chunk-S5ZTZANS.js.map +1 -0
- package/dist/chunk-S7B34VGN.js +14 -0
- package/dist/chunk-S7B34VGN.js.map +1 -0
- package/dist/chunk-TCUCYFT7.js +25 -0
- package/dist/chunk-TCUCYFT7.js.map +1 -0
- package/dist/chunk-TYHR4VNM.js +193 -0
- package/dist/chunk-TYHR4VNM.js.map +1 -0
- package/dist/chunk-U5QQZC3U.js +56 -0
- package/dist/chunk-U5QQZC3U.js.map +1 -0
- package/dist/chunk-UE63OOVG.js +13 -0
- package/dist/chunk-UE63OOVG.js.map +1 -0
- package/dist/chunk-VTJTGVZZ.js +22 -0
- package/dist/chunk-VTJTGVZZ.js.map +1 -0
- package/dist/chunk-W3SHPKSB.js +49 -0
- package/dist/chunk-W3SHPKSB.js.map +1 -0
- package/dist/chunk-XZAEIQDR.js +188 -0
- package/dist/chunk-XZAEIQDR.js.map +1 -0
- package/dist/chunk-YGGCQNNU.js +92 -0
- package/dist/chunk-YGGCQNNU.js.map +1 -0
- package/dist/collapse.css +88 -0
- package/dist/collapse.d.ts +46 -0
- package/dist/collapse.js +5 -0
- package/dist/collapse.js.map +1 -0
- package/dist/count-badge.d.ts +13 -0
- package/dist/count-badge.js +13 -0
- package/dist/count-badge.js.map +1 -0
- package/dist/date-field.d.ts +25 -0
- package/dist/date-field.js +87 -0
- package/dist/date-field.js.map +1 -0
- package/dist/date-picker.css +595 -0
- package/dist/date-range-field.d.ts +33 -0
- package/dist/date-range-field.js +404 -0
- package/dist/date-range-field.js.map +1 -0
- package/dist/datetime-field.d.ts +29 -0
- package/dist/datetime-field.js +155 -0
- package/dist/datetime-field.js.map +1 -0
- package/dist/dialog.css +180 -0
- package/dist/dialog.d.ts +42 -0
- package/dist/dialog.js +128 -0
- package/dist/dialog.js.map +1 -0
- package/dist/dom-props-C0jrllBw.d.ts +5 -0
- package/dist/field-chrome-CMPOQSLs.d.ts +28 -0
- package/dist/field-shell-Cuda-feL.d.ts +24 -0
- package/dist/glide.d.ts +15 -0
- package/dist/glide.js +5 -0
- package/dist/glide.js.map +1 -0
- package/dist/icon.css +33 -0
- package/dist/input.css +333 -0
- package/dist/mcp.js +359 -0
- package/dist/motion-devtools.css +265 -0
- package/dist/motion-devtools.d.ts +51 -0
- package/dist/motion-devtools.js +354 -0
- package/dist/motion-devtools.js.map +1 -0
- package/dist/motion-scale-Canq4stZ.d.ts +18 -0
- package/dist/motion-tokens.d.ts +28 -0
- package/dist/motion-tokens.js +3 -0
- package/dist/motion-tokens.js.map +1 -0
- package/dist/multi-select.d.ts +44 -0
- package/dist/multi-select.js +109 -0
- package/dist/multi-select.js.map +1 -0
- package/dist/number-field.d.ts +36 -0
- package/dist/number-field.js +112 -0
- package/dist/number-field.js.map +1 -0
- package/dist/odometer.css +62 -0
- package/dist/otp-field.d.ts +31 -0
- package/dist/otp-field.js +98 -0
- package/dist/otp-field.js.map +1 -0
- package/dist/overlay.css +23 -0
- package/dist/pagination.css +43 -0
- package/dist/pagination.d.ts +29 -0
- package/dist/pagination.js +107 -0
- package/dist/pagination.js.map +1 -0
- package/dist/popover.css +69 -0
- package/dist/popover.d.ts +35 -0
- package/dist/popover.js +15 -0
- package/dist/popover.js.map +1 -0
- package/dist/radio-group.css +300 -0
- package/dist/radio-group.d.ts +52 -0
- package/dist/radio-group.js +115 -0
- package/dist/radio-group.js.map +1 -0
- package/dist/select.css +416 -0
- package/dist/select.d.ts +49 -0
- package/dist/select.js +99 -0
- package/dist/select.js.map +1 -0
- package/dist/sheet.css +26 -0
- package/dist/sheet.d.ts +31 -0
- package/dist/sheet.js +14 -0
- package/dist/sheet.js.map +1 -0
- package/dist/status-badge.d.ts +19 -0
- package/dist/status-badge.js +72 -0
- package/dist/status-badge.js.map +1 -0
- package/dist/table.css +348 -0
- package/dist/table.d.ts +75 -0
- package/dist/table.js +262 -0
- package/dist/table.js.map +1 -0
- package/dist/tabs.css +208 -0
- package/dist/tabs.d.ts +71 -0
- package/dist/tabs.js +181 -0
- package/dist/tabs.js.map +1 -0
- package/dist/tag.css +248 -0
- package/dist/tag.d.ts +52 -0
- package/dist/tag.js +72 -0
- package/dist/tag.js.map +1 -0
- package/dist/text-field.d.ts +28 -0
- package/dist/text-field.js +69 -0
- package/dist/text-field.js.map +1 -0
- package/dist/textarea.css +219 -0
- package/dist/textarea.d.ts +38 -0
- package/dist/textarea.js +144 -0
- package/dist/textarea.js.map +1 -0
- package/dist/time-field.d.ts +28 -0
- package/dist/time-field.js +66 -0
- package/dist/time-field.js.map +1 -0
- package/dist/timing-7JA_zQHP.d.ts +27 -0
- package/dist/toast-store.d.ts +86 -0
- package/dist/toast-store.js +3 -0
- package/dist/toast-store.js.map +1 -0
- package/dist/toast.css +257 -0
- package/dist/toast.d.ts +12 -0
- package/dist/toast.js +310 -0
- package/dist/toast.js.map +1 -0
- package/dist/toggle-tag.d.ts +34 -0
- package/dist/toggle-tag.js +57 -0
- package/dist/toggle-tag.js.map +1 -0
- package/dist/toggle.css +152 -0
- package/dist/toggle.d.ts +31 -0
- package/dist/toggle.js +75 -0
- package/dist/toggle.js.map +1 -0
- package/dist/tooltip.css +86 -0
- package/dist/tooltip.d.ts +30 -0
- package/dist/tooltip.js +268 -0
- package/dist/tooltip.js.map +1 -0
- package/dist/types-SGMShK7R.d.ts +22 -0
- package/llms.txt +196 -0
- package/next.d.mts +7 -0
- package/next.mjs +59 -0
- package/package.json +257 -0
- package/src/components/composites/alert/Alert.tsx +141 -0
- package/src/components/composites/alert/alert.css +132 -0
- package/src/components/composites/date-picker/DateField.tsx +80 -0
- package/src/components/composites/date-picker/DateRangeField.tsx +110 -0
- package/src/components/composites/date-picker/DateTimeField.tsx +162 -0
- package/src/components/composites/date-picker/TimeField.tsx +78 -0
- package/src/components/composites/date-picker/calendar-panel.tsx +216 -0
- package/src/components/composites/date-picker/date-picker.css +595 -0
- package/src/components/composites/date-picker/date-utils.ts +56 -0
- package/src/components/composites/date-picker/field-shell.tsx +85 -0
- package/src/components/composites/date-picker/range-panel.tsx +363 -0
- package/src/components/composites/date-picker/time-core.tsx +290 -0
- package/src/components/composites/date-picker/use-day-focus.ts +35 -0
- package/src/components/composites/dialog/Dialog.tsx +198 -0
- package/src/components/composites/dialog/dialog.css +180 -0
- package/src/components/composites/pagination/Pagination.tsx +131 -0
- package/src/components/composites/pagination/pagination.css +43 -0
- package/src/components/composites/popover/Popover.tsx +141 -0
- package/src/components/composites/popover/popover.css +69 -0
- package/src/components/composites/select/MultiSelect.tsx +133 -0
- package/src/components/composites/select/Select.tsx +122 -0
- package/src/components/composites/select/core/index.ts +4 -0
- package/src/components/composites/select/core/menu.tsx +100 -0
- package/src/components/composites/select/core/panel.tsx +154 -0
- package/src/components/composites/select/core/trigger.tsx +74 -0
- package/src/components/composites/select/core/types.ts +39 -0
- package/src/components/composites/select/core/use-listbox.ts +182 -0
- package/src/components/composites/select/select.css +416 -0
- package/src/components/composites/sheet/Sheet.tsx +135 -0
- package/src/components/composites/sheet/sheet.css +26 -0
- package/src/components/composites/sheet/use-sheet-drag.ts +126 -0
- package/src/components/composites/table/Table.tsx +377 -0
- package/src/components/composites/table/table.css +348 -0
- package/src/components/composites/tabs/Tabs.tsx +278 -0
- package/src/components/composites/tabs/tabs.css +208 -0
- package/src/components/composites/toast/Toast.tsx +380 -0
- package/src/components/composites/toast/toast-store.ts +295 -0
- package/src/components/composites/toast/toast.css +257 -0
- package/src/components/composites/tooltip/Tooltip.tsx +110 -0
- package/src/components/composites/tooltip/tooltip-host.tsx +184 -0
- package/src/components/composites/tooltip/tooltip-store.ts +81 -0
- package/src/components/composites/tooltip/tooltip.css +86 -0
- package/src/components/dev/MotionDevtools.tsx +200 -0
- package/src/components/dev/motion-devtools.css +265 -0
- package/src/components/dev/slowmo-engine.ts +230 -0
- package/src/components/internal/glass/glass.css +100 -0
- package/src/components/internal/glass/glint.ts +9 -0
- package/src/components/internal/hooks/use-controllable.ts +21 -0
- package/src/components/internal/hooks/use-scroll-edges.ts +46 -0
- package/src/components/internal/icon/Icon.tsx +66 -0
- package/src/components/internal/icon/IconSlot.tsx +10 -0
- package/src/components/internal/icon/icon.css +33 -0
- package/src/components/internal/overlay/focus.ts +68 -0
- package/src/components/internal/overlay/layer.tsx +149 -0
- package/src/components/internal/overlay/modal.tsx +141 -0
- package/src/components/internal/overlay/overlay.css +23 -0
- package/src/components/internal/overlay/position.ts +103 -0
- package/src/components/internal/utils/cx.ts +3 -0
- package/src/components/internal/utils/token-px.ts +8 -0
- package/src/components/primitives/avatar/Avatar.tsx +120 -0
- package/src/components/primitives/avatar/AvatarGroup.tsx +56 -0
- package/src/components/primitives/avatar/avatar.css +200 -0
- package/src/components/primitives/badge/Badge.tsx +75 -0
- package/src/components/primitives/badge/CountBadge.tsx +20 -0
- package/src/components/primitives/badge/StatusBadge.tsx +111 -0
- package/src/components/primitives/badge/badge.css +182 -0
- package/src/components/primitives/badge/odometer.css +62 -0
- package/src/components/primitives/badge/odometer.tsx +36 -0
- package/src/components/primitives/button/Button.tsx +102 -0
- package/src/components/primitives/button/button.css +269 -0
- package/src/components/primitives/checkbox/Checkbox.tsx +76 -0
- package/src/components/primitives/checkbox/check-glyph.tsx +28 -0
- package/src/components/primitives/checkbox/checkbox.css +177 -0
- package/src/components/primitives/collapse/Collapse.tsx +86 -0
- package/src/components/primitives/collapse/collapse.css +88 -0
- package/src/components/primitives/collapse/collapse.typecheck.tsx +29 -0
- package/src/components/primitives/input/NumberField.tsx +139 -0
- package/src/components/primitives/input/OtpField.tsx +134 -0
- package/src/components/primitives/input/TextField.tsx +110 -0
- package/src/components/primitives/input/field-chrome.tsx +83 -0
- package/src/components/primitives/input/input.css +333 -0
- package/src/components/primitives/radio-group/RadioGroup.tsx +183 -0
- package/src/components/primitives/radio-group/radio-group.css +300 -0
- package/src/components/primitives/tag/Tag.tsx +143 -0
- package/src/components/primitives/tag/ToggleTag.tsx +97 -0
- package/src/components/primitives/tag/tag.css +248 -0
- package/src/components/primitives/textarea/Textarea.tsx +190 -0
- package/src/components/primitives/textarea/textarea.css +219 -0
- package/src/components/primitives/toggle/Toggle.tsx +105 -0
- package/src/components/primitives/toggle/toggle.css +152 -0
- package/src/css.d.ts +1 -0
- package/src/dom-props.ts +1 -0
- package/src/engine/animate.ts +141 -0
- package/src/engine/drag.ts +53 -0
- package/src/engine/ease.ts +155 -0
- package/src/engine/flip.ts +59 -0
- package/src/engine/index.ts +13 -0
- package/src/mcp/server.ts +420 -0
- package/src/motion/element.tsx +53 -0
- package/src/motion/flip.tsx +87 -0
- package/src/motion/glide.tsx +47 -0
- package/src/motion/motion-timing.ts +33 -0
- package/src/motion/presence-context.ts +18 -0
- package/src/motion/presence.tsx +91 -0
- package/src/motion/timing.ts +49 -0
- package/src/motion/use-motion.ts +64 -0
- package/src/styles.css +26 -0
- package/src/tokens/avatar.css +20 -0
- package/src/tokens/color.css +58 -0
- package/src/tokens/elevation.css +22 -0
- package/src/tokens/fonts.css +3 -0
- package/src/tokens/glass.css +20 -0
- package/src/tokens/icons.css +7 -0
- package/src/tokens/layers.css +6 -0
- package/src/tokens/motion-scale.ts +17 -0
- package/src/tokens/motion-tokens.ts +92 -0
- package/src/tokens/motion.css +49 -0
- package/src/tokens/radius.css +11 -0
- package/src/tokens/semantic.css +65 -0
- package/src/tokens/spacing.css +26 -0
- package/src/tokens/typography.css +52 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/* tabs.css - line tabs for VIEW SWITCHING (overview / activity / settings), sitting on a
|
|
2
|
+
hairline baseline. NOT a form control: pick Segmented control for 2-3 filter options.
|
|
3
|
+
|
|
4
|
+
Anatomy: .tabs > .tabs__list[role=tablist] > .tab__hover? + .tab* + .tabs__ink
|
|
5
|
+
.tab > .tab__pad ( Icon? + .tab__label + .tab__count? )
|
|
6
|
+
States: :hover - :active - :focus-visible - :disabled - .is-selected
|
|
7
|
+
|
|
8
|
+
Two motions, both driven from Tabs.tsx - nothing here transitions them:
|
|
9
|
+
- the ink ("reaches, then releases"): one persistent node; the edge facing the
|
|
10
|
+
destination travels first, then the trailing edge settles. animate() keyframes on
|
|
11
|
+
x + measured width (destination width is unknowable to transforms; scaleX would
|
|
12
|
+
distort the radius).
|
|
13
|
+
- the gliding hover: one persistent pill (../motion/glide) that fades in under the
|
|
14
|
+
first tab touched, springs between pads, and fades out where it rests on leave.
|
|
15
|
+
Label color, icon weight and press dip remain trivial-tier CSS. */
|
|
16
|
+
|
|
17
|
+
/* -- Shell - owns the full-width baseline ----------------------------------
|
|
18
|
+
The hairline is a ::after on the shell (not a border on the scroller) so it
|
|
19
|
+
spans edge-to-edge while the scroller above it clips, masks and scrolls
|
|
20
|
+
freely. The scroller gets z-index:1 so the ink paints OVER the hairline. */
|
|
21
|
+
.tabs {
|
|
22
|
+
position: relative;
|
|
23
|
+
}
|
|
24
|
+
.tabs::after {
|
|
25
|
+
content: '';
|
|
26
|
+
position: absolute;
|
|
27
|
+
inset-inline: 0;
|
|
28
|
+
bottom: 0;
|
|
29
|
+
height: var(--border-hairline);
|
|
30
|
+
background: var(--border-default);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* -- Scroller - the tablist ------------------------------------------------
|
|
34
|
+
Overflow is honest: the row scrolls (scrollbar hidden) and the clipped edge
|
|
35
|
+
fades out via a mask. The fade lengths are two custom props flipped by
|
|
36
|
+
data-fade (set from scroll geometry in Tabs.jsx) - at 0px the mask is fully
|
|
37
|
+
opaque, i.e. a no-op. Negative margin + matching padding align the first
|
|
38
|
+
label flush with the container edge while the hover pill still has room. */
|
|
39
|
+
.tabs__list {
|
|
40
|
+
position: relative;
|
|
41
|
+
z-index: 1;
|
|
42
|
+
display: flex;
|
|
43
|
+
gap: var(--space-1);
|
|
44
|
+
overflow-x: auto;
|
|
45
|
+
scrollbar-width: none;
|
|
46
|
+
margin-inline: calc(var(--space-2) * -1);
|
|
47
|
+
padding-inline: var(--space-2);
|
|
48
|
+
|
|
49
|
+
--tabs-fade-start: 0px;
|
|
50
|
+
--tabs-fade-end: 0px;
|
|
51
|
+
mask-image: linear-gradient(
|
|
52
|
+
to right,
|
|
53
|
+
transparent,
|
|
54
|
+
black var(--tabs-fade-start),
|
|
55
|
+
black calc(100% - var(--tabs-fade-end)),
|
|
56
|
+
transparent
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
.tabs__list::-webkit-scrollbar {
|
|
60
|
+
display: none;
|
|
61
|
+
}
|
|
62
|
+
.tabs__list[data-fade~='start'] {
|
|
63
|
+
--tabs-fade-start: var(--space-8);
|
|
64
|
+
}
|
|
65
|
+
.tabs__list[data-fade~='end'] {
|
|
66
|
+
--tabs-fade-end: var(--space-8);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* -- Tab - the hit target --------------------------------------------------
|
|
70
|
+
The button is pill + a bottom gap down to the baseline (total height 36px).
|
|
71
|
+
All visual feedback lives on the inner pill so hover/press/focus read as a
|
|
72
|
+
soft inset shape, while the ink below stays anchored to the baseline. */
|
|
73
|
+
.tab {
|
|
74
|
+
appearance: none;
|
|
75
|
+
border: 0;
|
|
76
|
+
background: none;
|
|
77
|
+
margin: 0;
|
|
78
|
+
padding: 0 0 var(--space-2);
|
|
79
|
+
flex: none;
|
|
80
|
+
display: block;
|
|
81
|
+
font-family: var(--font-sans);
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
-webkit-tap-highlight-color: transparent;
|
|
84
|
+
}
|
|
85
|
+
.tab:focus-visible {
|
|
86
|
+
outline: none;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.tab__pad {
|
|
90
|
+
position: relative; /* positioned so every pad paints above the shared glide
|
|
91
|
+
pill (z-index 0, first child of the list) */
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
gap: var(--space-2);
|
|
95
|
+
height: var(--control-height-sm); /* 28 */
|
|
96
|
+
padding-inline: var(--space-2);
|
|
97
|
+
border-radius: var(--radius-md);
|
|
98
|
+
font: var(--type-micro);
|
|
99
|
+
line-height: 1;
|
|
100
|
+
white-space: nowrap;
|
|
101
|
+
color: var(--text-subtle);
|
|
102
|
+
transition:
|
|
103
|
+
var(--transition-control),
|
|
104
|
+
transform var(--duration-fast) var(--ease-standard);
|
|
105
|
+
}
|
|
106
|
+
.tab__pad .icon {
|
|
107
|
+
display: inline-flex;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* States - hover strengthens the label (the shared glide pill supplies the
|
|
111
|
+
background), press dips 1px on a darker fill (tactile, like Button), focus
|
|
112
|
+
is the composed accent ring, selection is text-strong + the ink. */
|
|
113
|
+
.tab:hover:not(:disabled) .tab__pad {
|
|
114
|
+
color: var(--text-body);
|
|
115
|
+
}
|
|
116
|
+
.tab:active:not(:disabled) .tab__pad {
|
|
117
|
+
background: var(--neutral-wash-press);
|
|
118
|
+
transform: translateY(var(--space-px));
|
|
119
|
+
}
|
|
120
|
+
.tab.is-selected .tab__pad,
|
|
121
|
+
.tab.is-selected:hover:not(:disabled) .tab__pad {
|
|
122
|
+
color: var(--text-strong);
|
|
123
|
+
}
|
|
124
|
+
.tab:focus-visible .tab__pad {
|
|
125
|
+
box-shadow: var(--ring-accent);
|
|
126
|
+
}
|
|
127
|
+
.tab:disabled {
|
|
128
|
+
cursor: not-allowed;
|
|
129
|
+
}
|
|
130
|
+
.tab:disabled .tab__pad {
|
|
131
|
+
color: var(--text-disabled);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* -- Count - always mono, always tabular -----------------------------------*/
|
|
135
|
+
.tab__count {
|
|
136
|
+
font-family: var(--font-mono);
|
|
137
|
+
font-size: var(--size-micro);
|
|
138
|
+
line-height: 1;
|
|
139
|
+
font-variant-numeric: tabular-nums;
|
|
140
|
+
color: var(--text-subtle);
|
|
141
|
+
background: var(--bg-muted);
|
|
142
|
+
padding: var(--space-1);
|
|
143
|
+
border-radius: var(--radius-sm);
|
|
144
|
+
transition: var(--transition-colors);
|
|
145
|
+
}
|
|
146
|
+
.tab.is-selected .tab__count {
|
|
147
|
+
color: var(--text-accent);
|
|
148
|
+
background: var(--accent-wash);
|
|
149
|
+
}
|
|
150
|
+
.tab:disabled .tab__count {
|
|
151
|
+
color: var(--text-disabled);
|
|
152
|
+
background: var(--bg-subtle);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* -- The gliding hover ------------------------------------------------------
|
|
156
|
+
One persistent pill, first child of the scroller (../motion/glide writes
|
|
157
|
+
x/y/width/height/opacity inline; coordinates include scrollLeft, so it
|
|
158
|
+
scrolls with its tab). z-index 0 + DOM order keep it under every tab's
|
|
159
|
+
positioned pad in both travel directions. Nothing transitions here. */
|
|
160
|
+
.tab__hover {
|
|
161
|
+
opacity: 0;
|
|
162
|
+
position: absolute;
|
|
163
|
+
top: 0;
|
|
164
|
+
left: 0;
|
|
165
|
+
z-index: 0;
|
|
166
|
+
border-radius: var(--radius-md);
|
|
167
|
+
background: var(--neutral-wash);
|
|
168
|
+
pointer-events: none;
|
|
169
|
+
will-change: transform, opacity;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* -- The ink ---------------------------------------------------------------
|
|
173
|
+
Positioned in the scroller's layout coordinates (offsetLeft space), so it
|
|
174
|
+
scrolls WITH its tab. Motion writes transform + width at runtime; resting
|
|
175
|
+
geometry is set the same way, so there is nothing to transition here.
|
|
176
|
+
EXCEPTION (intentional, mirrors RadioGroup's dot dims): 2px ink height is a
|
|
177
|
+
bespoke control dim - elevation.css reserves 2px for selected emphasis, but
|
|
178
|
+
ships no width token for it (--border-emphasis is 1.5px, which renders as
|
|
179
|
+
sub-pixel fuzz on 1x screens). Do not "fix" it to a token. */
|
|
180
|
+
.tabs__ink {
|
|
181
|
+
--tabs-ink-h: 2px;
|
|
182
|
+
position: absolute;
|
|
183
|
+
left: 0;
|
|
184
|
+
bottom: 0;
|
|
185
|
+
height: var(--tabs-ink-h);
|
|
186
|
+
border-radius: var(--radius-full);
|
|
187
|
+
background: var(--accent);
|
|
188
|
+
pointer-events: none;
|
|
189
|
+
will-change: transform, width;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* -- Panel -----------------------------------------------------------------
|
|
193
|
+
The panel ROOT is static chrome - borders, padding, background put on it
|
|
194
|
+
by the consumer never move. Only .tab-panel__inner (the content) enters
|
|
195
|
+
from the direction of travel (Motion, TabPanel). The root is focusable per
|
|
196
|
+
the APG (tabindex=0) so keyboard users land on content right after the
|
|
197
|
+
tablist; the ring shows only for keyboard focus. */
|
|
198
|
+
.tab-panel {
|
|
199
|
+
outline: none;
|
|
200
|
+
overflow: hidden;
|
|
201
|
+
}
|
|
202
|
+
.tab-panel:focus-visible {
|
|
203
|
+
box-shadow: var(--ring-accent);
|
|
204
|
+
border-radius: var(--radius-md);
|
|
205
|
+
}
|
|
206
|
+
.tab-panel__inner {
|
|
207
|
+
will-change: transform, opacity;
|
|
208
|
+
}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import './toast.css';
|
|
4
|
+
import {
|
|
5
|
+
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
useSyncExternalStore,
|
|
10
|
+
type CSSProperties,
|
|
11
|
+
type FocusEvent as ReactFocusEvent,
|
|
12
|
+
type KeyboardEvent as ReactKeyboardEvent,
|
|
13
|
+
type PointerEvent as ReactPointerEvent,
|
|
14
|
+
type HTMLAttributes,
|
|
15
|
+
type ReactElement,
|
|
16
|
+
type ReactNode,
|
|
17
|
+
type RefObject,
|
|
18
|
+
} from 'react';
|
|
19
|
+
import { createPortal } from 'react-dom';
|
|
20
|
+
import { animate, startDrag, type Layer, type Playback } from '../../../engine';
|
|
21
|
+
import { Presence } from '../../../motion/presence';
|
|
22
|
+
import { Motion } from '../../../motion/element';
|
|
23
|
+
import { UIMotion } from '../../../tokens/motion-tokens';
|
|
24
|
+
import { fireGlint } from '../../internal/glass/glint';
|
|
25
|
+
import { tokenPx } from '../../internal/utils/token-px';
|
|
26
|
+
import { Icon, type IconName } from '../../internal/icon/Icon';
|
|
27
|
+
import { Button } from '../../primitives/button/Button';
|
|
28
|
+
import { UIToast, DEFAULT_TOASTER_CONFIG, type ToastRecord, type ToastTone, type ToasterConfig } from './toast-store';
|
|
29
|
+
import type { DataAttributes } from '../../../dom-props';
|
|
30
|
+
import { cx } from '../../internal/utils/cx';
|
|
31
|
+
|
|
32
|
+
const SM = UIMotion;
|
|
33
|
+
const store = UIToast;
|
|
34
|
+
|
|
35
|
+
const COLLAPSE_GRACE = 140;
|
|
36
|
+
const SWIPE_X = 64;
|
|
37
|
+
const SWIPE_V = 480;
|
|
38
|
+
|
|
39
|
+
let GAP = 0;
|
|
40
|
+
function stackGap() {
|
|
41
|
+
if (!GAP) GAP = tokenPx('--space-3') || 12;
|
|
42
|
+
return GAP;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const TONE_ICON: Partial<Record<ToastTone, IconName>> = {
|
|
46
|
+
success: 'check-circle',
|
|
47
|
+
danger: 'warning-circle',
|
|
48
|
+
warning: 'warning',
|
|
49
|
+
info: 'info',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function useToneGesture(tone: ToastTone, ref: RefObject<HTMLElement>) {
|
|
53
|
+
const prevTone = useRef<ToastTone | null>(null);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const prev = prevTone.current;
|
|
56
|
+
prevTone.current = tone;
|
|
57
|
+
if (tone === prev) return;
|
|
58
|
+
if (tone === 'success') return fireGlint(ref.current);
|
|
59
|
+
if (tone === 'danger' && ref.current) {
|
|
60
|
+
const shake = ref.current.firstElementChild as HTMLElement | null;
|
|
61
|
+
if (!shake) return;
|
|
62
|
+
const fall = animate(shake, {
|
|
63
|
+
x: [0, -7, 5, -2, 0],
|
|
64
|
+
timing: { duration: SM.dur.slow, ease: SM.ease.standard, delay: SM.dur.base },
|
|
65
|
+
});
|
|
66
|
+
fall.finished.then(() => fall.stop());
|
|
67
|
+
return () => fall.stop();
|
|
68
|
+
}
|
|
69
|
+
}, [tone]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function useReplay<T extends HTMLElement>(token: unknown, build: () => Layer[]) {
|
|
73
|
+
const ref = useRef<T>(null);
|
|
74
|
+
const latest = useRef(build);
|
|
75
|
+
latest.current = build;
|
|
76
|
+
useLayoutEffect(() => {
|
|
77
|
+
if (ref.current) animate(ref.current, ...latest.current());
|
|
78
|
+
}, [token]);
|
|
79
|
+
return ref;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ToastItem({
|
|
83
|
+
t,
|
|
84
|
+
depth,
|
|
85
|
+
offset,
|
|
86
|
+
hidden,
|
|
87
|
+
behind,
|
|
88
|
+
frameHeight,
|
|
89
|
+
isTop,
|
|
90
|
+
onHeight,
|
|
91
|
+
}: {
|
|
92
|
+
t: ToastRecord;
|
|
93
|
+
depth: number;
|
|
94
|
+
offset: number;
|
|
95
|
+
hidden: boolean;
|
|
96
|
+
behind: boolean;
|
|
97
|
+
frameHeight: number | null;
|
|
98
|
+
isTop: boolean;
|
|
99
|
+
onHeight: (id: string, h: number) => void;
|
|
100
|
+
}) {
|
|
101
|
+
const ref = useRef<HTMLLIElement>(null);
|
|
102
|
+
const settled = useRef(false);
|
|
103
|
+
const swipeBack = useRef<Playback | null>(null);
|
|
104
|
+
useToneGesture(t.tone, ref);
|
|
105
|
+
|
|
106
|
+
useLayoutEffect(() => {
|
|
107
|
+
if (ref.current && ref.current.firstElementChild)
|
|
108
|
+
onHeight(t.id, (ref.current.firstElementChild as HTMLElement).offsetHeight);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
useLayoutEffect(() => {
|
|
112
|
+
const el = ref.current;
|
|
113
|
+
if (!el) return;
|
|
114
|
+
el.style.pointerEvents = hidden ? 'none' : 'auto';
|
|
115
|
+
const y = isTop ? offset : -offset;
|
|
116
|
+
const scale = 1 - depth * 0.05;
|
|
117
|
+
if (settled.current) {
|
|
118
|
+
animate(
|
|
119
|
+
el,
|
|
120
|
+
{ y: [y], scale: [scale], height: [frameHeight ?? 'auto'], timing: SM.t.settle },
|
|
121
|
+
{ opacity: [hidden ? 0 : 1], timing: SM.t.enter },
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
settled.current = true;
|
|
126
|
+
animate(
|
|
127
|
+
el,
|
|
128
|
+
{ y: [isTop ? -24 : 24, y], scale: [0.97, scale], timing: SM.t.settle },
|
|
129
|
+
{ opacity: [0, hidden ? 0 : 1], timing: SM.t.enter },
|
|
130
|
+
);
|
|
131
|
+
}, [offset, depth, hidden, frameHeight, isTop]);
|
|
132
|
+
|
|
133
|
+
const onPointerDown = (e: ReactPointerEvent) => {
|
|
134
|
+
const inner = ref.current?.firstElementChild as HTMLElement | null;
|
|
135
|
+
if (!t.dismissible || e.button !== 0 || !inner) return;
|
|
136
|
+
swipeBack.current?.stop();
|
|
137
|
+
startDrag(e, {
|
|
138
|
+
onMove: (info) => {
|
|
139
|
+
const dx = info.offset.x;
|
|
140
|
+
inner.style.translate = `${dx > 0 ? dx * 0.9 : dx * 0.04}px 0px`;
|
|
141
|
+
},
|
|
142
|
+
onEnd: (info) => {
|
|
143
|
+
if (info.offset.x > SWIPE_X || info.velocity.x > SWIPE_V) {
|
|
144
|
+
animate(inner, { x: [420], timing: SM.t.exit }).finished.then(() => store.dismiss(t.id));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const back = animate(inner, { x: [0], timing: SM.t.settle });
|
|
148
|
+
swipeBack.current = back;
|
|
149
|
+
back.finished.then(() => back.stop());
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<Motion
|
|
156
|
+
as="li"
|
|
157
|
+
ref={ref}
|
|
158
|
+
exit={{ opacity: [0], scale: [0.96], timing: SM.t.exit }}
|
|
159
|
+
className="toast glass"
|
|
160
|
+
data-tone={t.tone}
|
|
161
|
+
data-behind={behind ? '' : undefined}
|
|
162
|
+
role={t.tone === 'danger' ? 'alert' : 'status'}
|
|
163
|
+
aria-atomic="true"
|
|
164
|
+
onPointerDown={onPointerDown}
|
|
165
|
+
onKeyDown={(e: ReactKeyboardEvent) => {
|
|
166
|
+
if (e.key === 'Escape' && t.dismissible) store.dismiss(t.id);
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
{t.node ? <div className="toast__custom">{t.node as ReactNode}</div> : <ToastBody t={t} />}
|
|
170
|
+
</Motion>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function ToastBody({ t }: { t: ToastRecord }) {
|
|
175
|
+
const glyphRef = useReplay<HTMLSpanElement>(t.tone, () => [
|
|
176
|
+
{ scale: [0.5, 1], timing: SM.t.settle },
|
|
177
|
+
{ opacity: [0, 1], timing: SM.t.enter },
|
|
178
|
+
]);
|
|
179
|
+
const textRef = useReplay<HTMLDivElement>(String(t.message) + '-' + String(t.description), () => [
|
|
180
|
+
{ opacity: [0, 1], timing: SM.t.enter },
|
|
181
|
+
]);
|
|
182
|
+
const countRef = useReplay<HTMLSpanElement>(t.count, () => [{ scale: [0.6, 1], timing: SM.t.settle }]);
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<div className="toast__inner">
|
|
186
|
+
{(t.tone !== 'default' || isFinite(t.duration)) && (
|
|
187
|
+
<span className="toast__icon">
|
|
188
|
+
{t.tone !== 'default' && (
|
|
189
|
+
<span ref={glyphRef} className="toast__icon-glyph">
|
|
190
|
+
{t.tone === 'loading' ? (
|
|
191
|
+
<span className="toast__spinner" aria-hidden="true"></span>
|
|
192
|
+
) : (
|
|
193
|
+
<Icon name={TONE_ICON[t.tone] || 'info'} weight="fill" />
|
|
194
|
+
)}
|
|
195
|
+
</span>
|
|
196
|
+
)}
|
|
197
|
+
{isFinite(t.duration) && (
|
|
198
|
+
<svg key={'ring-' + t.timerKey} className="toast__ring" viewBox="0 0 36 36" aria-hidden="true">
|
|
199
|
+
<circle className="toast__ring-track" cx="18" cy="18" r="16.5"></circle>
|
|
200
|
+
<circle
|
|
201
|
+
className="toast__ring-arc"
|
|
202
|
+
cx="18"
|
|
203
|
+
cy="18"
|
|
204
|
+
r="16.5"
|
|
205
|
+
pathLength={1}
|
|
206
|
+
style={{ animationDuration: t.duration + 'ms' }}
|
|
207
|
+
></circle>
|
|
208
|
+
</svg>
|
|
209
|
+
)}
|
|
210
|
+
</span>
|
|
211
|
+
)}
|
|
212
|
+
<div ref={textRef} className="toast__text">
|
|
213
|
+
<p className="toast__message">{t.message}</p>
|
|
214
|
+
{t.description != null && <p className="toast__desc">{t.description}</p>}
|
|
215
|
+
</div>
|
|
216
|
+
{t.count > 1 && (
|
|
217
|
+
<span ref={countRef} className="toast__count">
|
|
218
|
+
×{t.count}
|
|
219
|
+
</span>
|
|
220
|
+
)}
|
|
221
|
+
{t.action && (
|
|
222
|
+
<Button
|
|
223
|
+
variant="secondary"
|
|
224
|
+
size="sm"
|
|
225
|
+
className="toast__action"
|
|
226
|
+
onClick={() => {
|
|
227
|
+
if (t.action.onClick) t.action.onClick();
|
|
228
|
+
store.dismiss(t.id);
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
{t.action.label}
|
|
232
|
+
</Button>
|
|
233
|
+
)}
|
|
234
|
+
{t.dismissible && (
|
|
235
|
+
<button type="button" className="toast__close" aria-label="Dismiss" onClick={() => store.dismiss(t.id)}>
|
|
236
|
+
<Icon name="close" size="sm" />
|
|
237
|
+
</button>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function ToastHost({ config, htmlProps }: { config: ToasterConfig; htmlProps?: HTMLAttributes<HTMLOListElement> }) {
|
|
244
|
+
const { toasts, paused, expanded: hoverExpanded } = useSyncExternalStore(store.subscribe, store.get, store.get);
|
|
245
|
+
const [heights, setHeights] = useState<Record<string, number>>({});
|
|
246
|
+
const [ready, setReady] = useState(false);
|
|
247
|
+
const collapseTimer = useRef<ReturnType<typeof setTimeout> | 0>(0);
|
|
248
|
+
const hovering = useRef(false);
|
|
249
|
+
|
|
250
|
+
useEffect(() => setReady(true), []);
|
|
251
|
+
|
|
252
|
+
const onHeight = (id: string, h: number) => setHeights((prev) => (prev[id] === h ? prev : { ...prev, [id]: h }));
|
|
253
|
+
|
|
254
|
+
const onHold = () => {
|
|
255
|
+
hovering.current = true;
|
|
256
|
+
clearTimeout(collapseTimer.current);
|
|
257
|
+
store.setExpanded(true);
|
|
258
|
+
store.pause();
|
|
259
|
+
};
|
|
260
|
+
const onRelease = (graced: boolean) => {
|
|
261
|
+
hovering.current = false;
|
|
262
|
+
clearTimeout(collapseTimer.current);
|
|
263
|
+
collapseTimer.current = setTimeout(
|
|
264
|
+
() => {
|
|
265
|
+
store.setExpanded(false);
|
|
266
|
+
store.resume();
|
|
267
|
+
},
|
|
268
|
+
graced ? COLLAPSE_GRACE : 0,
|
|
269
|
+
);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
const onVis = () => {
|
|
274
|
+
if (document.hidden) store.pause();
|
|
275
|
+
else if (!hovering.current) store.resume();
|
|
276
|
+
};
|
|
277
|
+
document.addEventListener('visibilitychange', onVis);
|
|
278
|
+
return () => document.removeEventListener('visibilitychange', onVis);
|
|
279
|
+
}, []);
|
|
280
|
+
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
if (toasts.length === 0) {
|
|
283
|
+
hovering.current = false;
|
|
284
|
+
clearTimeout(collapseTimer.current);
|
|
285
|
+
store.setExpanded(false);
|
|
286
|
+
store.resume();
|
|
287
|
+
}
|
|
288
|
+
}, [toasts.length]);
|
|
289
|
+
|
|
290
|
+
if (!ready) return null;
|
|
291
|
+
|
|
292
|
+
const isTop = config.position.startsWith('top');
|
|
293
|
+
const visible = config.visibleToasts;
|
|
294
|
+
const rendered = visible * 2;
|
|
295
|
+
const gap = config.gap || stackGap();
|
|
296
|
+
const expanded = config.expand || hoverExpanded;
|
|
297
|
+
|
|
298
|
+
const slice = toasts.slice(-rendered);
|
|
299
|
+
const n = slice.length;
|
|
300
|
+
const frontH = n ? heights[slice[n - 1].id] || 0 : 0;
|
|
301
|
+
|
|
302
|
+
return createPortal(
|
|
303
|
+
<ol
|
|
304
|
+
{...htmlProps}
|
|
305
|
+
className={cx('toast-viewport', paused && 'is-paused', htmlProps?.className)}
|
|
306
|
+
data-position={config.position}
|
|
307
|
+
style={
|
|
308
|
+
config.offset
|
|
309
|
+
? ({ ...htmlProps?.style, '--toast-offset': `${config.offset}px` } as CSSProperties)
|
|
310
|
+
: htmlProps?.style
|
|
311
|
+
}
|
|
312
|
+
aria-label="Notifications"
|
|
313
|
+
onPointerOver={(e: ReactPointerEvent<HTMLOListElement>) => {
|
|
314
|
+
if (e.pointerType !== 'touch') onHold();
|
|
315
|
+
}}
|
|
316
|
+
onPointerOut={(e: ReactPointerEvent<HTMLOListElement>) => {
|
|
317
|
+
if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) onRelease(true);
|
|
318
|
+
}}
|
|
319
|
+
onFocus={onHold}
|
|
320
|
+
onBlur={(e: ReactFocusEvent<HTMLOListElement>) => {
|
|
321
|
+
if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) onRelease(false);
|
|
322
|
+
}}
|
|
323
|
+
>
|
|
324
|
+
<Presence>
|
|
325
|
+
{slice.map((t, i) => {
|
|
326
|
+
const depth = n - 1 - i;
|
|
327
|
+
let offset = depth * gap;
|
|
328
|
+
if (expanded) {
|
|
329
|
+
offset = 0;
|
|
330
|
+
for (let j = i + 1; j < n; j++) offset += (heights[slice[j].id] || 0) + gap;
|
|
331
|
+
}
|
|
332
|
+
return (
|
|
333
|
+
<ToastItem
|
|
334
|
+
key={t.id}
|
|
335
|
+
t={t}
|
|
336
|
+
depth={expanded ? 0 : depth}
|
|
337
|
+
offset={offset}
|
|
338
|
+
hidden={!expanded && depth >= visible}
|
|
339
|
+
behind={!expanded && depth > 0}
|
|
340
|
+
frameHeight={!expanded && depth > 0 && frontH ? frontH : null}
|
|
341
|
+
isTop={isTop}
|
|
342
|
+
onHeight={onHeight}
|
|
343
|
+
/>
|
|
344
|
+
);
|
|
345
|
+
})}
|
|
346
|
+
</Presence>
|
|
347
|
+
</ol>,
|
|
348
|
+
document.body,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function stripUndefined<T extends object>(o: T): Partial<T> {
|
|
353
|
+
const out: Partial<T> = {};
|
|
354
|
+
(Object.keys(o) as (keyof T)[]).forEach((k) => {
|
|
355
|
+
if (o[k] !== undefined) out[k] = o[k];
|
|
356
|
+
});
|
|
357
|
+
return out;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export interface ToasterProps extends Partial<ToasterConfig> {
|
|
361
|
+
/** Standard <ol> attributes (className, style, data-*, ...) forwarded to the toast viewport. */
|
|
362
|
+
htmlProps?: HTMLAttributes<HTMLOListElement> & DataAttributes;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function Toaster({ htmlProps, ...props }: ToasterProps): ReactElement {
|
|
366
|
+
const config: ToasterConfig = { ...DEFAULT_TOASTER_CONFIG, ...stripUndefined(props) };
|
|
367
|
+
|
|
368
|
+
useEffect(() => {
|
|
369
|
+
store.config = config;
|
|
370
|
+
store.mounted = true;
|
|
371
|
+
if (config.expand) store.setExpanded(true);
|
|
372
|
+
return () => {
|
|
373
|
+
store.mounted = false;
|
|
374
|
+
};
|
|
375
|
+
}, [config.position, config.duration, config.visibleToasts, config.gap, config.offset, config.expand]);
|
|
376
|
+
|
|
377
|
+
return <ToastHost config={config} htmlProps={htmlProps} />;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export { toast } from './toast-store';
|