@open-mercato/ui 0.6.1-develop.3276.1.1fcd145078 → 0.6.1-develop.3287.1.450f4ffb56
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +23 -14
- package/dist/backend/AppShell.js +8 -32
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/CrudForm.js +18 -0
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +149 -53
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/FilterBar.js +11 -14
- package/dist/backend/FilterBar.js.map +2 -2
- package/dist/backend/columns/ColumnChooserPanel.js +4 -6
- package/dist/backend/columns/ColumnChooserPanel.js.map +2 -2
- package/dist/backend/filters/ActiveFilterChips.js +142 -0
- package/dist/backend/filters/ActiveFilterChips.js.map +7 -0
- package/dist/backend/filters/AdvancedFilterBuilder.js +689 -163
- package/dist/backend/filters/AdvancedFilterBuilder.js.map +2 -2
- package/dist/backend/filters/AdvancedFilterPanel.js +354 -0
- package/dist/backend/filters/AdvancedFilterPanel.js.map +7 -0
- package/dist/backend/filters/FilterEmptyState.js +41 -0
- package/dist/backend/filters/FilterEmptyState.js.map +7 -0
- package/dist/backend/filters/FilterFieldPicker.js +148 -0
- package/dist/backend/filters/FilterFieldPicker.js.map +7 -0
- package/dist/backend/filters/FilteredEmptyResults.js +26 -0
- package/dist/backend/filters/FilteredEmptyResults.js.map +7 -0
- package/dist/backend/filters/QuickFilters.js +41 -0
- package/dist/backend/filters/QuickFilters.js.map +7 -0
- package/dist/backend/filters/filterValidation.js +47 -0
- package/dist/backend/filters/filterValidation.js.map +7 -0
- package/dist/backend/filters/treeReducer.js +159 -0
- package/dist/backend/filters/treeReducer.js.map +7 -0
- package/dist/backend/hooks/useAdvancedFilter.js +82 -1
- package/dist/backend/hooks/useAdvancedFilter.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js +11 -7
- package/dist/backend/inputs/ComboboxInput.js.map +2 -2
- package/dist/backend/inputs/EventPatternInput.js +5 -2
- package/dist/backend/inputs/EventPatternInput.js.map +2 -2
- package/dist/backend/inputs/EventSelect.js +10 -4
- package/dist/backend/inputs/EventSelect.js.map +2 -2
- package/dist/backend/inputs/LookupSelect.js +51 -47
- package/dist/backend/inputs/LookupSelect.js.map +2 -2
- package/dist/backend/inputs/PhoneNumberField.js +196 -45
- package/dist/backend/inputs/PhoneNumberField.js.map +2 -2
- package/dist/backend/inputs/SwitchableMarkdownInput.js.map +2 -2
- package/dist/backend/messages/message-compose-form-groups.js +2 -2
- package/dist/backend/messages/message-compose-form-groups.js.map +2 -2
- package/dist/backend/utils/api.js +18 -1
- package/dist/backend/utils/api.js.map +2 -2
- package/dist/backend/utils/serverErrors.js +6 -2
- package/dist/backend/utils/serverErrors.js.map +2 -2
- package/dist/backend/utils/useAutoDiscoveredFields.js +1 -0
- package/dist/backend/utils/useAutoDiscoveredFields.js.map +2 -2
- package/dist/backend/utils/useCurrentUserId.js +31 -0
- package/dist/backend/utils/useCurrentUserId.js.map +7 -0
- package/dist/portal/utils/nav.js +17 -0
- package/dist/portal/utils/nav.js.map +2 -2
- package/dist/primitives/amount-input.js +128 -0
- package/dist/primitives/amount-input.js.map +7 -0
- package/dist/primitives/button-input.js +47 -0
- package/dist/primitives/button-input.js.map +7 -0
- package/dist/primitives/card-input.js +133 -0
- package/dist/primitives/card-input.js.map +7 -0
- package/dist/primitives/dialog.js +7 -5
- package/dist/primitives/dialog.js.map +2 -2
- package/dist/primitives/email-input.js +29 -0
- package/dist/primitives/email-input.js.map +7 -0
- package/dist/primitives/password-input.js +64 -0
- package/dist/primitives/password-input.js.map +7 -0
- package/dist/primitives/search-input.js +57 -0
- package/dist/primitives/search-input.js.map +7 -0
- package/dist/primitives/select.js +43 -5
- package/dist/primitives/select.js.map +2 -2
- package/dist/primitives/tag.js +4 -2
- package/dist/primitives/tag.js.map +2 -2
- package/dist/primitives/website-input.js +56 -0
- package/dist/primitives/website-input.js.map +7 -0
- package/package.json +3 -3
- package/src/backend/AppShell.tsx +7 -30
- package/src/backend/CrudForm.tsx +17 -0
- package/src/backend/DataTable.tsx +307 -63
- package/src/backend/FilterBar.tsx +5 -6
- package/src/backend/__tests__/DataTable.extensions.test.tsx +170 -0
- package/src/backend/__tests__/serverErrors.test.ts +79 -1
- package/src/backend/columns/ColumnChooserPanel.tsx +4 -5
- package/src/backend/filters/ActiveFilterChips.tsx +179 -0
- package/src/backend/filters/AdvancedFilterBuilder.tsx +840 -237
- package/src/backend/filters/AdvancedFilterPanel.tsx +456 -0
- package/src/backend/filters/FilterEmptyState.tsx +51 -0
- package/src/backend/filters/FilterFieldPicker.tsx +139 -0
- package/src/backend/filters/FilteredEmptyResults.tsx +40 -0
- package/src/backend/filters/QuickFilters.tsx +62 -0
- package/src/backend/filters/__tests__/ActiveFilterChips.test.tsx +75 -0
- package/src/backend/filters/__tests__/AdvancedFilterBuilder.test.tsx +210 -0
- package/src/backend/filters/__tests__/AdvancedFilterPanel.test.tsx +212 -0
- package/src/backend/filters/__tests__/FilterEmptyState.test.tsx +31 -0
- package/src/backend/filters/__tests__/FilterFieldPicker.test.tsx +61 -0
- package/src/backend/filters/__tests__/FilteredEmptyResults.test.tsx +35 -0
- package/src/backend/filters/__tests__/QuickFilters.test.tsx +34 -0
- package/src/backend/filters/__tests__/filterValidation.test.ts +69 -0
- package/src/backend/filters/__tests__/treeReducer.test.ts +189 -0
- package/src/backend/filters/filterValidation.ts +61 -0
- package/src/backend/filters/treeReducer.ts +201 -0
- package/src/backend/hooks/__tests__/useAdvancedFilter.test.tsx +213 -0
- package/src/backend/hooks/useAdvancedFilter.ts +130 -0
- package/src/backend/inputs/ComboboxInput.tsx +32 -26
- package/src/backend/inputs/EventPatternInput.tsx +11 -2
- package/src/backend/inputs/EventSelect.tsx +13 -4
- package/src/backend/inputs/LookupSelect.tsx +75 -53
- package/src/backend/inputs/PhoneNumberField.tsx +237 -47
- package/src/backend/inputs/SwitchableMarkdownInput.tsx +30 -0
- package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +4 -0
- package/src/backend/inputs/__tests__/PhoneNumberField.test.tsx +4 -0
- package/src/backend/messages/message-compose-form-groups.tsx +2 -2
- package/src/backend/utils/api.ts +27 -2
- package/src/backend/utils/serverErrors.ts +15 -8
- package/src/backend/utils/useAutoDiscoveredFields.ts +2 -0
- package/src/backend/utils/useCurrentUserId.ts +40 -0
- package/src/portal/utils/__tests__/nav.test.ts +80 -0
- package/src/portal/utils/nav.ts +23 -1
- package/src/primitives/__tests__/password-input.test.tsx +93 -0
- package/src/primitives/__tests__/search-input.test.tsx +95 -0
- package/src/primitives/__tests__/zindex-overlay.test.tsx +26 -0
- package/src/primitives/amount-input.tsx +184 -0
- package/src/primitives/button-input.tsx +68 -0
- package/src/primitives/card-input.tsx +193 -0
- package/src/primitives/dialog.tsx +18 -7
- package/src/primitives/email-input.tsx +36 -0
- package/src/primitives/password-input.tsx +100 -0
- package/src/primitives/search-input.tsx +87 -0
- package/src/primitives/select.tsx +98 -11
- package/src/primitives/tag.tsx +3 -1
- package/src/primitives/website-input.tsx +72 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { Loader2, Search, X } from "lucide-react";
|
|
4
|
+
import { Check, Loader2, Search, X } from "lucide-react";
|
|
5
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
5
6
|
import { Button } from "../../primitives/button.js";
|
|
6
7
|
import { cn } from "@open-mercato/shared/lib/utils";
|
|
7
8
|
function LookupSelect({
|
|
@@ -14,19 +15,32 @@ function LookupSelect({
|
|
|
14
15
|
actionSlot,
|
|
15
16
|
onReady,
|
|
16
17
|
placeholder,
|
|
17
|
-
searchPlaceholder
|
|
18
|
-
clearLabel
|
|
19
|
-
emptyLabel
|
|
20
|
-
loadingLabel
|
|
21
|
-
selectLabel
|
|
22
|
-
selectedLabel
|
|
18
|
+
searchPlaceholder,
|
|
19
|
+
clearLabel,
|
|
20
|
+
emptyLabel,
|
|
21
|
+
loadingLabel,
|
|
22
|
+
selectLabel,
|
|
23
|
+
selectedLabel,
|
|
23
24
|
minQueryHintLabel,
|
|
24
|
-
startTypingLabel
|
|
25
|
+
startTypingLabel,
|
|
25
26
|
selectedHintLabel,
|
|
26
27
|
disabled = false,
|
|
27
28
|
loading: loadingProp = false,
|
|
28
29
|
defaultOpen = false
|
|
29
30
|
}) {
|
|
31
|
+
const t = useT();
|
|
32
|
+
const resolvedSearchPlaceholder = searchPlaceholder ?? placeholder ?? t("ui.lookupSelect.searchPlaceholder", "Search\u2026");
|
|
33
|
+
const resolvedClearLabel = clearLabel ?? t("ui.lookupSelect.clearSelection", "Clear selection");
|
|
34
|
+
const resolvedEmptyLabel = emptyLabel ?? t("ui.lookupSelect.noResults", "No results");
|
|
35
|
+
const resolvedLoadingLabel = loadingLabel ?? t("ui.lookupSelect.searching", "Searching\u2026");
|
|
36
|
+
const resolvedSelectLabel = selectLabel ?? t("ui.lookupSelect.select", "Select");
|
|
37
|
+
const resolvedSelectedLabel = selectedLabel ?? t("ui.lookupSelect.selected", "Selected");
|
|
38
|
+
const resolvedStartTypingLabel = startTypingLabel ?? t("ui.lookupSelect.startTyping", "Start typing to search.");
|
|
39
|
+
const resolvedMinQueryHintLabel = minQueryHintLabel ?? t(
|
|
40
|
+
"ui.lookupSelect.minQueryHint",
|
|
41
|
+
"Type at least {minQuery} characters or paste an id to search.",
|
|
42
|
+
{ minQuery: String(minQuery) }
|
|
43
|
+
);
|
|
30
44
|
const [query, setQuery] = React.useState("");
|
|
31
45
|
const [items, setItems] = React.useState(options ?? []);
|
|
32
46
|
const [loading, setLoading] = React.useState(false);
|
|
@@ -95,17 +109,17 @@ function LookupSelect({
|
|
|
95
109
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
96
110
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3", children: [
|
|
97
111
|
/* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
|
|
98
|
-
/* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-
|
|
112
|
+
/* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }),
|
|
99
113
|
/* @__PURE__ */ jsx(
|
|
100
114
|
"input",
|
|
101
115
|
{
|
|
102
|
-
className: "w-full rounded border pl-
|
|
116
|
+
className: "w-full h-10 rounded-lg border border-input bg-background pl-10 pr-3 text-sm shadow-xs transition-colors outline-none placeholder:text-muted-foreground hover:border-foreground/20 focus-visible:shadow-focus focus-visible:border-brand-violet disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-muted-foreground disabled:cursor-not-allowed",
|
|
103
117
|
value: query,
|
|
104
118
|
onChange: (event) => {
|
|
105
119
|
setQuery(event.target.value);
|
|
106
120
|
setHasTyped(true);
|
|
107
121
|
},
|
|
108
|
-
placeholder:
|
|
122
|
+
placeholder: resolvedSearchPlaceholder,
|
|
109
123
|
disabled
|
|
110
124
|
}
|
|
111
125
|
)
|
|
@@ -115,60 +129,50 @@ function LookupSelect({
|
|
|
115
129
|
shouldSearch ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
116
130
|
loading || loadingProp ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
|
|
117
131
|
/* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
|
|
118
|
-
|
|
132
|
+
resolvedLoadingLabel
|
|
119
133
|
] }) : null,
|
|
120
|
-
!loading && !loadingProp && !items.length ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children:
|
|
121
|
-
/* @__PURE__ */ jsx("div", { className: "
|
|
134
|
+
!loading && !loadingProp && !items.length ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: resolvedEmptyLabel }) : null,
|
|
135
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1.5 max-h-80 overflow-y-auto -mx-0.5 px-0.5 py-0.5", children: items.map((item) => {
|
|
122
136
|
const isSelected = value === item.id;
|
|
137
|
+
const isInteractive = !item.disabled || isSelected;
|
|
123
138
|
return /* @__PURE__ */ jsxs(
|
|
124
139
|
"div",
|
|
125
140
|
{
|
|
126
141
|
className: cn(
|
|
127
|
-
"flex gap-
|
|
128
|
-
|
|
142
|
+
"group flex items-center gap-4 rounded-xl border p-4 transition-all duration-150 focus-visible:outline-none focus-visible:shadow-focus",
|
|
143
|
+
isInteractive ? "cursor-pointer" : "cursor-not-allowed opacity-60",
|
|
144
|
+
isSelected ? "border-brand-violet bg-brand-violet/5 shadow-sm" : "border-input bg-card hover:border-foreground/20 hover:bg-muted/30 hover:shadow-sm"
|
|
129
145
|
),
|
|
130
146
|
role: "button",
|
|
131
147
|
tabIndex: item.disabled ? -1 : 0,
|
|
132
148
|
onClick: () => {
|
|
133
|
-
if (
|
|
149
|
+
if (!isInteractive) return;
|
|
134
150
|
onChange(item.id);
|
|
135
151
|
},
|
|
136
152
|
onKeyDown: (event) => {
|
|
137
153
|
if (event.key === "Enter" || event.key === " ") {
|
|
138
154
|
event.preventDefault();
|
|
139
|
-
if (
|
|
155
|
+
if (!isInteractive) return;
|
|
140
156
|
onChange(item.id);
|
|
141
157
|
}
|
|
142
158
|
},
|
|
143
159
|
"aria-pressed": isSelected,
|
|
160
|
+
"aria-disabled": item.disabled && !isSelected ? true : void 0,
|
|
161
|
+
title: isSelected ? resolvedSelectedLabel : resolvedSelectLabel,
|
|
144
162
|
children: [
|
|
145
|
-
/* @__PURE__ */ jsx("div", { className: "flex h-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
item.rightLabel ? /* @__PURE__ */ jsx("div", { className: "shrink-0 text-xs font-medium text-muted-foreground", children: item.rightLabel }) : null
|
|
163
|
+
item.icon ? /* @__PURE__ */ jsx("div", { className: "flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden [&>svg]:size-6 [&_svg]:text-muted-foreground", children: item.icon }) : /* @__PURE__ */ jsx("div", { className: cn(
|
|
164
|
+
"flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-lg border transition-colors",
|
|
165
|
+
isSelected ? "border-brand-violet/40 bg-brand-violet/10 text-brand-violet" : "border-input bg-muted text-muted-foreground group-hover:border-foreground/20"
|
|
166
|
+
), children: /* @__PURE__ */ jsx("span", { className: "text-base font-semibold uppercase", children: item.title.slice(0, 1) }) }),
|
|
167
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
|
|
168
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
|
|
169
|
+
/* @__PURE__ */ jsx("div", { className: "truncate text-sm font-semibold text-foreground", children: item.title }),
|
|
170
|
+
item.rightLabel ? /* @__PURE__ */ jsx("div", { className: "shrink-0 text-overline font-medium uppercase tracking-wider text-muted-foreground", children: item.rightLabel }) : null
|
|
154
171
|
] }),
|
|
155
|
-
/* @__PURE__ */ jsx("div", { className: "
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
variant: isSelected ? "secondary" : "outline",
|
|
160
|
-
size: "sm",
|
|
161
|
-
className: "shrink-0",
|
|
162
|
-
onClick: (event) => {
|
|
163
|
-
event.stopPropagation();
|
|
164
|
-
if (item.disabled && !isSelected) return;
|
|
165
|
-
onChange(item.id);
|
|
166
|
-
},
|
|
167
|
-
disabled: item.disabled && !isSelected,
|
|
168
|
-
children: isSelected ? selectedLabel : selectLabel
|
|
169
|
-
}
|
|
170
|
-
) })
|
|
171
|
-
] })
|
|
172
|
+
item.subtitle ? /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground truncate", children: item.subtitle }) : null,
|
|
173
|
+
item.description ? /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground/70 truncate", children: item.description }) : null
|
|
174
|
+
] }),
|
|
175
|
+
/* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center justify-center", children: isSelected ? /* @__PURE__ */ jsx(Check, { className: "size-5 text-brand-violet", "aria-hidden": "true" }) : /* @__PURE__ */ jsx("div", { className: "size-5", "aria-hidden": "true" }) })
|
|
172
176
|
]
|
|
173
177
|
},
|
|
174
178
|
item.id
|
|
@@ -184,12 +188,12 @@ function LookupSelect({
|
|
|
184
188
|
onClick: () => onChange(null),
|
|
185
189
|
children: [
|
|
186
190
|
/* @__PURE__ */ jsx(X, { className: "h-4 w-4" }),
|
|
187
|
-
|
|
191
|
+
resolvedClearLabel
|
|
188
192
|
]
|
|
189
193
|
}
|
|
190
194
|
) : null
|
|
191
|
-
] }) : hasTyped ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children:
|
|
192
|
-
error ? /* @__PURE__ */ jsx("p", { className: "text-xs text-
|
|
195
|
+
] }) : hasTyped ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: resolvedMinQueryHintLabel }) : /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: resolvedStartTypingLabel }),
|
|
196
|
+
error ? /* @__PURE__ */ jsx("p", { className: "text-xs text-status-error-text", role: "alert", children: resolvedEmptyLabel }) : null
|
|
193
197
|
] });
|
|
194
198
|
}
|
|
195
199
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/inputs/LookupSelect.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Loader2, Search, X } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport type LookupSelectItem = {\n id: string\n title: string\n subtitle?: string | null\n badge?: string | null\n icon?: React.ReactNode\n disabled?: boolean\n rightLabel?: string | null\n description?: string | null\n}\n\ntype LookupSelectProps = {\n value: string | null\n onChange: (next: string | null) => void\n fetchItems?: (query: string) => Promise<LookupSelectItem[]>\n fetchOptions?: (query?: string) => Promise<LookupSelectItem[]>\n options?: LookupSelectItem[]\n minQuery?: number\n actionSlot?: React.ReactNode\n onReady?: (controls: { setQuery: (value: string) => void }) => void\n searchPlaceholder?: string\n placeholder?: string\n clearLabel?: string\n emptyLabel?: string\n loadingLabel?: string\n selectLabel?: string\n selectedLabel?: string\n minQueryHintLabel?: string\n startTypingLabel?: string\n selectedHintLabel?: (id: string) => string\n disabled?: boolean\n loading?: boolean\n defaultOpen?: boolean\n}\n\nexport function LookupSelect({\n value,\n onChange,\n fetchItems,\n fetchOptions,\n options,\n minQuery = 2,\n actionSlot,\n onReady,\n placeholder,\n searchPlaceholder = placeholder ?? 'Search\u2026',\n clearLabel = 'Clear selection',\n emptyLabel = 'No results',\n loadingLabel = 'Searching\u2026',\n selectLabel = 'Select',\n selectedLabel = 'Selected',\n minQueryHintLabel,\n startTypingLabel = 'Start typing to search.',\n selectedHintLabel,\n disabled = false,\n loading: loadingProp = false,\n defaultOpen = false,\n}: LookupSelectProps) {\n const [query, setQuery] = React.useState('')\n const [items, setItems] = React.useState<LookupSelectItem[]>(options ?? [])\n const [loading, setLoading] = React.useState(false)\n const [hasTyped, setHasTyped] = React.useState(defaultOpen)\n const [error, setError] = React.useState<string | null>(null)\n const [fetchKey, setFetchKey] = React.useState(0)\n const fetchItemsRef = React.useRef(fetchItems ?? fetchOptions)\n const setQueryRef = React.useRef(setQuery)\n const optionsWasArrayRef = React.useRef(Array.isArray(options))\n\n React.useEffect(() => {\n fetchItemsRef.current = fetchItems ?? fetchOptions\n }, [fetchItems, fetchOptions])\n\n React.useEffect(() => {\n if (Array.isArray(options)) {\n optionsWasArrayRef.current = true\n setItems(options)\n } else if (optionsWasArrayRef.current) {\n optionsWasArrayRef.current = false\n setFetchKey((k) => k + 1)\n }\n }, [options])\n\n React.useEffect(() => {\n setQueryRef.current = setQuery\n if (onReady) onReady({ setQuery })\n }, [onReady, setQuery])\n\n const shouldSearch =\n defaultOpen || query.trim().length >= minQuery || Boolean(value && (options?.length ?? 0) > 0)\n React.useEffect(() => {\n if (disabled) {\n setItems(options ?? [])\n setLoading(false)\n return\n }\n let cancelled = false\n let timer: ReturnType<typeof setTimeout> | null = null\n if (!shouldSearch) {\n setItems(options ?? [])\n setLoading(false)\n setError(null)\n return () => { cancelled = true }\n }\n setLoading(true)\n setError(null)\n timer = setTimeout(() => {\n const requestId = Date.now()\n const fetcher = fetchItemsRef.current\n const loader = fetcher ?? (() => Promise.resolve(options ?? []))\n loader(query.trim())\n .then((result) => {\n if (cancelled) return\n setItems(result)\n })\n .catch((err) => {\n if (cancelled) return\n console.error('LookupSelect.fetchItems', err)\n setError('error')\n })\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n return requestId\n }, 220)\n return () => {\n cancelled = true\n if (timer) clearTimeout(timer)\n }\n }, [query, shouldSearch, fetchKey])\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3\">\n <div className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-2 top-2.5 h-4 w-4 text-muted-foreground\" />\n <input\n className=\"w-full rounded border pl-8 pr-2 py-2 text-sm\"\n value={query}\n onChange={(event) => {\n setQuery(event.target.value)\n setHasTyped(true)\n }}\n placeholder={searchPlaceholder}\n disabled={disabled}\n />\n </div>\n {actionSlot ? <div className=\"sm:self-start\">{actionSlot}</div> : null}\n </div>\n {shouldSearch ? (\n <div className=\"space-y-2\">\n {loading || loadingProp ? (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n {loadingLabel}\n </div>\n ) : null}\n {!loading && !loadingProp && !items.length ? (\n <p className=\"text-xs text-muted-foreground\">{emptyLabel}</p>\n ) : null}\n <div className=\"space-y-2 max-h-80 overflow-y-auto\">\n {items.map((item) => {\n const isSelected = value === item.id\n return (\n <div\n key={item.id}\n className={cn(\n 'flex gap-3 rounded border bg-card p-3 transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background',\n isSelected ? 'border-primary/70 bg-primary/5' : 'hover:border-primary/50'\n )}\n role=\"button\"\n tabIndex={item.disabled ? -1 : 0}\n onClick={() => {\n if (item.disabled && !isSelected) return\n onChange(item.id)\n }}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n if (item.disabled && !isSelected) return\n onChange(item.id)\n }\n }}\n aria-pressed={isSelected}\n >\n <div className=\"flex h-10 w-10 sm:h-12 sm:w-12 shrink-0 items-center justify-center overflow-hidden rounded border bg-muted\">\n {item.icon ?? <span className=\"text-muted-foreground\">\u2022</span>}\n </div>\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <div className=\"truncate text-sm font-medium\">{item.title}</div>\n {item.subtitle ? (\n <div className=\"text-xs text-muted-foreground truncate\">{item.subtitle}</div>\n ) : null}\n {item.description ? (\n <div className=\"text-xs text-muted-foreground truncate\">{item.description}</div>\n ) : null}\n </div>\n {item.rightLabel ? (\n <div className=\"shrink-0 text-xs font-medium text-muted-foreground\">{item.rightLabel}</div>\n ) : null}\n </div>\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n variant={isSelected ? 'secondary' : 'outline'}\n size=\"sm\"\n className=\"shrink-0\"\n onClick={(event) => {\n event.stopPropagation()\n if (item.disabled && !isSelected) return\n onChange(item.id)\n }}\n disabled={item.disabled && !isSelected}\n >\n {isSelected ? selectedLabel : selectLabel}\n </Button>\n </div>\n </div>\n </div>\n )\n })}\n </div>\n {value ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-fit gap-1 text-sm font-normal\"\n onClick={() => onChange(null)}\n >\n <X className=\"h-4 w-4\" />\n {clearLabel}\n </Button>\n ) : null}\n </div>\n ) : hasTyped ? (\n <p className=\"text-xs text-muted-foreground\">\n {minQueryHintLabel ?? `Type at least ${minQuery} characters or paste an id to search.`}\n </p>\n ) : (\n <p className=\"text-xs text-muted-foreground\">{startTypingLabel}</p>\n )}\n {error ? <p className=\"text-xs text-destructive\">{emptyLabel}</p> : null}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Check, Loader2, Search, X } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport type LookupSelectItem = {\n id: string\n title: string\n subtitle?: string | null\n badge?: string | null\n icon?: React.ReactNode\n disabled?: boolean\n rightLabel?: string | null\n description?: string | null\n}\n\ntype LookupSelectProps = {\n value: string | null\n onChange: (next: string | null) => void\n fetchItems?: (query: string) => Promise<LookupSelectItem[]>\n fetchOptions?: (query?: string) => Promise<LookupSelectItem[]>\n options?: LookupSelectItem[]\n minQuery?: number\n actionSlot?: React.ReactNode\n onReady?: (controls: { setQuery: (value: string) => void }) => void\n searchPlaceholder?: string\n placeholder?: string\n clearLabel?: string\n emptyLabel?: string\n loadingLabel?: string\n selectLabel?: string\n selectedLabel?: string\n minQueryHintLabel?: string\n startTypingLabel?: string\n selectedHintLabel?: (id: string) => string\n disabled?: boolean\n loading?: boolean\n defaultOpen?: boolean\n}\n\nexport function LookupSelect({\n value,\n onChange,\n fetchItems,\n fetchOptions,\n options,\n minQuery = 2,\n actionSlot,\n onReady,\n placeholder,\n searchPlaceholder,\n clearLabel,\n emptyLabel,\n loadingLabel,\n selectLabel,\n selectedLabel,\n minQueryHintLabel,\n startTypingLabel,\n selectedHintLabel,\n disabled = false,\n loading: loadingProp = false,\n defaultOpen = false,\n}: LookupSelectProps) {\n const t = useT()\n const resolvedSearchPlaceholder = searchPlaceholder ?? placeholder ?? t('ui.lookupSelect.searchPlaceholder', 'Search\u2026')\n const resolvedClearLabel = clearLabel ?? t('ui.lookupSelect.clearSelection', 'Clear selection')\n const resolvedEmptyLabel = emptyLabel ?? t('ui.lookupSelect.noResults', 'No results')\n const resolvedLoadingLabel = loadingLabel ?? t('ui.lookupSelect.searching', 'Searching\u2026')\n const resolvedSelectLabel = selectLabel ?? t('ui.lookupSelect.select', 'Select')\n const resolvedSelectedLabel = selectedLabel ?? t('ui.lookupSelect.selected', 'Selected')\n const resolvedStartTypingLabel = startTypingLabel ?? t('ui.lookupSelect.startTyping', 'Start typing to search.')\n const resolvedMinQueryHintLabel = minQueryHintLabel ?? t(\n 'ui.lookupSelect.minQueryHint',\n 'Type at least {minQuery} characters or paste an id to search.',\n { minQuery: String(minQuery) }\n )\n const [query, setQuery] = React.useState('')\n const [items, setItems] = React.useState<LookupSelectItem[]>(options ?? [])\n const [loading, setLoading] = React.useState(false)\n const [hasTyped, setHasTyped] = React.useState(defaultOpen)\n const [error, setError] = React.useState<string | null>(null)\n const [fetchKey, setFetchKey] = React.useState(0)\n const fetchItemsRef = React.useRef(fetchItems ?? fetchOptions)\n const setQueryRef = React.useRef(setQuery)\n const optionsWasArrayRef = React.useRef(Array.isArray(options))\n\n React.useEffect(() => {\n fetchItemsRef.current = fetchItems ?? fetchOptions\n }, [fetchItems, fetchOptions])\n\n React.useEffect(() => {\n if (Array.isArray(options)) {\n optionsWasArrayRef.current = true\n setItems(options)\n } else if (optionsWasArrayRef.current) {\n optionsWasArrayRef.current = false\n setFetchKey((k) => k + 1)\n }\n }, [options])\n\n React.useEffect(() => {\n setQueryRef.current = setQuery\n if (onReady) onReady({ setQuery })\n }, [onReady, setQuery])\n\n const shouldSearch =\n defaultOpen || query.trim().length >= minQuery || Boolean(value && (options?.length ?? 0) > 0)\n React.useEffect(() => {\n if (disabled) {\n setItems(options ?? [])\n setLoading(false)\n return\n }\n let cancelled = false\n let timer: ReturnType<typeof setTimeout> | null = null\n if (!shouldSearch) {\n setItems(options ?? [])\n setLoading(false)\n setError(null)\n return () => { cancelled = true }\n }\n setLoading(true)\n setError(null)\n timer = setTimeout(() => {\n const requestId = Date.now()\n const fetcher = fetchItemsRef.current\n const loader = fetcher ?? (() => Promise.resolve(options ?? []))\n loader(query.trim())\n .then((result) => {\n if (cancelled) return\n setItems(result)\n })\n .catch((err) => {\n if (cancelled) return\n console.error('LookupSelect.fetchItems', err)\n setError('error')\n })\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n return requestId\n }, 220)\n return () => {\n cancelled = true\n if (timer) clearTimeout(timer)\n }\n }, [query, shouldSearch, fetchKey])\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3\">\n <div className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground\" />\n <input\n className=\"w-full h-10 rounded-lg border border-input bg-background pl-10 pr-3 text-sm shadow-xs transition-colors outline-none placeholder:text-muted-foreground hover:border-foreground/20 focus-visible:shadow-focus focus-visible:border-brand-violet disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-muted-foreground disabled:cursor-not-allowed\"\n value={query}\n onChange={(event) => {\n setQuery(event.target.value)\n setHasTyped(true)\n }}\n placeholder={resolvedSearchPlaceholder}\n disabled={disabled}\n />\n </div>\n {actionSlot ? <div className=\"sm:self-start\">{actionSlot}</div> : null}\n </div>\n {shouldSearch ? (\n <div className=\"space-y-2\">\n {loading || loadingProp ? (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n {resolvedLoadingLabel}\n </div>\n ) : null}\n {!loading && !loadingProp && !items.length ? (\n <p className=\"text-xs text-muted-foreground\">{resolvedEmptyLabel}</p>\n ) : null}\n <div className=\"flex flex-col gap-1.5 max-h-80 overflow-y-auto -mx-0.5 px-0.5 py-0.5\">\n {items.map((item) => {\n const isSelected = value === item.id\n const isInteractive = !item.disabled || isSelected\n return (\n <div\n key={item.id}\n className={cn(\n 'group flex items-center gap-4 rounded-xl border p-4 transition-all duration-150 focus-visible:outline-none focus-visible:shadow-focus',\n isInteractive ? 'cursor-pointer' : 'cursor-not-allowed opacity-60',\n isSelected\n ? 'border-brand-violet bg-brand-violet/5 shadow-sm'\n : 'border-input bg-card hover:border-foreground/20 hover:bg-muted/30 hover:shadow-sm'\n )}\n role=\"button\"\n tabIndex={item.disabled ? -1 : 0}\n onClick={() => {\n if (!isInteractive) return\n onChange(item.id)\n }}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n if (!isInteractive) return\n onChange(item.id)\n }\n }}\n aria-pressed={isSelected}\n aria-disabled={item.disabled && !isSelected ? true : undefined}\n title={isSelected ? resolvedSelectedLabel : resolvedSelectLabel}\n >\n {item.icon ? (\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden [&>svg]:size-6 [&_svg]:text-muted-foreground\">\n {item.icon}\n </div>\n ) : (\n <div className={cn(\n 'flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-lg border transition-colors',\n isSelected\n ? 'border-brand-violet/40 bg-brand-violet/10 text-brand-violet'\n : 'border-input bg-muted text-muted-foreground group-hover:border-foreground/20'\n )}>\n <span className=\"text-base font-semibold uppercase\">{item.title.slice(0, 1)}</span>\n </div>\n )}\n <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <div className=\"flex items-center justify-between gap-3\">\n <div className=\"truncate text-sm font-semibold text-foreground\">{item.title}</div>\n {item.rightLabel ? (\n <div className=\"shrink-0 text-overline font-medium uppercase tracking-wider text-muted-foreground\">\n {item.rightLabel}\n </div>\n ) : null}\n </div>\n {item.subtitle ? (\n <div className=\"text-xs text-muted-foreground truncate\">{item.subtitle}</div>\n ) : null}\n {item.description ? (\n <div className=\"text-xs text-muted-foreground/70 truncate\">{item.description}</div>\n ) : null}\n </div>\n <div className=\"flex shrink-0 items-center justify-center\">\n {isSelected ? (\n <Check className=\"size-5 text-brand-violet\" aria-hidden=\"true\" />\n ) : (\n <div className=\"size-5\" aria-hidden=\"true\" />\n )}\n </div>\n </div>\n )\n })}\n </div>\n {value ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-fit gap-1 text-sm font-normal\"\n onClick={() => onChange(null)}\n >\n <X className=\"h-4 w-4\" />\n {resolvedClearLabel}\n </Button>\n ) : null}\n </div>\n ) : hasTyped ? (\n <p className=\"text-xs text-muted-foreground\">\n {resolvedMinQueryHintLabel}\n </p>\n ) : (\n <p className=\"text-xs text-muted-foreground\">{resolvedStartTypingLabel}</p>\n )}\n {error ? <p className=\"text-xs text-status-error-text\" role=\"alert\">{resolvedEmptyLabel}</p> : null}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,10 +1,64 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
4
5
|
import { extractPhoneDigits, validatePhoneNumber } from "@open-mercato/shared/lib/phone";
|
|
5
|
-
import {
|
|
6
|
+
import { cn } from "@open-mercato/shared/lib/utils";
|
|
7
|
+
import {
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectItemLeading,
|
|
12
|
+
SelectTrigger
|
|
13
|
+
} from "../../primitives/select.js";
|
|
14
|
+
const PHONE_COUNTRIES = [
|
|
15
|
+
{ iso2: "US", dialCode: "+1", label: "United States", flag: "\u{1F1FA}\u{1F1F8}" },
|
|
16
|
+
{ iso2: "CA", dialCode: "+1", label: "Canada", flag: "\u{1F1E8}\u{1F1E6}" },
|
|
17
|
+
{ iso2: "GB", dialCode: "+44", label: "United Kingdom", flag: "\u{1F1EC}\u{1F1E7}" },
|
|
18
|
+
{ iso2: "PL", dialCode: "+48", label: "Poland", flag: "\u{1F1F5}\u{1F1F1}" },
|
|
19
|
+
{ iso2: "DE", dialCode: "+49", label: "Germany", flag: "\u{1F1E9}\u{1F1EA}" },
|
|
20
|
+
{ iso2: "FR", dialCode: "+33", label: "France", flag: "\u{1F1EB}\u{1F1F7}" },
|
|
21
|
+
{ iso2: "ES", dialCode: "+34", label: "Spain", flag: "\u{1F1EA}\u{1F1F8}" },
|
|
22
|
+
{ iso2: "IT", dialCode: "+39", label: "Italy", flag: "\u{1F1EE}\u{1F1F9}" },
|
|
23
|
+
{ iso2: "NL", dialCode: "+31", label: "Netherlands", flag: "\u{1F1F3}\u{1F1F1}" },
|
|
24
|
+
{ iso2: "SE", dialCode: "+46", label: "Sweden", flag: "\u{1F1F8}\u{1F1EA}" },
|
|
25
|
+
{ iso2: "AT", dialCode: "+43", label: "Austria", flag: "\u{1F1E6}\u{1F1F9}" },
|
|
26
|
+
{ iso2: "CH", dialCode: "+41", label: "Switzerland", flag: "\u{1F1E8}\u{1F1ED}" },
|
|
27
|
+
{ iso2: "PT", dialCode: "+351", label: "Portugal", flag: "\u{1F1F5}\u{1F1F9}" },
|
|
28
|
+
{ iso2: "CZ", dialCode: "+420", label: "Czechia", flag: "\u{1F1E8}\u{1F1FF}" },
|
|
29
|
+
{ iso2: "RO", dialCode: "+40", label: "Romania", flag: "\u{1F1F7}\u{1F1F4}" },
|
|
30
|
+
{ iso2: "UA", dialCode: "+380", label: "Ukraine", flag: "\u{1F1FA}\u{1F1E6}" }
|
|
31
|
+
];
|
|
32
|
+
const DEFAULT_COUNTRY = PHONE_COUNTRIES[0];
|
|
33
|
+
const COUNTRIES_BY_DIAL_LENGTH = [...PHONE_COUNTRIES].sort(
|
|
34
|
+
(a, b) => b.dialCode.length - a.dialCode.length
|
|
35
|
+
);
|
|
36
|
+
function findCountryByIso(iso2) {
|
|
37
|
+
return PHONE_COUNTRIES.find((c) => c.iso2 === iso2);
|
|
38
|
+
}
|
|
39
|
+
function parseCountryFromValue(value) {
|
|
40
|
+
if (!value) return null;
|
|
41
|
+
const trimmed = value.trim();
|
|
42
|
+
if (!trimmed.startsWith("+")) return null;
|
|
43
|
+
for (const country of COUNTRIES_BY_DIAL_LENGTH) {
|
|
44
|
+
if (trimmed.startsWith(country.dialCode)) return country;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function extractLocalNumber(value, dialCode) {
|
|
49
|
+
if (!value) return "";
|
|
50
|
+
const trimmed = value.trim();
|
|
51
|
+
if (!trimmed.startsWith(dialCode)) return trimmed;
|
|
52
|
+
return trimmed.slice(dialCode.length).trim();
|
|
53
|
+
}
|
|
54
|
+
function composeValue(country, localNumber) {
|
|
55
|
+
const trimmed = localNumber.trim();
|
|
56
|
+
if (!trimmed) return "";
|
|
57
|
+
if (trimmed.startsWith("+")) return trimmed;
|
|
58
|
+
return `${country.dialCode} ${trimmed}`;
|
|
59
|
+
}
|
|
6
60
|
const DEFAULT_MIN_DIGITS = 6;
|
|
7
|
-
const
|
|
61
|
+
const DEFAULT_PLACEHOLDER = "(555) 000-0000";
|
|
8
62
|
function PhoneNumberField({
|
|
9
63
|
id,
|
|
10
64
|
value,
|
|
@@ -21,38 +75,68 @@ function PhoneNumberField({
|
|
|
21
75
|
duplicateLabel,
|
|
22
76
|
duplicateLinkLabel,
|
|
23
77
|
invalidLabel,
|
|
24
|
-
onDuplicateLookup
|
|
78
|
+
onDuplicateLookup,
|
|
79
|
+
countries: countriesProp,
|
|
80
|
+
defaultCountryIso2
|
|
25
81
|
}) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
82
|
+
const t = useT();
|
|
83
|
+
const resolvedInvalidLabel = invalidLabel ?? t(
|
|
84
|
+
"ui.inputs.phoneNumberField.invalid",
|
|
85
|
+
"Enter a valid phone number with country code (e.g. +1 212 555 1234)"
|
|
86
|
+
);
|
|
87
|
+
const resolvedCheckingLabel = checkingLabel ?? t(
|
|
88
|
+
"ui.inputs.phoneNumberField.checking",
|
|
89
|
+
"Checking for duplicates\u2026"
|
|
90
|
+
);
|
|
91
|
+
const resolvedDuplicateLinkLabel = duplicateLinkLabel ?? t(
|
|
92
|
+
"ui.inputs.phoneNumberField.duplicateLink",
|
|
93
|
+
"Open record"
|
|
94
|
+
);
|
|
95
|
+
const resolvedPlaceholder = placeholder ?? DEFAULT_PLACEHOLDER;
|
|
96
|
+
const countries = countriesProp ?? PHONE_COUNTRIES;
|
|
97
|
+
const fallbackCountry = React.useMemo(
|
|
98
|
+
() => defaultCountryIso2 && findCountryByIso(defaultCountryIso2) || DEFAULT_COUNTRY,
|
|
99
|
+
[defaultCountryIso2]
|
|
100
|
+
);
|
|
101
|
+
const initialCountry = React.useMemo(
|
|
102
|
+
() => parseCountryFromValue(value) ?? fallbackCountry,
|
|
103
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
104
|
+
[]
|
|
105
|
+
);
|
|
106
|
+
const [country, setCountry] = React.useState(initialCountry);
|
|
107
|
+
const [localNumber, setLocalNumber] = React.useState(
|
|
108
|
+
() => extractLocalNumber(value, initialCountry.dialCode)
|
|
109
|
+
);
|
|
30
110
|
const [duplicate, setDuplicate] = React.useState(null);
|
|
31
111
|
const [checking, setChecking] = React.useState(false);
|
|
32
112
|
const [validationHint, setValidationHint] = React.useState(null);
|
|
113
|
+
const [focused, setFocused] = React.useState(false);
|
|
33
114
|
const userEditingRef = React.useRef(false);
|
|
34
115
|
const externalFieldError = externalError && externalError.trim().length > 0 ? externalError : null;
|
|
35
116
|
const errorMessage = externalFieldError ?? validationHint;
|
|
36
117
|
const errorId = errorMessage && id ? `${id}-error` : void 0;
|
|
37
118
|
const describedBy = [ariaDescribedBy, errorId].filter((part) => Boolean(part)).join(" ") || void 0;
|
|
119
|
+
const composedValue = composeValue(country, localNumber);
|
|
38
120
|
React.useEffect(() => {
|
|
39
121
|
if (userEditingRef.current) return;
|
|
40
122
|
if (value == null || value === "") {
|
|
41
|
-
|
|
123
|
+
setLocalNumber("");
|
|
42
124
|
onDigitsChange?.(null);
|
|
43
125
|
return;
|
|
44
126
|
}
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
127
|
+
const parsed = parseCountryFromValue(value);
|
|
128
|
+
if (parsed) setCountry(parsed);
|
|
129
|
+
const next = extractLocalNumber(value, parsed?.dialCode ?? country.dialCode);
|
|
130
|
+
setLocalNumber(next);
|
|
131
|
+
onDigitsChange?.(extractPhoneDigits(String(value)) || null);
|
|
132
|
+
}, [value]);
|
|
49
133
|
React.useEffect(() => {
|
|
50
134
|
if (!onDuplicateLookup || disabled) {
|
|
51
135
|
setDuplicate(null);
|
|
52
136
|
setChecking(false);
|
|
53
137
|
return;
|
|
54
138
|
}
|
|
55
|
-
const digits = extractPhoneDigits(
|
|
139
|
+
const digits = extractPhoneDigits(composedValue);
|
|
56
140
|
if (!digits || digits.length < minDigits) {
|
|
57
141
|
setDuplicate(null);
|
|
58
142
|
setChecking(false);
|
|
@@ -74,70 +158,137 @@ function PhoneNumberField({
|
|
|
74
158
|
cancelled = true;
|
|
75
159
|
window.clearTimeout(handle);
|
|
76
160
|
};
|
|
77
|
-
}, [
|
|
78
|
-
const
|
|
161
|
+
}, [composedValue, disabled, minDigits, onDuplicateLookup]);
|
|
162
|
+
const emit = React.useCallback(
|
|
163
|
+
(nextCountry, nextLocal) => {
|
|
164
|
+
const composed = composeValue(nextCountry, nextLocal);
|
|
165
|
+
onValueChange(composed.length ? composed : void 0);
|
|
166
|
+
onDigitsChange?.(extractPhoneDigits(composed) || null);
|
|
167
|
+
},
|
|
168
|
+
[onDigitsChange, onValueChange]
|
|
169
|
+
);
|
|
170
|
+
const handleCountryChange = React.useCallback(
|
|
171
|
+
(iso2) => {
|
|
172
|
+
const next = findCountryByIso(iso2);
|
|
173
|
+
if (!next) return;
|
|
174
|
+
userEditingRef.current = true;
|
|
175
|
+
setCountry(next);
|
|
176
|
+
setValidationHint(null);
|
|
177
|
+
emit(next, localNumber);
|
|
178
|
+
},
|
|
179
|
+
[emit, localNumber]
|
|
180
|
+
);
|
|
181
|
+
const handleLocalChange = React.useCallback(
|
|
79
182
|
(event) => {
|
|
80
183
|
const next = event.target.value;
|
|
81
|
-
const cleanDigits = extractPhoneDigits(next);
|
|
82
184
|
userEditingRef.current = true;
|
|
83
|
-
|
|
185
|
+
setLocalNumber(next);
|
|
84
186
|
setValidationHint(null);
|
|
85
|
-
|
|
86
|
-
onDigitsChange?.(cleanDigits.length ? cleanDigits : null);
|
|
187
|
+
emit(country, next);
|
|
87
188
|
},
|
|
88
|
-
[
|
|
189
|
+
[country, emit]
|
|
89
190
|
);
|
|
90
191
|
const handleBlur = React.useCallback(() => {
|
|
91
192
|
userEditingRef.current = false;
|
|
92
|
-
|
|
193
|
+
setFocused(false);
|
|
194
|
+
const trimmed = localNumber.trim();
|
|
93
195
|
if (!trimmed) {
|
|
94
|
-
|
|
196
|
+
setLocalNumber("");
|
|
95
197
|
setValidationHint(null);
|
|
96
198
|
onValueChange(void 0);
|
|
97
199
|
onDigitsChange?.(null);
|
|
98
200
|
return;
|
|
99
201
|
}
|
|
100
|
-
const
|
|
202
|
+
const composed = composeValue(country, trimmed);
|
|
203
|
+
const result = validatePhoneNumber(composed);
|
|
101
204
|
if (result.valid) {
|
|
102
|
-
|
|
205
|
+
const normalizedLocal = extractLocalNumber(result.normalized ?? composed, country.dialCode);
|
|
206
|
+
setLocalNumber(normalizedLocal);
|
|
103
207
|
setValidationHint(null);
|
|
104
208
|
onValueChange(result.normalized || void 0);
|
|
105
209
|
onDigitsChange?.(result.digits || null);
|
|
106
210
|
} else {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
onValueChange(trimmed);
|
|
211
|
+
setValidationHint(resolvedInvalidLabel);
|
|
212
|
+
onValueChange(composed);
|
|
110
213
|
onDigitsChange?.(result.digits || null);
|
|
111
214
|
}
|
|
112
|
-
}, [
|
|
215
|
+
}, [country, localNumber, onDigitsChange, onValueChange, resolvedInvalidLabel]);
|
|
216
|
+
const containerErrorBorder = errorMessage ? "border-status-error-icon" : "border-input";
|
|
217
|
+
const containerFocusBorder = errorMessage ? "border-status-error-icon shadow-focus" : "border-brand-violet shadow-focus";
|
|
113
218
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
114
|
-
/* @__PURE__ */
|
|
115
|
-
|
|
219
|
+
/* @__PURE__ */ jsxs(
|
|
220
|
+
"div",
|
|
116
221
|
{
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
placeholder,
|
|
122
|
-
autoFocus,
|
|
123
|
-
disabled,
|
|
124
|
-
id,
|
|
125
|
-
"aria-label": ariaLabel,
|
|
126
|
-
"aria-describedby": describedBy,
|
|
222
|
+
className: cn(
|
|
223
|
+
"flex items-stretch w-full rounded-md border bg-background shadow-xs transition-colors",
|
|
224
|
+
disabled ? "bg-bg-disabled border-border-disabled cursor-not-allowed" : focused ? containerFocusBorder : `${containerErrorBorder} hover:border-foreground/30`
|
|
225
|
+
),
|
|
127
226
|
"aria-invalid": errorMessage ? "true" : void 0,
|
|
128
|
-
|
|
227
|
+
children: [
|
|
228
|
+
/* @__PURE__ */ jsxs(Select, { value: country.iso2, onValueChange: handleCountryChange, disabled, children: [
|
|
229
|
+
/* @__PURE__ */ jsxs(
|
|
230
|
+
SelectTrigger,
|
|
231
|
+
{
|
|
232
|
+
"aria-label": ariaLabel ? `${ariaLabel} country` : "Country code",
|
|
233
|
+
className: cn(
|
|
234
|
+
"h-auto w-auto shrink-0 gap-1.5 rounded-none rounded-l-md border-0 bg-transparent px-2.5 py-2 shadow-none",
|
|
235
|
+
"hover:bg-muted/40 focus:bg-muted/40 focus-visible:shadow-none focus-visible:border-0",
|
|
236
|
+
"disabled:bg-transparent disabled:hover:bg-transparent"
|
|
237
|
+
),
|
|
238
|
+
onFocus: () => setFocused(true),
|
|
239
|
+
onBlur: () => setFocused(false),
|
|
240
|
+
children: [
|
|
241
|
+
/* @__PURE__ */ jsx("span", { className: "text-base leading-none", "aria-hidden": "true", children: country.flag }),
|
|
242
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm text-foreground tabular-nums", children: country.dialCode })
|
|
243
|
+
]
|
|
244
|
+
}
|
|
245
|
+
),
|
|
246
|
+
/* @__PURE__ */ jsx(SelectContent, { align: "start", children: countries.map((c) => /* @__PURE__ */ jsxs(SelectItem, { value: c.iso2, children: [
|
|
247
|
+
/* @__PURE__ */ jsx(SelectItemLeading, { children: /* @__PURE__ */ jsx("span", { className: "text-base leading-none", children: c.flag }) }),
|
|
248
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: c.label }),
|
|
249
|
+
/* @__PURE__ */ jsx("span", { className: "ml-2 text-xs text-muted-foreground tabular-nums", children: c.dialCode })
|
|
250
|
+
] }, `${c.iso2}-${c.dialCode}`)) })
|
|
251
|
+
] }),
|
|
252
|
+
/* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "w-px self-stretch bg-input" }),
|
|
253
|
+
/* @__PURE__ */ jsx(
|
|
254
|
+
"input",
|
|
255
|
+
{
|
|
256
|
+
type: "tel",
|
|
257
|
+
inputMode: "tel",
|
|
258
|
+
autoComplete: "tel-national",
|
|
259
|
+
value: localNumber,
|
|
260
|
+
onChange: handleLocalChange,
|
|
261
|
+
onBlur: handleBlur,
|
|
262
|
+
onFocus: () => setFocused(true),
|
|
263
|
+
placeholder: resolvedPlaceholder,
|
|
264
|
+
autoFocus,
|
|
265
|
+
disabled,
|
|
266
|
+
id,
|
|
267
|
+
"aria-label": ariaLabel,
|
|
268
|
+
"aria-describedby": describedBy,
|
|
269
|
+
"aria-invalid": errorMessage ? "true" : void 0,
|
|
270
|
+
"data-crud-focus-target": "",
|
|
271
|
+
className: cn(
|
|
272
|
+
"flex-1 min-w-0 bg-transparent px-3 py-2 text-sm leading-5 outline-none",
|
|
273
|
+
"placeholder:text-muted-foreground",
|
|
274
|
+
"disabled:cursor-not-allowed disabled:text-muted-foreground"
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
]
|
|
129
279
|
}
|
|
130
280
|
),
|
|
131
|
-
errorMessage ? /* @__PURE__ */ jsx("p", { id: errorId, className: "text-xs text-
|
|
132
|
-
!disabled && duplicate && duplicateLabel
|
|
281
|
+
errorMessage ? /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "text-xs text-status-error-text", children: errorMessage }) : null,
|
|
282
|
+
!disabled && duplicate && duplicateLabel ? /* @__PURE__ */ jsxs("p", { className: "text-xs text-status-warning-text", children: [
|
|
133
283
|
duplicateLabel(duplicate),
|
|
134
284
|
" ",
|
|
135
|
-
/* @__PURE__ */ jsx("a", { className: "font-medium text-
|
|
285
|
+
/* @__PURE__ */ jsx("a", { className: "font-medium text-brand-violet underline underline-offset-2", href: duplicate.href, children: resolvedDuplicateLinkLabel })
|
|
136
286
|
] }) : null,
|
|
137
|
-
!disabled && !duplicate && checking
|
|
287
|
+
!disabled && !duplicate && checking ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: resolvedCheckingLabel }) : null
|
|
138
288
|
] });
|
|
139
289
|
}
|
|
140
290
|
export {
|
|
291
|
+
PHONE_COUNTRIES,
|
|
141
292
|
PhoneNumberField
|
|
142
293
|
};
|
|
143
294
|
//# sourceMappingURL=PhoneNumberField.js.map
|