@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.
@@ -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.tsx
14
- const COUNTRIES = getPhoneCountries();
15
- function getNationalDigits(value) {
16
- return value.replace(/\D/g, "");
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
- function formatNational(digits, country) {
19
- if (!digits) return "";
20
- return new AsYouType(country).input(digits);
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
- function toE164(digits, country) {
23
- if (!digits) return void 0;
24
- return `+${getCountryCallingCode(country)}${digits}`;
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
- function parseValue(value) {
27
- if (!value) return { digits: "" };
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
- country: parsed.country,
31
- digits: parsed.nationalNumber
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
- function PhoneInput({ value, onChange, defaultCountry = "NG", size, disabled = false, readOnly = false, placeholder = "(0000) 000-0000", className, id, name, ref, "aria-invalid": ariaInvalid }) {
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 initial = React.useMemo(() => parseValue(value), []);
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 (toE164(getNationalDigits(national), country) === value) return;
184
- const next = parseValue(value);
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 emit(digits, nextCountry) {
190
- onChange?.(toE164(digits, nextCountry));
252
+ function commit(next) {
253
+ setPhone(next);
254
+ if (next.value !== phone.value) onChange?.(next.value);
191
255
  }
192
256
  function handleInputChange(event) {
193
- const digits = getNationalDigits(event.target.value);
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
- const digits = getNationalDigits(national);
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: [/* @__PURE__ */ jsxs(InputGroupAddon, {
208
- align: "inline-start",
209
- className: "gap-2 self-stretch py-0",
210
- children: [/* @__PURE__ */ jsx(CountrySelect, {
211
- country,
212
- size: resolvedSize.base,
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
- onSelect: handleCountrySelect,
215
- finalFocusRef: inputRef
216
- }), /* @__PURE__ */ jsx(Separator, {
217
- orientation: "vertical",
218
- shade: "deep",
219
- className: cn("self-stretch transition-colors", dividerStateClasses)
220
- })]
221
- }), /* @__PURE__ */ jsx(InputGroupInput, {
222
- ref: setInputRef,
223
- id,
224
- name,
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
@@ -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 px-3 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", className),
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 px-3 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", className),
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("px-3 py-1 text-gdt-xs text-fg-secondary", className),
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(DialogNestingProvider, { children: /* @__PURE__ */ jsx(Dialog.Root, {
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-nested": nested || void 0,
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 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)]", {
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
- className: cn(sheetContentVariants({ size: resolvedSize }), popupClassName),
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-[nested-dialog-open]/sheet-content:hidden 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",
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",