@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/dist/match.js CHANGED
@@ -3,7 +3,7 @@ import { ChevronDown } from "./icons.js";
3
3
  import { cn } from "./utils/cn.js";
4
4
  import { resolveResponsiveComponentSize, useComponentSizeValue } from "./size-context.js";
5
5
  import { CircularProgress } from "./progress.js";
6
- import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
  import { cva } from "class-variance-authority";
8
8
  //#region src/match.tsx
9
9
  const matchVariants = cva("inline-flex w-fit shrink-0 items-center rounded-full border border-line-subtle bg-surface text-fg-primary shadow-none transition-[background-color,border-color,box-shadow] outline-none focus-visible:border-line-focus focus-visible:shadow-[0px_0px_0px_2px_var(--color-line-focus)] disabled:pointer-events-none disabled:opacity-60", {
@@ -83,54 +83,71 @@ function normalizeMatchValue(value) {
83
83
  if (!Number.isFinite(value)) return 0;
84
84
  return Math.min(100, Math.max(0, Math.round(value)));
85
85
  }
86
- function Match({ className, value, variant = "bar", size, label = "Match", showChevron = true, type = "button", "aria-label": ariaLabel, ...props }) {
86
+ function Match({ className, value, variant = "bar", size, label = "Match", showChevron, readOnly, type = "button", disabled, onClick, ref, "aria-label": ariaLabel, ...props }) {
87
87
  const sizeValue = useComponentSizeValue(size);
88
88
  const resolvedSizes = resolveResponsiveComponentSize(sizeValue);
89
89
  const responsive = typeof sizeValue !== "string";
90
90
  const resolvedVariant = variant ?? "bar";
91
91
  const progress = normalizeMatchValue(value);
92
92
  const text = `${progress}% ${label}`;
93
+ const interactive = readOnly === void 0 ? onClick !== void 0 : !readOnly;
94
+ const chevron = showChevron ?? interactive;
93
95
  const rootSizeClasses = cn(matchVariants({
94
96
  variant: resolvedVariant,
95
97
  size: resolvedSizes.base
96
98
  }), responsive && matchBreakpointClasses(resolvedVariant, "md", resolvedSizes.md), responsive && matchBreakpointClasses(resolvedVariant, "lg", resolvedSizes.lg));
97
- return /* @__PURE__ */ jsxs("button", {
98
- type,
99
+ const rootProps = {
99
100
  "aria-label": ariaLabel ?? text,
100
101
  "data-slot": "match",
101
102
  "data-variant": resolvedVariant,
102
103
  "data-size": resolvedSizes.base,
103
104
  "data-size-md": responsive ? resolvedSizes.md : void 0,
104
105
  "data-size-lg": responsive ? resolvedSizes.lg : void 0,
105
- className: cn(rootSizeClasses, className),
106
+ className: cn(rootSizeClasses, className)
107
+ };
108
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
109
+ resolvedVariant === "donut" ? /* @__PURE__ */ jsx(MatchDonut, {
110
+ progress,
111
+ sizes: resolvedSizes,
112
+ responsive
113
+ }) : null,
114
+ /* @__PURE__ */ jsx("span", {
115
+ "data-slot": "match-label",
116
+ className: "shrink-0 font-bold",
117
+ children: text
118
+ }),
119
+ resolvedVariant === "bar" ? /* @__PURE__ */ jsx("span", {
120
+ "aria-hidden": "true",
121
+ "data-slot": "match-bar-track",
122
+ className: cn("relative shrink-0 overflow-hidden rounded-full bg-surface-raised", matchBarSizes[resolvedSizes.base], responsive && responsiveClass("md", matchBarSizes[resolvedSizes.md]), responsive && responsiveClass("lg", matchBarSizes[resolvedSizes.lg])),
123
+ children: /* @__PURE__ */ jsx("span", {
124
+ "data-slot": "match-bar-fill",
125
+ className: "absolute inset-y-0 left-0 rounded-full bg-action-primary",
126
+ style: { width: `${progress}%` }
127
+ })
128
+ }) : null,
129
+ chevron ? /* @__PURE__ */ jsx(ChevronDown, {
130
+ "aria-hidden": "true",
131
+ "data-slot": "match-chevron",
132
+ className: cn("shrink-0 text-fg-secondary", matchChevronSizes[resolvedSizes.base], responsive && responsiveClass("md", matchChevronSizes[resolvedSizes.md]), responsive && responsiveClass("lg", matchChevronSizes[resolvedSizes.lg]))
133
+ }) : null
134
+ ] });
135
+ if (!interactive) return /* @__PURE__ */ jsx("span", {
136
+ ...rootProps,
137
+ "data-readonly": "true",
138
+ role: "img",
139
+ ref,
140
+ ...props,
141
+ children: content
142
+ });
143
+ return /* @__PURE__ */ jsx("button", {
144
+ ...rootProps,
145
+ type,
146
+ disabled,
147
+ onClick,
148
+ ref,
106
149
  ...props,
107
- children: [
108
- resolvedVariant === "donut" ? /* @__PURE__ */ jsx(MatchDonut, {
109
- progress,
110
- sizes: resolvedSizes,
111
- responsive
112
- }) : null,
113
- /* @__PURE__ */ jsx("span", {
114
- "data-slot": "match-label",
115
- className: "shrink-0 font-bold",
116
- children: text
117
- }),
118
- resolvedVariant === "bar" ? /* @__PURE__ */ jsx("span", {
119
- "aria-hidden": "true",
120
- "data-slot": "match-bar-track",
121
- className: cn("relative shrink-0 overflow-hidden rounded-full bg-surface-raised", matchBarSizes[resolvedSizes.base], responsive && responsiveClass("md", matchBarSizes[resolvedSizes.md]), responsive && responsiveClass("lg", matchBarSizes[resolvedSizes.lg])),
122
- children: /* @__PURE__ */ jsx("span", {
123
- "data-slot": "match-bar-fill",
124
- className: "absolute inset-y-0 left-0 rounded-full bg-action-primary",
125
- style: { width: `${progress}%` }
126
- })
127
- }) : null,
128
- showChevron ? /* @__PURE__ */ jsx(ChevronDown, {
129
- "aria-hidden": "true",
130
- "data-slot": "match-chevron",
131
- className: cn("shrink-0 text-fg-secondary", matchChevronSizes[resolvedSizes.base], responsive && responsiveClass("md", matchChevronSizes[resolvedSizes.md]), responsive && responsiveClass("lg", matchChevronSizes[resolvedSizes.lg]))
132
- }) : null
133
- ]
150
+ children: content
134
151
  });
135
152
  }
136
153
  const responsiveSizeClasses = {
@@ -171,7 +171,7 @@ function MediaPlayerRoot({ audio, children, className, onBlurCapture, onFocusCap
171
171
  "data-slot": "media-player",
172
172
  role: "region",
173
173
  tabIndex: 0,
174
- className: cn("group/media-player relative isolate block w-full overflow-hidden rounded-2xl bg-fg-primary text-fg-on-brand shadow-2 outline-none", "data-[controls=hidden]:cursor-none", "data-[audio]:bg-[image:var(--gradient-accent-forest-to-brass)]", "data-[fullscreen]:rounded-none data-[fullscreen]:shadow-none", className),
174
+ className: cn("group/media-player relative isolate block w-full overflow-hidden rounded-2xl bg-fg-primary text-fg-on-brand shadow-2 outline-none [transform:translateZ(0)]", "data-[controls=hidden]:cursor-none", "data-[audio]:bg-[image:var(--gradient-accent-forest-to-brass)]", "data-[fullscreen]:rounded-none data-[fullscreen]:shadow-none", className),
175
175
  ...props,
176
176
  onBlurCapture: (event) => {
177
177
  onBlurCapture?.(event);
@@ -264,7 +264,7 @@ function MediaPlayerVideo({ className, ref, ...props }) {
264
264
  setRef(ref, node);
265
265
  }, [ref]),
266
266
  "data-slot": "media-player-video",
267
- className: cn("block aspect-video size-full rounded-[inherit] object-cover", "group-data-[fullscreen]/media-player:aspect-auto group-data-[fullscreen]/media-player:object-contain", className),
267
+ className: cn("block aspect-video size-full object-cover", "group-data-[fullscreen]/media-player:aspect-auto group-data-[fullscreen]/media-player:object-contain", className),
268
268
  playsInline: true,
269
269
  ...props
270
270
  });
@@ -891,7 +891,7 @@ function MediaPlayerError({ children, className, ...props }) {
891
891
  return /* @__PURE__ */ jsx("div", {
892
892
  "data-slot": "media-player-error",
893
893
  role: "alert",
894
- className: cn("absolute inset-0 z-20 flex flex-col items-center justify-center gap-1 rounded-[inherit] bg-canvas/85 px-6 text-center text-fg-primary backdrop-blur-sm", className),
894
+ className: cn("absolute inset-0 z-20 flex flex-col items-center justify-center gap-1 bg-canvas px-6 text-center text-fg-primary", className),
895
895
  ...props,
896
896
  children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
897
897
  /* @__PURE__ */ jsx(TriangleAlert, {
@@ -27,8 +27,8 @@ function MobileSidebarContent({ className, showCloseButton = false, ...props })
27
27
  side: "right",
28
28
  size: "sm",
29
29
  showCloseButton,
30
- popupClassName: "h-dvh w-[min(100vw,24.375rem)]",
31
- className: cn("h-dvh w-[min(100vw,24.375rem)] rounded-none border-y-0 border-r-0 bg-canvas [--sheet-padding-x:1.25rem] [--sheet-padding-top:0.8125rem]", className),
30
+ popupClassName: "h-dvh w-full max-w-[24.375rem]",
31
+ className: cn("h-dvh w-full rounded-none border-y-0 border-r-0 bg-canvas [--sheet-padding-x:1.25rem] [--sheet-padding-top:0.8125rem]", className),
32
32
  ...props
33
33
  });
34
34
  }
@@ -81,7 +81,7 @@ function MobileSidebarNav({ className, ...props }) {
81
81
  function MobileSidebarList({ className, ...props }) {
82
82
  return /* @__PURE__ */ jsx("ul", {
83
83
  "data-slot": "mobile-sidebar-list",
84
- className: cn("flex flex-col", className),
84
+ className: cn("-mx-4 flex flex-col gap-1", className),
85
85
  ...props
86
86
  });
87
87
  }
@@ -97,7 +97,7 @@ function MobileSidebarLink({ active = false, icon, className, children, render,
97
97
  defaultTagName: "a",
98
98
  props: mergeProps({
99
99
  "aria-current": active ? "page" : void 0,
100
- className: cn("group/mobile-sidebar-link grid min-h-11 w-full grid-cols-1 items-center rounded-xl px-2 py-2.5 text-left text-gdt-md font-semibold text-fg-primary outline-none transition-colors hover:bg-surface focus-visible:shadow-[0_0_0_2px_var(--color-line-focus)]", active && "bg-surface-brand-subtle text-fg-brand", className),
100
+ className: cn("group/mobile-sidebar-link grid min-h-11 w-full grid-cols-1 items-center rounded-xl px-4 py-2.5 text-left text-gdt-md font-semibold text-fg-primary outline-none transition-colors hover:bg-surface focus-visible:shadow-[0_0_0_2px_var(--color-line-focus)]", active && "bg-surface-brand-subtle text-fg-brand hover:bg-surface-brand-subtle", className),
101
101
  children: /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("span", {
102
102
  className: cn("grid min-w-0 items-center", icon ? "grid-cols-[1.25rem_minmax(0,1fr)] gap-3" : "grid-cols-1"),
103
103
  children: [icon ? /* @__PURE__ */ jsx("span", {
@@ -127,7 +127,7 @@ function MobileSidebarGroupTrigger({ active = false, icon, className, children,
127
127
  return /* @__PURE__ */ jsxs(CollapsibleTrigger, {
128
128
  "data-slot": "mobile-sidebar-group-trigger",
129
129
  "data-active": active || void 0,
130
- className: cn("grid min-h-11 w-full grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-xl px-2 py-2.5 text-left text-gdt-md font-semibold text-fg-primary outline-none transition-colors hover:bg-surface focus-visible:shadow-[0_0_0_2px_var(--color-line-focus)]", active && "bg-surface-brand-subtle text-fg-brand", className),
130
+ className: cn("grid min-h-11 w-full grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-xl px-4 py-2.5 text-left text-gdt-md font-semibold text-fg-primary outline-none transition-colors hover:bg-surface focus-visible:shadow-[0_0_0_2px_var(--color-line-focus)]", active && "bg-surface-brand-subtle text-fg-brand hover:bg-surface-brand-subtle", className),
131
131
  ...props,
132
132
  children: [/* @__PURE__ */ jsxs("span", {
133
133
  className: cn("grid min-w-0 items-center", icon ? "grid-cols-[1.25rem_minmax(0,1fr)] gap-3" : "grid-cols-1"),
@@ -154,7 +154,7 @@ function MobileSidebarGroupPanel({ className, ...props }) {
154
154
  function MobileSidebarSubList({ className, ...props }) {
155
155
  return /* @__PURE__ */ jsx("ul", {
156
156
  "data-slot": "mobile-sidebar-sub-list",
157
- className: cn("ml-8 flex flex-col pb-1", className),
157
+ className: cn("ml-8 flex flex-col gap-0.5 pb-2 pt-1", className),
158
158
  ...props
159
159
  });
160
160
  }
@@ -168,7 +168,7 @@ function MobileSidebarSubheading({ className, ...props }) {
168
168
  function MobileSidebarSubLink({ className, render, ...props }) {
169
169
  return useRender({
170
170
  defaultTagName: "a",
171
- props: mergeProps({ className: cn("block rounded-lg px-2 py-1.5 text-gdt-sm font-medium text-fg-secondary outline-none transition-colors hover:bg-surface hover:text-fg-primary focus-visible:shadow-[0_0_0_2px_var(--color-line-focus)]", className) }, props),
171
+ props: mergeProps({ className: cn("block rounded-lg px-4 py-1.5 text-gdt-sm font-medium text-fg-secondary outline-none transition-colors hover:bg-surface hover:text-fg-primary focus-visible:shadow-[0_0_0_2px_var(--color-line-focus)]", className) }, props),
172
172
  render,
173
173
  state: { slot: "mobile-sidebar-sub-link" }
174
174
  });
@@ -3,19 +3,36 @@ import * as React from "react";
3
3
  import { CountryCode } from "libphonenumber-js";
4
4
  //#region src/phone-input.d.ts
5
5
  interface PhoneInputProps {
6
+ /** Controlled E.164 value, e.g. `"+2348012345678"`. */
6
7
  value?: string;
7
8
  onChange?: (value: string | undefined) => void;
9
+ /** Country the field starts on, before anything is typed. */
8
10
  defaultCountry?: CountryCode;
9
11
  size?: ComponentSizeValue;
10
12
  disabled?: boolean;
11
13
  readOnly?: boolean;
12
14
  placeholder?: string;
15
+ /**
16
+ * Content pinned to the trailing edge of the field — a validity tick, a
17
+ * "Verify" button, an extension. It sits outside the number, so it never
18
+ * shifts as the number is formatted.
19
+ */
20
+ rightAddon?: React.ReactNode;
13
21
  className?: string;
14
22
  id?: string;
15
23
  name?: string;
16
24
  ref?: React.Ref<HTMLInputElement>;
17
25
  "aria-invalid"?: boolean;
18
26
  }
19
- declare function PhoneInput({ value, onChange, defaultCountry, size, disabled, readOnly, placeholder, className, id, name, ref, "aria-invalid": ariaInvalid }: PhoneInputProps): React.JSX.Element;
27
+ /**
28
+ * A phone field that takes a number written either way round.
29
+ *
30
+ * `0801 234 5678` and `+234 801 234 5678` are the same number, and both are
31
+ * accepted here: typing or pasting a leading `+` switches the field to
32
+ * international entry and moves the country selector to match, while a plain
33
+ * national number is read against the selected country, trunk prefix and all.
34
+ * `onChange` receives E.164 in both cases.
35
+ */
36
+ declare function PhoneInput({ value, onChange, defaultCountry, size, disabled, readOnly, placeholder, rightAddon, className, id, name, ref, "aria-invalid": ariaInvalid }: PhoneInputProps): React.JSX.Element;
20
37
  //#endregion
21
38
  export { type CountryCode, PhoneInput, type PhoneInputProps };
@@ -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
@@ -1,5 +1,5 @@
1
1
  import "./input-size-context-BMhves-J.js";
2
- import { r as PopoverContent } from "./popover-CNymD33m.js";
2
+ import { r as PopoverContent } from "./popover-BMTZp3AQ.js";
3
3
  import "./icons-C9JXR5Xv.js";
4
4
  import * as React from "react";
5
5
  //#region src/lib/picker.d.ts
@@ -4,11 +4,11 @@ import { Popover } from "@base-ui/react/popover";
4
4
  //#region src/popover.d.ts
5
5
  declare function Popover$1({ ...props }: Popover.Root.Props): React.JSX.Element;
6
6
  declare function PopoverTrigger({ ...props }: Popover.Trigger.Props): React.JSX.Element;
7
- type PopoverContentProps = Popover.Popup.Props & Pick<Popover.Positioner.Props, "align" | "alignOffset" | "collisionAvoidance" | "side" | "sideOffset"> & {
7
+ type PopoverContentProps = Popover.Popup.Props & Pick<Popover.Positioner.Props, "align" | "alignOffset" | "collisionAvoidance" | "positionMethod" | "side" | "sideOffset"> & {
8
8
  showArrow?: boolean;
9
9
  size?: ComponentSize;
10
10
  };
11
- declare function PopoverContent({ className, align, alignOffset, collisionAvoidance, side, sideOffset, showArrow, size, children, ...props }: PopoverContentProps): React.JSX.Element;
11
+ declare function PopoverContent({ className, align, alignOffset, collisionAvoidance, positionMethod, side, sideOffset, showArrow, size, children, ...props }: PopoverContentProps): React.JSX.Element;
12
12
  declare function PopoverHeader({ className, border, ...props }: React.ComponentProps<"div"> & {
13
13
  border?: boolean;
14
14
  }): React.JSX.Element;
package/dist/popover.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as PopoverFooter, c as PopoverTrigger, i as PopoverDescription, n as PopoverBody, o as PopoverHeader, r as PopoverContent, s as PopoverTitle, t as Popover } from "./popover-CNymD33m.js";
1
+ import { a as PopoverFooter, c as PopoverTrigger, i as PopoverDescription, n as PopoverBody, o as PopoverHeader, r as PopoverContent, s as PopoverTitle, t as Popover } from "./popover-BMTZp3AQ.js";
2
2
  export { Popover, PopoverBody, PopoverContent, PopoverDescription, PopoverFooter, PopoverHeader, PopoverTitle, PopoverTrigger };
package/dist/popover.js CHANGED
@@ -16,11 +16,12 @@ function PopoverTrigger({ ...props }) {
16
16
  ...props
17
17
  });
18
18
  }
19
- function PopoverContent({ className, align = "center", alignOffset = 0, collisionAvoidance, side = "bottom", sideOffset = 6, showArrow = true, size = "md", children, ...props }) {
19
+ function PopoverContent({ className, align = "center", alignOffset = 0, collisionAvoidance, positionMethod = "fixed", side = "bottom", sideOffset = 6, showArrow = true, size = "md", children, ...props }) {
20
20
  return /* @__PURE__ */ jsx(Popover$1.Portal, { children: /* @__PURE__ */ jsx(Popover$1.Positioner, {
21
21
  align,
22
22
  alignOffset,
23
23
  collisionAvoidance,
24
+ positionMethod,
24
25
  side,
25
26
  sideOffset,
26
27
  className: "isolate z-50",
@@ -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 };