@giddaa-housing/ui 3.6.0 → 3.7.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/calendar.js +1 -1
- package/dist/{combobox-CACVdp1R.js → combobox-D70DFHoM.js} +16 -3
- package/dist/combobox.js +1 -1
- package/dist/dialog.d.ts +4 -2
- package/dist/dialog.js +10 -11
- package/dist/infotip.js +0 -1
- package/dist/input-group.js +5 -5
- 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/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 +126 -22
- 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/phone-input.js
CHANGED
|
@@ -10,28 +10,87 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
import * as React from "react";
|
|
11
11
|
import { AsYouType, getCountryCallingCode, parsePhoneNumberFromString } from "libphonenumber-js";
|
|
12
12
|
import flags from "react-phone-number-input/flags";
|
|
13
|
-
//#region src/phone-input.
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
//#region src/lib/phone-input-value.ts
|
|
14
|
+
const EMPTY = {
|
|
15
|
+
text: "",
|
|
16
|
+
national: "",
|
|
17
|
+
international: false,
|
|
18
|
+
value: void 0
|
|
19
|
+
};
|
|
20
|
+
/** Drops everything a phone number cannot contain, keeping a leading `+`. */
|
|
21
|
+
function sanitizePhoneText(input) {
|
|
22
|
+
const digits = input.replace(/\D/g, "");
|
|
23
|
+
return input.trimStart().startsWith("+") ? `+${digits}` : digits;
|
|
17
24
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Groups national digits that `AsYouType` handed back untouched.
|
|
27
|
+
*
|
|
28
|
+
* Most countries only publish a national format that assumes the trunk prefix,
|
|
29
|
+
* so `AsYouType("NG")` formats `08161827754` as `0816 182 7754` but hands
|
|
30
|
+
* `8161827754` straight back in one run — and it formats neither until enough
|
|
31
|
+
* digits have arrived. The same digits always group under the international
|
|
32
|
+
* pattern, so that grouping is borrowed and the calling code taken back off.
|
|
33
|
+
*
|
|
34
|
+
* Whatever the reader typed in front of the national digits is theirs, and goes
|
|
35
|
+
* back where they left it. This only ever inserts separators: typing `081`
|
|
36
|
+
* shows `081`, never `81`.
|
|
37
|
+
*/
|
|
38
|
+
function groupNationalDigits(sanitized, national, country) {
|
|
39
|
+
if (!national || !sanitized.endsWith(national)) return "";
|
|
40
|
+
const callingCode = `+${getCountryCallingCode(country)}`;
|
|
41
|
+
const international = new AsYouType().input(`${callingCode}${national}`);
|
|
42
|
+
if (!international.startsWith(callingCode)) return "";
|
|
43
|
+
return sanitized.slice(0, sanitized.length - national.length) + international.slice(callingCode.length).trimStart();
|
|
21
44
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Reads what is in the field after a keystroke.
|
|
47
|
+
*
|
|
48
|
+
* `country` is the currently selected country; it is only overridden when the
|
|
49
|
+
* text is international *and* the digits so far name exactly one country —
|
|
50
|
+
* `+1` is still both the US and Canada, so the selector holds still until it
|
|
51
|
+
* is not.
|
|
52
|
+
*/
|
|
53
|
+
function derivePhoneValue(input, country) {
|
|
54
|
+
const sanitized = sanitizePhoneText(input);
|
|
55
|
+
const formatter = new AsYouType(country);
|
|
56
|
+
const formatted = formatter.input(sanitized);
|
|
57
|
+
const international = sanitized.startsWith("+");
|
|
58
|
+
const national = formatter.getNationalNumber();
|
|
59
|
+
return {
|
|
60
|
+
text: !international && formatted === sanitized && groupNationalDigits(sanitized, national, country) || formatted,
|
|
61
|
+
country: (international ? formatter.getCountry() : void 0) ?? country,
|
|
62
|
+
national,
|
|
63
|
+
international,
|
|
64
|
+
value: national ? formatter.getNumberValue() : void 0
|
|
65
|
+
};
|
|
25
66
|
}
|
|
26
|
-
|
|
27
|
-
|
|
67
|
+
/** Reads an externally supplied value — a form's default, a fetched record. */
|
|
68
|
+
function readPhoneValue(value, fallback) {
|
|
69
|
+
if (!value) return {
|
|
70
|
+
...EMPTY,
|
|
71
|
+
country: fallback
|
|
72
|
+
};
|
|
28
73
|
const parsed = parsePhoneNumberFromString(value);
|
|
29
|
-
if (parsed?.nationalNumber) return
|
|
30
|
-
|
|
31
|
-
|
|
74
|
+
if (!parsed?.nationalNumber) return derivePhoneValue(value, fallback);
|
|
75
|
+
if (!parsed.country) return derivePhoneValue(parsed.number, fallback);
|
|
76
|
+
return {
|
|
77
|
+
...derivePhoneValue(parsed.formatNational(), parsed.country),
|
|
78
|
+
country: parsed.country
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Rewrites the field for a country picked from the selector. The digits the
|
|
83
|
+
* reader typed are kept; only the code in front of them changes.
|
|
84
|
+
*/
|
|
85
|
+
function selectPhoneCountry(current, next) {
|
|
86
|
+
return {
|
|
87
|
+
...derivePhoneValue(current.international ? `+${getCountryCallingCode(next)}${current.national}` : current.national, next),
|
|
88
|
+
country: next
|
|
32
89
|
};
|
|
33
|
-
return { digits: "" };
|
|
34
90
|
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/phone-input.tsx
|
|
93
|
+
const COUNTRIES = getPhoneCountries();
|
|
35
94
|
const flagSizeVariants = {
|
|
36
95
|
sm: "h-3 w-[18px]",
|
|
37
96
|
md: "h-3.5 w-5",
|
|
@@ -110,7 +169,7 @@ function CountryList({ items, country, onSelect }) {
|
|
|
110
169
|
})
|
|
111
170
|
});
|
|
112
171
|
}
|
|
113
|
-
function CountrySelect({ country, size, disabled, onSelect, finalFocusRef }) {
|
|
172
|
+
function CountrySelect({ country, size, disabled, showCallingCode, onSelect, finalFocusRef }) {
|
|
114
173
|
const [open, setOpen] = React.useState(false);
|
|
115
174
|
const [query, setQuery] = React.useState("");
|
|
116
175
|
const searchRef = React.useRef(null);
|
|
@@ -140,7 +199,7 @@ function CountrySelect({ country, size, disabled, onSelect, finalFocusRef }) {
|
|
|
140
199
|
country,
|
|
141
200
|
size
|
|
142
201
|
}),
|
|
143
|
-
/* @__PURE__ */ jsxs("span", { children: ["+", getCountryCallingCode(country)] }),
|
|
202
|
+
showCallingCode ? /* @__PURE__ */ jsxs("span", { children: ["+", getCountryCallingCode(country)] }) : null,
|
|
144
203
|
/* @__PURE__ */ jsx(ChevronDown, { className: cn("text-fg-secondary transition-transform", open && "rotate-180", size === "lg" ? "size-4" : "size-3.5") })
|
|
145
204
|
]
|
|
146
205
|
}), /* @__PURE__ */ jsxs(PopoverContent, {
|
|
@@ -167,7 +226,16 @@ function CountrySelect({ country, size, disabled, onSelect, finalFocusRef }) {
|
|
|
167
226
|
})]
|
|
168
227
|
});
|
|
169
228
|
}
|
|
170
|
-
|
|
229
|
+
/**
|
|
230
|
+
* A phone field that takes a number written either way round.
|
|
231
|
+
*
|
|
232
|
+
* `0801 234 5678` and `+234 801 234 5678` are the same number, and both are
|
|
233
|
+
* accepted here: typing or pasting a leading `+` switches the field to
|
|
234
|
+
* international entry and moves the country selector to match, while a plain
|
|
235
|
+
* national number is read against the selected country, trunk prefix and all.
|
|
236
|
+
* `onChange` receives E.164 in both cases.
|
|
237
|
+
*/
|
|
238
|
+
function PhoneInput({ value, onChange, defaultCountry = "NG", size, disabled = false, readOnly = false, placeholder = "(0000) 000-0000", rightAddon, className, id, name, ref, "aria-invalid": ariaInvalid }) {
|
|
171
239
|
const sizeValue = useComponentSizeValue(size);
|
|
172
240
|
const resolvedSize = resolveResponsiveComponentSize(sizeValue);
|
|
173
241
|
const inputRef = React.useRef(null);
|
|
@@ -176,63 +244,64 @@ function PhoneInput({ value, onChange, defaultCountry = "NG", size, disabled = f
|
|
|
176
244
|
if (typeof ref === "function") ref(node);
|
|
177
245
|
else if (ref) ref.current = node;
|
|
178
246
|
}, [ref]);
|
|
179
|
-
const
|
|
180
|
-
const [country, setCountry] = React.useState(initial.country ?? defaultCountry);
|
|
181
|
-
const [national, setNational] = React.useState(() => formatNational(initial.digits, initial.country ?? defaultCountry));
|
|
247
|
+
const [phone, setPhone] = React.useState(() => readPhoneValue(value, defaultCountry));
|
|
182
248
|
React.useEffect(() => {
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
const nextCountry = next.country ?? country;
|
|
186
|
-
setCountry(nextCountry);
|
|
187
|
-
setNational(formatNational(next.digits, nextCountry));
|
|
249
|
+
if (phone.value === value) return;
|
|
250
|
+
setPhone(readPhoneValue(value, phone.country));
|
|
188
251
|
}, [value]);
|
|
189
|
-
function
|
|
190
|
-
|
|
252
|
+
function commit(next) {
|
|
253
|
+
setPhone(next);
|
|
254
|
+
if (next.value !== phone.value) onChange?.(next.value);
|
|
191
255
|
}
|
|
192
256
|
function handleInputChange(event) {
|
|
193
|
-
|
|
194
|
-
setNational(formatNational(digits, country));
|
|
195
|
-
emit(digits, country);
|
|
257
|
+
commit(derivePhoneValue(event.target.value, phone.country));
|
|
196
258
|
}
|
|
197
259
|
function handleCountrySelect(nextCountry) {
|
|
198
|
-
|
|
199
|
-
setCountry(nextCountry);
|
|
200
|
-
setNational(formatNational(digits, nextCountry));
|
|
201
|
-
emit(digits, nextCountry);
|
|
260
|
+
commit(selectPhoneCountry(phone, nextCountry));
|
|
202
261
|
}
|
|
203
262
|
const dividerStateClasses = cn("group-hover/input-group:bg-line-strong", "group-focus-within/input-group:bg-line-focus", "group-has-[[aria-invalid=true]]/input-group:bg-line-danger", "group-has-disabled/input-group:bg-line-subtle");
|
|
204
263
|
return /* @__PURE__ */ jsxs(InputGroup, {
|
|
205
264
|
size: sizeValue,
|
|
206
265
|
className,
|
|
207
|
-
children: [
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
266
|
+
children: [
|
|
267
|
+
/* @__PURE__ */ jsxs(InputGroupAddon, {
|
|
268
|
+
align: "inline-start",
|
|
269
|
+
className: "gap-2 self-stretch py-0",
|
|
270
|
+
children: [/* @__PURE__ */ jsx(CountrySelect, {
|
|
271
|
+
country: phone.country,
|
|
272
|
+
size: resolvedSize.base,
|
|
273
|
+
disabled,
|
|
274
|
+
showCallingCode: !phone.international,
|
|
275
|
+
onSelect: handleCountrySelect,
|
|
276
|
+
finalFocusRef: inputRef
|
|
277
|
+
}), /* @__PURE__ */ jsx(Separator, {
|
|
278
|
+
orientation: "vertical",
|
|
279
|
+
shade: "deep",
|
|
280
|
+
className: cn("self-stretch transition-colors", dividerStateClasses)
|
|
281
|
+
})]
|
|
282
|
+
}),
|
|
283
|
+
/* @__PURE__ */ jsx(InputGroupInput, {
|
|
284
|
+
ref: setInputRef,
|
|
285
|
+
id,
|
|
286
|
+
name,
|
|
287
|
+
type: "tel",
|
|
288
|
+
inputMode: "tel",
|
|
289
|
+
autoComplete: "tel",
|
|
290
|
+
value: phone.text,
|
|
291
|
+
onChange: handleInputChange,
|
|
292
|
+
placeholder,
|
|
213
293
|
disabled,
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
type: "tel",
|
|
226
|
-
inputMode: "tel",
|
|
227
|
-
autoComplete: "tel",
|
|
228
|
-
value: national,
|
|
229
|
-
onChange: handleInputChange,
|
|
230
|
-
placeholder,
|
|
231
|
-
disabled,
|
|
232
|
-
readOnly,
|
|
233
|
-
"aria-invalid": ariaInvalid || void 0,
|
|
234
|
-
className: "pl-0"
|
|
235
|
-
})]
|
|
294
|
+
readOnly,
|
|
295
|
+
"aria-invalid": ariaInvalid || void 0,
|
|
296
|
+
className: "pl-0"
|
|
297
|
+
}),
|
|
298
|
+
rightAddon ? /* @__PURE__ */ jsx(InputGroupAddon, {
|
|
299
|
+
align: "inline-end",
|
|
300
|
+
"data-slot": "phone-right-addon",
|
|
301
|
+
className: "gap-2 self-stretch py-0",
|
|
302
|
+
children: rightAddon
|
|
303
|
+
}) : null
|
|
304
|
+
]
|
|
236
305
|
});
|
|
237
306
|
}
|
|
238
307
|
//#endregion
|
package/dist/rating.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { n as ComponentSizeValue } from "./size-context-BVhHduLi.js";
|
|
2
|
+
import { j as IconProps } from "./icons-C9JXR5Xv.js";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
//#region src/rating.d.ts
|
|
5
|
+
/** How finely a rating can be *set*. Display always honours the exact value. */
|
|
6
|
+
type RatingPrecision = 1 | 0.5;
|
|
7
|
+
type RatingIcon = React.ComponentType<IconProps>;
|
|
8
|
+
interface RatingProps {
|
|
9
|
+
/** Controlled rating. Fractions render as partially filled icons. */
|
|
10
|
+
value?: number;
|
|
11
|
+
/** Starting rating when the component owns its own state. */
|
|
12
|
+
defaultValue?: number;
|
|
13
|
+
onValueChange?: (value: number) => void;
|
|
14
|
+
/** How many icons to render. */
|
|
15
|
+
max?: number;
|
|
16
|
+
/** Smallest increment a reader can pick. Display is not rounded to it. */
|
|
17
|
+
precision?: RatingPrecision;
|
|
18
|
+
size?: ComponentSizeValue;
|
|
19
|
+
/** Renders the score as an image instead of a control. */
|
|
20
|
+
readOnly?: boolean;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
required?: boolean;
|
|
23
|
+
/** Groups the radios and names the value in submitted form data. */
|
|
24
|
+
name?: string;
|
|
25
|
+
id?: string;
|
|
26
|
+
/** Accessible name for each option, e.g. `(value) => \`${value} stars\``. */
|
|
27
|
+
itemLabel?: (value: number, max: number) => string;
|
|
28
|
+
/** Swaps the glyph. Anything that takes SVG props and paints `currentColor`. */
|
|
29
|
+
icon?: RatingIcon;
|
|
30
|
+
/**
|
|
31
|
+
* Overrides the icon box for a size the `sm`/`md`/`lg` scale does not cover
|
|
32
|
+
* — `iconClassName="size-9"`. Merged after the scale, so it wins outright.
|
|
33
|
+
*/
|
|
34
|
+
iconClassName?: string;
|
|
35
|
+
className?: string;
|
|
36
|
+
onBlur?: React.FocusEventHandler<HTMLElement>;
|
|
37
|
+
/** The `div` that groups the radios, or the `span` when `readOnly`. */
|
|
38
|
+
ref?: React.Ref<HTMLElement>;
|
|
39
|
+
"aria-label"?: string;
|
|
40
|
+
"aria-labelledby"?: string;
|
|
41
|
+
"aria-describedby"?: string;
|
|
42
|
+
"aria-invalid"?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A star rating that reads and writes.
|
|
46
|
+
*
|
|
47
|
+
* Interactive ratings are a group of real radio inputs — one per selectable
|
|
48
|
+
* value — visually replaced by the icons. That is deliberate: it buys native
|
|
49
|
+
* arrow-key navigation, native form submission under `name`, and the
|
|
50
|
+
* "3 of 5, radio button 3 of 5" announcement, none of which a div with click
|
|
51
|
+
* handlers gets for free.
|
|
52
|
+
*
|
|
53
|
+
* ```tsx
|
|
54
|
+
* <Rating defaultValue={4} onValueChange={setRating} />
|
|
55
|
+
* <Rating value={4.3} readOnly />
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
declare function Rating({ value, defaultValue, onValueChange, max, precision, size, readOnly, disabled, required, name, id, itemLabel, icon, iconClassName, className, onBlur, ref, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid }: RatingProps): React.JSX.Element;
|
|
59
|
+
//#endregion
|
|
60
|
+
export { Rating, type RatingIcon, type RatingPrecision, type RatingProps };
|
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",
|