@giddaa-housing/ui 3.6.0 → 3.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/css/generated/shared.css +1 -1
- package/css/theme.css +11 -8
- package/dist/button.js +4 -2
- package/dist/calendar.js +1 -1
- package/dist/{combobox-CACVdp1R.js → combobox-D7QdQzN8.js} +76 -10
- package/dist/combobox.d.ts +2 -2
- package/dist/combobox.js +1 -1
- package/dist/data-table.d.ts +1 -1
- package/dist/date-picker.d.ts +1 -1
- package/dist/dialog.d.ts +4 -2
- package/dist/dialog.js +10 -11
- package/dist/field.d.ts +3 -2
- package/dist/field.js +9 -2
- package/dist/infotip.js +0 -1
- package/dist/input-group.js +5 -5
- package/dist/label.d.ts +2 -1
- package/dist/label.js +12 -24
- package/dist/match.d.ts +11 -2
- package/dist/match.js +49 -32
- package/dist/media-player.js +3 -3
- package/dist/mobile-sidebar.js +7 -7
- package/dist/phone-input.d.ts +18 -1
- package/dist/phone-input.js +133 -64
- package/dist/{picker-j3txxA5t.d.ts → picker-qxWi9wZ1.d.ts} +1 -1
- package/dist/{popover-CNymD33m.d.ts → popover-BMTZp3AQ.d.ts} +2 -2
- package/dist/popover.d.ts +1 -1
- package/dist/popover.js +2 -1
- package/dist/rating.d.ts +60 -0
- package/dist/rating.js +187 -0
- package/dist/select.js +38 -3
- package/dist/sheet.d.ts +3 -1
- package/dist/sheet.js +10 -11
- package/dist/sonar.d.ts +3 -1
- package/dist/sonar.js +116 -28
- package/dist/styles.css +180 -30
- package/dist/time-picker.d.ts +1 -1
- package/dist/toast.js +3 -3
- package/dist/video-player-dialog.js +1 -1
- package/package.json +5 -1
- package/dist/dialog-nesting.d.ts +0 -9
- package/dist/dialog-nesting.js +0 -27
package/dist/rating.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Star } from "./icons.js";
|
|
3
|
+
import { cn } from "./utils/cn.js";
|
|
4
|
+
import { responsiveValueClasses, useComponentSizeValue } from "./size-context.js";
|
|
5
|
+
import { useUncontrolled } from "./utils/use-uncontrolled.js";
|
|
6
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
//#region src/rating.tsx
|
|
9
|
+
const ratingIconSizes = {
|
|
10
|
+
sm: "size-4",
|
|
11
|
+
md: "size-5",
|
|
12
|
+
lg: "size-6"
|
|
13
|
+
};
|
|
14
|
+
const ratingGapSizes = {
|
|
15
|
+
sm: "gap-0.5",
|
|
16
|
+
md: "gap-1",
|
|
17
|
+
lg: "gap-1.5"
|
|
18
|
+
};
|
|
19
|
+
const responsiveIconSizes = {
|
|
20
|
+
base: ratingIconSizes,
|
|
21
|
+
md: {
|
|
22
|
+
sm: "md:size-4",
|
|
23
|
+
md: "md:size-5",
|
|
24
|
+
lg: "md:size-6"
|
|
25
|
+
},
|
|
26
|
+
lg: {
|
|
27
|
+
sm: "lg:size-4",
|
|
28
|
+
md: "lg:size-5",
|
|
29
|
+
lg: "lg:size-6"
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const responsiveGapSizes = {
|
|
33
|
+
base: ratingGapSizes,
|
|
34
|
+
md: {
|
|
35
|
+
sm: "md:gap-0.5",
|
|
36
|
+
md: "md:gap-1",
|
|
37
|
+
lg: "md:gap-1.5"
|
|
38
|
+
},
|
|
39
|
+
lg: {
|
|
40
|
+
sm: "lg:gap-0.5",
|
|
41
|
+
md: "lg:gap-1",
|
|
42
|
+
lg: "lg:gap-1.5"
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function clampRating(value, max) {
|
|
46
|
+
if (!Number.isFinite(value)) return 0;
|
|
47
|
+
return Math.min(max, Math.max(0, value));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* How much of the star at `index` (1-based) the value fills, as a percentage.
|
|
51
|
+
* Rounded because `4.3 - 4` is `0.30000000000000027` in binary floating point,
|
|
52
|
+
* and that number should not reach the DOM as a width.
|
|
53
|
+
*/
|
|
54
|
+
function getIconFill(index, value) {
|
|
55
|
+
const fill = Math.min(1, Math.max(0, value - (index - 1)));
|
|
56
|
+
return Math.round(fill * 1e4) / 100;
|
|
57
|
+
}
|
|
58
|
+
function formatRatingValue(value) {
|
|
59
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(1);
|
|
60
|
+
}
|
|
61
|
+
function defaultItemLabel(value, max) {
|
|
62
|
+
return `${formatRatingValue(value)} of ${max}`;
|
|
63
|
+
}
|
|
64
|
+
/** Every value a reader can pick, low to high: `[0.5, 1, 1.5, …]` or `[1, 2, …]`. */
|
|
65
|
+
function getSelectableValues(max, precision) {
|
|
66
|
+
const steps = Math.round(max / precision);
|
|
67
|
+
return Array.from({ length: steps }, (_, step) => (step + 1) * precision);
|
|
68
|
+
}
|
|
69
|
+
function RatingIconPair({ fill, icon: Icon, sizeClassName }) {
|
|
70
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Icon, {
|
|
71
|
+
"aria-hidden": "true",
|
|
72
|
+
"data-slot": "rating-icon",
|
|
73
|
+
className: cn("block shrink-0 text-line-strong transition-colors", sizeClassName)
|
|
74
|
+
}), fill > 0 ? /* @__PURE__ */ jsx("span", {
|
|
75
|
+
"aria-hidden": "true",
|
|
76
|
+
"data-slot": "rating-icon-fill",
|
|
77
|
+
className: "pointer-events-none absolute inset-y-0 left-0 overflow-hidden text-line-focus",
|
|
78
|
+
style: { width: `${fill}%` },
|
|
79
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
80
|
+
fill: "currentColor",
|
|
81
|
+
className: cn("block max-w-none shrink-0 transition-colors", sizeClassName)
|
|
82
|
+
})
|
|
83
|
+
}) : null] });
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A star rating that reads and writes.
|
|
87
|
+
*
|
|
88
|
+
* Interactive ratings are a group of real radio inputs — one per selectable
|
|
89
|
+
* value — visually replaced by the icons. That is deliberate: it buys native
|
|
90
|
+
* arrow-key navigation, native form submission under `name`, and the
|
|
91
|
+
* "3 of 5, radio button 3 of 5" announcement, none of which a div with click
|
|
92
|
+
* handlers gets for free.
|
|
93
|
+
*
|
|
94
|
+
* ```tsx
|
|
95
|
+
* <Rating defaultValue={4} onValueChange={setRating} />
|
|
96
|
+
* <Rating value={4.3} readOnly />
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
function Rating({ value, defaultValue, onValueChange, max = 5, precision = 1, size, readOnly = false, disabled = false, required = false, name, id, itemLabel = defaultItemLabel, icon = Star, iconClassName, className, onBlur, ref, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid }) {
|
|
100
|
+
const generatedName = React.useId();
|
|
101
|
+
const [current, setCurrent] = useUncontrolled({
|
|
102
|
+
value,
|
|
103
|
+
defaultValue,
|
|
104
|
+
finalValue: 0,
|
|
105
|
+
onChange: onValueChange
|
|
106
|
+
});
|
|
107
|
+
const [preview, setPreview] = React.useState(null);
|
|
108
|
+
const sizeValue = useComponentSizeValue(size);
|
|
109
|
+
const iconSizes = responsiveValueClasses(sizeValue, responsiveIconSizes);
|
|
110
|
+
const gapSizes = responsiveValueClasses(sizeValue, responsiveGapSizes);
|
|
111
|
+
const iconBox = cn(iconSizes.className, iconClassName);
|
|
112
|
+
const interactive = !readOnly;
|
|
113
|
+
const displayed = clampRating(interactive && preview !== null ? preview : current, max);
|
|
114
|
+
const icons = Array.from({ length: max }, (_, index) => index + 1);
|
|
115
|
+
const rootProps = {
|
|
116
|
+
"data-slot": "rating",
|
|
117
|
+
"data-size": iconSizes.base,
|
|
118
|
+
"data-size-md": iconSizes.responsive ? iconSizes.md : void 0,
|
|
119
|
+
"data-size-lg": iconSizes.responsive ? iconSizes.lg : void 0,
|
|
120
|
+
className: cn("inline-flex w-fit shrink-0 items-center", gapSizes.className, className)
|
|
121
|
+
};
|
|
122
|
+
if (!interactive) return /* @__PURE__ */ jsx("span", {
|
|
123
|
+
...rootProps,
|
|
124
|
+
ref,
|
|
125
|
+
id,
|
|
126
|
+
"aria-label": ariaLabel ?? itemLabel(displayed, max),
|
|
127
|
+
"data-readonly": "true",
|
|
128
|
+
role: "img",
|
|
129
|
+
children: icons.map((index) => /* @__PURE__ */ jsx("span", {
|
|
130
|
+
className: cn("relative inline-flex shrink-0", iconBox),
|
|
131
|
+
children: /* @__PURE__ */ jsx(RatingIconPair, {
|
|
132
|
+
fill: getIconFill(index, displayed),
|
|
133
|
+
icon,
|
|
134
|
+
sizeClassName: iconBox
|
|
135
|
+
})
|
|
136
|
+
}, index))
|
|
137
|
+
});
|
|
138
|
+
const groupName = name ?? generatedName;
|
|
139
|
+
const selectable = getSelectableValues(max, precision);
|
|
140
|
+
return /* @__PURE__ */ jsx("div", {
|
|
141
|
+
...rootProps,
|
|
142
|
+
ref,
|
|
143
|
+
id,
|
|
144
|
+
role: "radiogroup",
|
|
145
|
+
"aria-label": ariaLabelledBy ? void 0 : ariaLabel ?? "Rating",
|
|
146
|
+
"aria-labelledby": ariaLabelledBy,
|
|
147
|
+
"aria-describedby": ariaDescribedBy,
|
|
148
|
+
"aria-invalid": ariaInvalid || void 0,
|
|
149
|
+
"aria-required": required || void 0,
|
|
150
|
+
"data-disabled": disabled || void 0,
|
|
151
|
+
onBlur,
|
|
152
|
+
onPointerLeave: () => setPreview(null),
|
|
153
|
+
className: cn(rootProps.className, disabled && "cursor-not-allowed opacity-60"),
|
|
154
|
+
children: icons.map((index) => {
|
|
155
|
+
const values = selectable.filter((item) => item > index - 1 && item <= index);
|
|
156
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
157
|
+
"data-slot": "rating-item",
|
|
158
|
+
className: cn("relative inline-flex shrink-0 transition-[scale] duration-[var(--duration-motion-press)] ease-gdt-out", !disabled && "active:scale-[0.97] motion-reduce:active:scale-100", iconBox),
|
|
159
|
+
children: [/* @__PURE__ */ jsx(RatingIconPair, {
|
|
160
|
+
fill: getIconFill(index, displayed),
|
|
161
|
+
icon,
|
|
162
|
+
sizeClassName: iconBox
|
|
163
|
+
}), values.map((item, position) => /* @__PURE__ */ jsx("label", {
|
|
164
|
+
"data-slot": "rating-control",
|
|
165
|
+
"data-value": item,
|
|
166
|
+
onPointerEnter: () => {
|
|
167
|
+
if (!disabled) setPreview(item);
|
|
168
|
+
},
|
|
169
|
+
className: cn("absolute inset-y-0 rounded-[3px]", "has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-line-focus has-[:focus-visible]:ring-offset-2 has-[:focus-visible]:ring-offset-canvas", disabled ? "cursor-not-allowed" : "cursor-pointer", values.length === 1 ? "left-0 w-full" : position === 0 ? "left-0 w-1/2" : "right-0 w-1/2"),
|
|
170
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
171
|
+
type: "radio",
|
|
172
|
+
className: "sr-only",
|
|
173
|
+
name: groupName,
|
|
174
|
+
value: item,
|
|
175
|
+
checked: current === item,
|
|
176
|
+
disabled,
|
|
177
|
+
required: required && item === selectable[0],
|
|
178
|
+
"aria-label": itemLabel(item, max),
|
|
179
|
+
onChange: () => setCurrent(item)
|
|
180
|
+
})
|
|
181
|
+
}, item))]
|
|
182
|
+
}, index);
|
|
183
|
+
})
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
export { Rating };
|
package/dist/select.js
CHANGED
|
@@ -113,6 +113,40 @@ const responsiveSelectBodySizes = {
|
|
|
113
113
|
lg: "lg:p-2"
|
|
114
114
|
}
|
|
115
115
|
};
|
|
116
|
+
const responsiveSelectEdgeSizes = {
|
|
117
|
+
base: {
|
|
118
|
+
sm: "px-4",
|
|
119
|
+
md: "px-5.5",
|
|
120
|
+
lg: "px-6"
|
|
121
|
+
},
|
|
122
|
+
md: {
|
|
123
|
+
sm: "md:px-4",
|
|
124
|
+
md: "md:px-5.5",
|
|
125
|
+
lg: "md:px-6"
|
|
126
|
+
},
|
|
127
|
+
lg: {
|
|
128
|
+
sm: "lg:px-4",
|
|
129
|
+
md: "lg:px-5.5",
|
|
130
|
+
lg: "lg:px-6"
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const responsiveSelectLabelSizes = {
|
|
134
|
+
base: {
|
|
135
|
+
sm: "px-3",
|
|
136
|
+
md: "px-4",
|
|
137
|
+
lg: "px-4"
|
|
138
|
+
},
|
|
139
|
+
md: {
|
|
140
|
+
sm: "md:px-3",
|
|
141
|
+
md: "md:px-4",
|
|
142
|
+
lg: "md:px-4"
|
|
143
|
+
},
|
|
144
|
+
lg: {
|
|
145
|
+
sm: "lg:px-3",
|
|
146
|
+
md: "lg:px-4",
|
|
147
|
+
lg: "lg:px-4"
|
|
148
|
+
}
|
|
149
|
+
};
|
|
116
150
|
function SelectGroup({ className, ...props }) {
|
|
117
151
|
return /* @__PURE__ */ jsx(Select$1.Group, {
|
|
118
152
|
"data-slot": "select-group",
|
|
@@ -175,7 +209,7 @@ function SelectHeader({ className, type = "button", ...props }) {
|
|
|
175
209
|
return /* @__PURE__ */ jsx("button", {
|
|
176
210
|
type,
|
|
177
211
|
"data-slot": "select-header",
|
|
178
|
-
className: cn("relative z-20 flex min-h-9 w-full cursor-pointer items-center gap-2 rounded-none rounded-t-[inherit] border-line-subtle border-b bg-canvas
|
|
212
|
+
className: cn("relative z-20 flex min-h-9 w-full cursor-pointer items-center gap-2 rounded-none rounded-t-[inherit] border-line-subtle border-b bg-canvas py-2 text-left text-gdt-sm font-bold text-fg-brand outline-none transition-colors hover:bg-surface-brand-subtle focus-visible:bg-surface-brand-subtle focus-visible:inset-ring-2 focus-visible:inset-ring-line-focus active:bg-surface-brand-subtle disabled:pointer-events-none disabled:text-fg-caption-placeholder", "[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", responsiveValueClasses(useComponentSizeValue(), responsiveSelectEdgeSizes).className, className),
|
|
179
213
|
...props
|
|
180
214
|
});
|
|
181
215
|
}
|
|
@@ -183,14 +217,15 @@ function SelectFooter({ className, type = "button", ...props }) {
|
|
|
183
217
|
return /* @__PURE__ */ jsx("button", {
|
|
184
218
|
type,
|
|
185
219
|
"data-slot": "select-footer",
|
|
186
|
-
className: cn("relative z-20 flex min-h-9 w-full cursor-pointer items-center gap-2 rounded-none rounded-b-[inherit] border-line-subtle border-t bg-canvas
|
|
220
|
+
className: cn("relative z-20 flex min-h-9 w-full cursor-pointer items-center gap-2 rounded-none rounded-b-[inherit] border-line-subtle border-t bg-canvas py-2 text-left text-gdt-sm font-semibold text-fg-secondary outline-none transition-colors hover:bg-surface focus-visible:bg-surface focus-visible:inset-ring-2 focus-visible:inset-ring-line-focus active:bg-surface-raised disabled:pointer-events-none disabled:text-fg-caption-placeholder", "[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", responsiveValueClasses(useComponentSizeValue(), responsiveSelectEdgeSizes).className, className),
|
|
187
221
|
...props
|
|
188
222
|
});
|
|
189
223
|
}
|
|
190
224
|
function SelectLabel({ className, ...props }) {
|
|
225
|
+
const resolvedSize = responsiveValueClasses(useComponentSizeValue(), responsiveSelectLabelSizes);
|
|
191
226
|
return /* @__PURE__ */ jsx(Select$1.GroupLabel, {
|
|
192
227
|
"data-slot": "select-label",
|
|
193
|
-
className: cn("
|
|
228
|
+
className: cn("py-1 text-gdt-xs text-fg-secondary", resolvedSize.className, className),
|
|
194
229
|
...props
|
|
195
230
|
});
|
|
196
231
|
}
|
package/dist/sheet.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ type SheetContentProps = Dialog.Popup.Props & {
|
|
|
9
9
|
side?: SheetSide;
|
|
10
10
|
showCloseButton?: boolean;
|
|
11
11
|
showOverlay?: boolean;
|
|
12
|
+
/** Controls the visual response when this surface participates in a nested dialog stack. */
|
|
13
|
+
nestingEffect?: "stack" | "none";
|
|
12
14
|
/** @internal Positions specialized popup shells without changing the surface. */
|
|
13
15
|
popupClassName?: string;
|
|
14
16
|
} & ({
|
|
@@ -18,7 +20,7 @@ type SheetContentProps = Dialog.Popup.Props & {
|
|
|
18
20
|
side?: "left" | "right";
|
|
19
21
|
size?: "sm" | "md" | "lg";
|
|
20
22
|
});
|
|
21
|
-
declare function SheetContent({ className, children, side, size, showCloseButton, showOverlay, popupClassName, ...props }: SheetContentProps): React.JSX.Element;
|
|
23
|
+
declare function SheetContent({ className, children, side, size, showCloseButton, showOverlay, nestingEffect, popupClassName, ...props }: SheetContentProps): React.JSX.Element;
|
|
22
24
|
declare function SheetHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
23
25
|
declare function SheetBody({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
24
26
|
declare function SheetFooter({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
package/dist/sheet.js
CHANGED
|
@@ -3,16 +3,15 @@ import { X } from "./icons.js";
|
|
|
3
3
|
import { cn } from "./utils/cn.js";
|
|
4
4
|
import { SizeProvider } from "./size-context.js";
|
|
5
5
|
import { Button } from "./button.js";
|
|
6
|
-
import { DialogNestingProvider, useDialogNestingDepth } from "./dialog-nesting.js";
|
|
7
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
7
|
import { cva } from "class-variance-authority";
|
|
9
8
|
import { Dialog } from "@base-ui/react/dialog";
|
|
10
9
|
//#region src/sheet.tsx
|
|
11
10
|
function Sheet({ ...props }) {
|
|
12
|
-
return /* @__PURE__ */ jsx(
|
|
11
|
+
return /* @__PURE__ */ jsx(Dialog.Root, {
|
|
13
12
|
"data-slot": "sheet",
|
|
14
13
|
...props
|
|
15
|
-
})
|
|
14
|
+
});
|
|
16
15
|
}
|
|
17
16
|
function SheetTrigger({ ...props }) {
|
|
18
17
|
return /* @__PURE__ */ jsx(Dialog.Trigger, {
|
|
@@ -26,23 +25,22 @@ function SheetClose({ ...props }) {
|
|
|
26
25
|
...props
|
|
27
26
|
});
|
|
28
27
|
}
|
|
29
|
-
function SheetPortal({ ...props }) {
|
|
28
|
+
function SheetPortal({ className, ...props }) {
|
|
30
29
|
return /* @__PURE__ */ jsx(Dialog.Portal, {
|
|
31
30
|
"data-slot": "sheet-portal",
|
|
31
|
+
className: (state) => cn("group/sheet-portal", typeof className === "function" ? className(state) : className),
|
|
32
32
|
...props
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
function SheetOverlay({ className, ...props }) {
|
|
36
|
-
const nested = useDialogNestingDepth() > 1;
|
|
37
36
|
return /* @__PURE__ */ jsx(Dialog.Backdrop, {
|
|
38
37
|
forceRender: true,
|
|
39
38
|
"data-slot": "sheet-overlay",
|
|
40
|
-
"data-
|
|
41
|
-
className: cn("fixed inset-0 z-50 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0", nested ? "bg-black/25" : "bg-black/55 supports-backdrop-filter:backdrop-blur-xl", className),
|
|
39
|
+
className: cn("fixed inset-0 z-50 bg-black/55 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xl group-has-[>.is-nested-dialog]/sheet-portal:bg-black/25 supports-backdrop-filter:group-has-[>.is-nested-dialog]/sheet-portal:backdrop-blur-none", className),
|
|
42
40
|
...props
|
|
43
41
|
});
|
|
44
42
|
}
|
|
45
|
-
const sheetContentVariants = cva("group/sheet-content pointer-events-none fixed z-50 overflow-visible transition-[opacity,scale,translate] duration-[var(--duration-motion-surface)] ease-gdt-drawer data-ending-style:opacity-0 data-starting-style:opacity-0 [--sheet-padding-x:1rem] [--sheet-padding-top:1rem] data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:w-full data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:w-full data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=bottom]:data-ending-style:translate-y-full data-[side=bottom]:data-starting-style:translate-y-full data-[side=left]:data-ending-style:-translate-x-full data-[side=left]:data-starting-style:-translate-x-full data-[side=right]:data-ending-style:translate-x-full data-[side=right]:data-starting-style:translate-x-full data-[side=top]:data-ending-style:-translate-y-full data-[side=top]:data-starting-style:-translate-y-full motion-reduce:data-[side=bottom]:data-ending-style:translate-y-0 motion-reduce:data-[side=bottom]:data-starting-style:translate-y-0 motion-reduce:data-[side=left]:data-ending-style:translate-x-0 motion-reduce:data-[side=left]:data-starting-style:translate-x-0 motion-reduce:data-[side=right]:data-ending-style:translate-x-0 motion-reduce:data-[side=right]:data-starting-style:translate-x-0 motion-reduce:data-[side=top]:data-ending-style:translate-y-0 motion-reduce:data-[side=top]:data-starting-style:translate-y-0
|
|
43
|
+
const sheetContentVariants = cva("group/sheet-content pointer-events-none fixed z-50 overflow-visible transition-[opacity,scale,translate] duration-[var(--duration-motion-surface)] ease-gdt-drawer data-ending-style:opacity-0 data-starting-style:opacity-0 [--sheet-padding-x:1rem] [--sheet-padding-top:1rem] data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:w-full data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:w-full data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=bottom]:data-ending-style:translate-y-full data-[side=bottom]:data-starting-style:translate-y-full data-[side=left]:data-ending-style:-translate-x-full data-[side=left]:data-starting-style:-translate-x-full data-[side=right]:data-ending-style:translate-x-full data-[side=right]:data-starting-style:translate-x-full data-[side=top]:data-ending-style:-translate-y-full data-[side=top]:data-starting-style:-translate-y-full motion-reduce:data-[side=bottom]:data-ending-style:translate-y-0 motion-reduce:data-[side=bottom]:data-starting-style:translate-y-0 motion-reduce:data-[side=left]:data-ending-style:translate-x-0 motion-reduce:data-[side=left]:data-starting-style:translate-x-0 motion-reduce:data-[side=right]:data-ending-style:translate-x-0 motion-reduce:data-[side=right]:data-starting-style:translate-x-0 motion-reduce:data-[side=top]:data-ending-style:translate-y-0 motion-reduce:data-[side=top]:data-starting-style:translate-y-0", {
|
|
46
44
|
variants: { size: {
|
|
47
45
|
sm: "sm:[--sheet-padding-top:1.5rem] sm:[--sheet-padding-x:1.5rem] w-90 max-w-[calc(100vw-2.5rem)] [--sheet-title:var(--text-gdt-h5)]",
|
|
48
46
|
md: "sm:[--sheet-padding-top:1.5rem] sm:[--sheet-padding-x:2rem] xl:[--sheet-padding-x:2.5rem] w-120 max-w-[calc(100vw-2.5rem)] [--sheet-title:var(--text-gdt-h4)]",
|
|
@@ -51,7 +49,7 @@ const sheetContentVariants = cva("group/sheet-content pointer-events-none fixed
|
|
|
51
49
|
} },
|
|
52
50
|
defaultVariants: { size: "md" }
|
|
53
51
|
});
|
|
54
|
-
function SheetContent({ className, children, side = "left", size = "md", showCloseButton = true, showOverlay = true, popupClassName, ...props }) {
|
|
52
|
+
function SheetContent({ className, children, side = "left", size = "md", showCloseButton = true, showOverlay = true, nestingEffect = "stack", popupClassName, ...props }) {
|
|
55
53
|
const resolvedSize = side === "top" || side === "bottom" ? "fullscreen" : size !== "fullscreen" ? size : "md";
|
|
56
54
|
return /* @__PURE__ */ jsxs(SheetPortal, { children: [showOverlay && /* @__PURE__ */ jsx(SheetOverlay, {}), /* @__PURE__ */ jsx(SizeProvider, {
|
|
57
55
|
size: resolvedSize === "fullscreen" ? "lg" : resolvedSize,
|
|
@@ -59,7 +57,8 @@ function SheetContent({ className, children, side = "left", size = "md", showClo
|
|
|
59
57
|
"data-slot": "sheet-content",
|
|
60
58
|
"data-side": side,
|
|
61
59
|
"data-size": resolvedSize,
|
|
62
|
-
|
|
60
|
+
"data-nesting-effect": nestingEffect,
|
|
61
|
+
className: (state) => cn(sheetContentVariants({ size: resolvedSize }), nestingEffect === "stack" && state.nested && "is-nested-dialog", nestingEffect === "stack" && "data-[side=bottom]:data-[nested-dialog-open]:translate-y-[calc(var(--nested-dialogs)*-1.5rem)] data-[side=top]:data-[nested-dialog-open]:translate-y-[calc(var(--nested-dialogs)*1.5rem)] data-[side=left]:data-[nested-dialog-open]:translate-x-[calc(var(--nested-dialogs)*1.5rem)] data-[side=right]:data-[nested-dialog-open]:translate-x-[calc(var(--nested-dialogs)*-1.5rem)] data-[side=bottom]:data-[nested-dialog-open]:scale-x-[calc(1_-_var(--nested-dialogs)*0.05)] data-[side=top]:data-[nested-dialog-open]:scale-x-[calc(1_-_var(--nested-dialogs)*0.05)]", popupClassName),
|
|
63
62
|
...props,
|
|
64
63
|
children: [/* @__PURE__ */ jsx("div", {
|
|
65
64
|
"data-slot": "sheet-surface",
|
|
@@ -69,7 +68,7 @@ function SheetContent({ className, children, side = "left", size = "md", showClo
|
|
|
69
68
|
"data-slot": "sheet-close",
|
|
70
69
|
render: /* @__PURE__ */ jsxs(Button, {
|
|
71
70
|
variant: "ghost",
|
|
72
|
-
className: "pointer-events-auto absolute z-10 size-7 border border-line bg-surface p-0 text-fg-primary shadow-(--elevation-e1-shadow) hover:bg-surface-raised group-data-[
|
|
71
|
+
className: cn("pointer-events-auto absolute z-10 size-7 border border-line bg-surface p-0 text-fg-primary shadow-(--elevation-e1-shadow) hover:bg-surface-raised group-data-[side=bottom]/sheet-content:-top-9 group-data-[side=bottom]/sheet-content:right-4 group-data-[side=top]/sheet-content:-bottom-9 group-data-[side=top]/sheet-content:right-4 group-data-[side=left]/sheet-content:-right-9 group-data-[side=left]/sheet-content:top-4 group-data-[side=right]/sheet-content:-left-9 group-data-[side=right]/sheet-content:top-4", nestingEffect === "stack" && "group-data-[nested-dialog-open]/sheet-content:hidden"),
|
|
73
72
|
size: "icon-xs",
|
|
74
73
|
children: [/* @__PURE__ */ jsx(X, {}), /* @__PURE__ */ jsx("span", {
|
|
75
74
|
className: "sr-only",
|
package/dist/sonar.d.ts
CHANGED
|
@@ -44,7 +44,9 @@ type SonarProps = React.ComponentProps<"span"> & VariantProps<typeof sonarWaveVa
|
|
|
44
44
|
* child.
|
|
45
45
|
*
|
|
46
46
|
* The rings are painted with `box-shadow` spread, which lives outside the
|
|
47
|
-
* element's box, so an ancestor with `overflow: hidden` will clip them.
|
|
47
|
+
* element's box, so an ancestor with `overflow: hidden` will clip them. They
|
|
48
|
+
* sit behind the child with its shape punched out of them, so nothing paints
|
|
49
|
+
* inside it whatever background — or none — the child has of its own.
|
|
48
50
|
*/
|
|
49
51
|
declare function Sonar({ className, children, active, waves, size, tone, radius, style, ...props }: SonarProps): React.JSX.Element;
|
|
50
52
|
//#endregion
|
package/dist/sonar.js
CHANGED
|
@@ -18,13 +18,56 @@ const SQUARE = {
|
|
|
18
18
|
borderBottomLeftRadius: "0px"
|
|
19
19
|
};
|
|
20
20
|
function sameCorners(a, b) {
|
|
21
|
-
|
|
21
|
+
if (a === null || b === null) return a === b;
|
|
22
|
+
return a.borderTopLeftRadius === b.borderTopLeftRadius && a.borderTopRightRadius === b.borderTopRightRadius && a.borderBottomRightRadius === b.borderBottomRightRadius && a.borderBottomLeftRadius === b.borderBottomLeftRadius;
|
|
23
|
+
}
|
|
24
|
+
function sameMetrics(a, b) {
|
|
25
|
+
return a !== null && a.scaleX === b.scaleX && a.scaleY === b.scaleY && sameCorners(a.corners, b.corners);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The ring is painted at its full travel and scaled *back*, so this is where it
|
|
29
|
+
* starts: small enough that its outer edge lands exactly on the child's own,
|
|
30
|
+
* with nothing showing yet. Growing from there to scale 1 moves that edge out
|
|
31
|
+
* by `spread` — the same distance the old spread animation moved it.
|
|
32
|
+
*
|
|
33
|
+
* Worked out per axis on purpose: one factor for both would move a 200x40
|
|
34
|
+
* button's ring five times further sideways than vertically, and equal travel
|
|
35
|
+
* on every side is the whole look.
|
|
36
|
+
*/
|
|
37
|
+
function startScale(extent, spread) {
|
|
38
|
+
if (extent <= 0) return 1;
|
|
39
|
+
return Math.round(extent / (extent + 2 * spread) * 1e3) / 1e3;
|
|
40
|
+
}
|
|
41
|
+
function readCorners(target) {
|
|
42
|
+
if (!target) return SQUARE;
|
|
43
|
+
const computed = getComputedStyle(target);
|
|
44
|
+
return {
|
|
45
|
+
borderTopLeftRadius: computed.borderTopLeftRadius,
|
|
46
|
+
borderTopRightRadius: computed.borderTopRightRadius,
|
|
47
|
+
borderBottomRightRadius: computed.borderBottomRightRadius,
|
|
48
|
+
borderBottomLeftRadius: computed.borderBottomLeftRadius
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* `--sonar-spread` is authored in rem, and `getComputedStyle` hands custom
|
|
53
|
+
* properties back as written rather than resolved to pixels, so this does the
|
|
54
|
+
* one conversion the token is ever written to need. Reading it from the DOM
|
|
55
|
+
* rather than the map below keeps a `[--sonar-spread:…]` override on the
|
|
56
|
+
* wrapper driving the travel as well as the ring.
|
|
57
|
+
*/
|
|
58
|
+
function spreadInPixels(host) {
|
|
59
|
+
const raw = getComputedStyle(host).getPropertyValue("--sonar-spread").trim();
|
|
60
|
+
const value = Number.parseFloat(raw);
|
|
61
|
+
if (!Number.isFinite(value)) return 10;
|
|
62
|
+
if (!raw.endsWith("rem")) return value;
|
|
63
|
+
const root = Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
64
|
+
return value * (Number.isFinite(root) ? root : 16);
|
|
22
65
|
}
|
|
23
66
|
/**
|
|
24
67
|
* Colour rides on `currentColor` so the keyframe stays tone-agnostic: one set
|
|
25
68
|
* of keyframes, one class per tone.
|
|
26
69
|
*/
|
|
27
|
-
const sonarWaveVariants = cva(cn("pointer-events-none absolute inset-0 animate-sonar-wave will-change-[
|
|
70
|
+
const sonarWaveVariants = cva(cn("pointer-events-none absolute inset-0 animate-sonar-wave opacity-0 shadow-[0_0_0_var(--sonar-spread,0.625rem)_currentColor] will-change-[transform,opacity] [backface-visibility:hidden]", "motion-reduce:animate-none motion-reduce:opacity-40 motion-reduce:shadow-[0_0_0_2px_currentColor]"), {
|
|
28
71
|
variants: { tone: {
|
|
29
72
|
brand: "text-fg-brand",
|
|
30
73
|
info: "text-status-info",
|
|
@@ -35,9 +78,46 @@ const sonarWaveVariants = cva(cn("pointer-events-none absolute inset-0 animate-s
|
|
|
35
78
|
defaultVariants: { tone: "brand" }
|
|
36
79
|
});
|
|
37
80
|
/**
|
|
81
|
+
* The cut-out the rings are painted through: everything from the child's own
|
|
82
|
+
* edge out to one `--sonar-spread`, and nothing inside it.
|
|
83
|
+
*
|
|
84
|
+
* A ring is painted at full spread and scaled back to start, which means that
|
|
85
|
+
* early in its travel it lies *within* the child rather than around it. Behind
|
|
86
|
+
* a child with a background of its own that goes unseen, but through an outline
|
|
87
|
+
* button it is a band sweeping across the middle. The child's shape is punched
|
|
88
|
+
* out of the rings here instead, so what shows is only ever the halo.
|
|
89
|
+
*
|
|
90
|
+
* A transparent border, rather than padding, because it is the padding box that
|
|
91
|
+
* absolutely positioned rings are laid out against: this way the border box is
|
|
92
|
+
* the far edge of the travel, the padding box is the child, and each ring can
|
|
93
|
+
* go on being `inset-0`. CSS shrinks the corner radii between the two boxes by
|
|
94
|
+
* the border width on its own, so the hole traces the child's corners exactly.
|
|
95
|
+
*/
|
|
96
|
+
const sonarRingsClass = cn("pointer-events-none absolute -z-10 [inset:calc(var(--sonar-spread,0.625rem)*-1)] [border:var(--sonar-spread,0.625rem)_solid_transparent]", "[mask-image:linear-gradient(#000,#000),linear-gradient(#000,#000)] [mask-clip:border-box,padding-box] [mask-composite:exclude]");
|
|
97
|
+
/**
|
|
98
|
+
* The child's corner, grown by one spread, for the box the cut-out is measured
|
|
99
|
+
* off. Written per token because a computed radius can be elliptical ("8px
|
|
100
|
+
* 4px") or, from the `radius` prop, a whole shorthand — each length grows, and
|
|
101
|
+
* the "/" between horizontal and vertical radii is passed through untouched.
|
|
102
|
+
*/
|
|
103
|
+
function growRadius(value) {
|
|
104
|
+
return value.trim().split(/\s+/).map((token) => token === "/" ? token : `calc(${token} + var(--sonar-spread, 0.625rem))`).join(" ");
|
|
105
|
+
}
|
|
106
|
+
/** The same shape as the rings, one spread wider on every side. */
|
|
107
|
+
function grownShape(shape) {
|
|
108
|
+
return Object.fromEntries(Object.entries(shape).map(([property, value]) => [property, growRadius(String(value))]));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
38
111
|
* Rings evenly spaced across one cycle, so the last finishes just as the first
|
|
39
112
|
* comes round again. Doubles as each ring's key — the offsets are distinct by
|
|
40
113
|
* construction, which an index would only pretend to be.
|
|
114
|
+
*
|
|
115
|
+
* Applied as *negative* delays, which start every ring on the first frame at a
|
|
116
|
+
* different point in the cycle rather than holding it still until its turn
|
|
117
|
+
* comes round. A ring waiting out a positive delay shows the style it is
|
|
118
|
+
* animating from — here a finished, full-spread, fully opaque ring — so the
|
|
119
|
+
* sonar would open on a solid halo and snap into motion three quarters of a
|
|
120
|
+
* second later.
|
|
41
121
|
*/
|
|
42
122
|
function waveDelays(waves) {
|
|
43
123
|
return Array.from({ length: waves }, (_, index) => Math.round(index * SONAR_DURATION_MS / waves));
|
|
@@ -63,54 +143,62 @@ const sonarSpread = {
|
|
|
63
143
|
* child.
|
|
64
144
|
*
|
|
65
145
|
* The rings are painted with `box-shadow` spread, which lives outside the
|
|
66
|
-
* element's box, so an ancestor with `overflow: hidden` will clip them.
|
|
146
|
+
* element's box, so an ancestor with `overflow: hidden` will clip them. They
|
|
147
|
+
* sit behind the child with its shape punched out of them, so nothing paints
|
|
148
|
+
* inside it whatever background — or none — the child has of its own.
|
|
67
149
|
*/
|
|
68
150
|
function Sonar({ className, children, active = true, waves = 2, size, tone, radius, style, ...props }) {
|
|
69
151
|
const resolvedSize = useComponentSize(size);
|
|
70
152
|
const ref = React.useRef(null);
|
|
71
|
-
const [
|
|
153
|
+
const [metrics, setMetrics] = React.useState(null);
|
|
72
154
|
React.useEffect(() => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
setCorners(SQUARE);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
155
|
+
const host = ref.current;
|
|
156
|
+
if (!host || !active) return;
|
|
157
|
+
const target = radius === void 0 ? host.querySelector(":scope > :not([data-slot=\"sonar-rings\"])") : null;
|
|
79
158
|
const measure = () => {
|
|
80
|
-
const
|
|
159
|
+
const box = host.getBoundingClientRect();
|
|
160
|
+
const spread = spreadInPixels(host);
|
|
81
161
|
const next = {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
borderBottomLeftRadius: computed.borderBottomLeftRadius
|
|
162
|
+
corners: radius !== void 0 ? null : readCorners(target),
|
|
163
|
+
scaleX: startScale(box.width, spread),
|
|
164
|
+
scaleY: startScale(box.height, spread)
|
|
86
165
|
};
|
|
87
|
-
|
|
166
|
+
setMetrics((current) => sameMetrics(current, next) ? current : next);
|
|
88
167
|
};
|
|
89
168
|
measure();
|
|
90
169
|
if (typeof ResizeObserver === "undefined") return;
|
|
91
170
|
const observer = new ResizeObserver(measure);
|
|
92
|
-
observer.observe(
|
|
171
|
+
observer.observe(host);
|
|
93
172
|
return () => observer.disconnect();
|
|
94
173
|
}, [active, radius]);
|
|
95
|
-
const shape = radius !== void 0 ? { borderRadius: radius } : corners;
|
|
174
|
+
const shape = radius !== void 0 ? { borderRadius: radius } : metrics?.corners ?? null;
|
|
96
175
|
return /* @__PURE__ */ jsxs("span", {
|
|
97
176
|
ref,
|
|
98
177
|
"data-slot": "sonar",
|
|
99
178
|
"data-tone": tone ?? "brand",
|
|
100
179
|
"data-size": resolvedSize,
|
|
101
180
|
"data-active": active ? "" : void 0,
|
|
102
|
-
className: cn("relative inline-flex w-fit shrink-0", sonarSpread[resolvedSize], className),
|
|
103
|
-
style
|
|
181
|
+
className: cn("relative isolate inline-flex w-fit shrink-0", sonarSpread[resolvedSize], className),
|
|
182
|
+
style: metrics ? {
|
|
183
|
+
"--sonar-scale-x": metrics.scaleX,
|
|
184
|
+
"--sonar-scale-y": metrics.scaleY,
|
|
185
|
+
...style
|
|
186
|
+
} : style,
|
|
104
187
|
...props,
|
|
105
|
-
children: [children, active && shape ?
|
|
188
|
+
children: [children, active && shape ? /* @__PURE__ */ jsx("span", {
|
|
106
189
|
"aria-hidden": "true",
|
|
107
|
-
"data-slot": "sonar-
|
|
108
|
-
style:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
190
|
+
"data-slot": "sonar-rings",
|
|
191
|
+
style: grownShape(shape),
|
|
192
|
+
className: sonarRingsClass,
|
|
193
|
+
children: waveDelays(waves).map((delay) => /* @__PURE__ */ jsx("span", {
|
|
194
|
+
"data-slot": "sonar-wave",
|
|
195
|
+
style: {
|
|
196
|
+
...shape,
|
|
197
|
+
animationDelay: delay === 0 ? void 0 : `-${delay}ms`
|
|
198
|
+
},
|
|
199
|
+
className: cn(sonarWaveVariants({ tone }), delay > 0 && "motion-reduce:hidden")
|
|
200
|
+
}, `sonar-wave-${delay}`))
|
|
201
|
+
}) : null]
|
|
114
202
|
});
|
|
115
203
|
}
|
|
116
204
|
//#endregion
|