@mohasinac/appkit 3.5.2 → 3.5.4
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/features/homepage/components/CustomCardsSection.js +1 -1
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/seller/components/CategoryInlineSelect.d.ts +1 -1
- package/dist/features/seller/components/CategoryInlineSelect.js +1 -1
- package/dist/react/hooks/useBulkSelection.js +12 -3
- package/dist/react/hooks/useGesture.js +48 -4
- package/dist/react/hooks/useLongPress.d.ts +1 -0
- package/dist/react/hooks/useLongPress.js +5 -0
- package/dist/react/hooks/usePullToRefresh.js +12 -1
- package/dist/react/hooks/useRealtimeEvent.js +18 -1
- package/dist/react/hooks/useSwipe.js +14 -0
- package/dist/seed/addresses-seed-data.js +8 -99
- package/dist/seed/categories-seed-data.js +103 -2082
- package/dist/seed/homepage-sections-seed-data.js +9 -9
- package/dist/seed/products-art-seed-data.d.ts +1 -10
- package/dist/seed/products-art-seed-data.js +13 -122
- package/dist/seed/products-auctions-seed-data.js +39 -747
- package/dist/seed/products-classifieds-seed-data.d.ts +1 -9
- package/dist/seed/products-classifieds-seed-data.js +13 -195
- package/dist/seed/products-digital-codes-seed-data.d.ts +1 -9
- package/dist/seed/products-digital-codes-seed-data.js +13 -202
- package/dist/seed/products-live-items-seed-data.d.ts +1 -12
- package/dist/seed/products-live-items-seed-data.js +13 -188
- package/dist/seed/products-preorders-seed-data.js +18 -149
- package/dist/seed/products-prize-draws-seed-data.d.ts +1 -16
- package/dist/seed/products-prize-draws-seed-data.js +13 -385
- package/dist/seed/products-standard-seed-data.js +152 -1331
- package/dist/seed/products-stickers-seed-data.d.ts +1 -10
- package/dist/seed/products-stickers-seed-data.js +13 -122
- package/dist/seed/site-settings-seed-data.js +106 -108
- package/dist/seed/store-addresses-seed-data.js +14 -46
- package/dist/seed/stores-seed-data.js +13 -205
- package/dist/styles.css +19 -18
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/BaseListingCard.d.ts +2 -1
- package/dist/ui/components/BaseListingCard.js +4 -5
- package/dist/ui/components/BulkActionBar.style.css +4 -4
- package/dist/ui/components/Button.js +52 -22
- package/dist/ui/components/Button.style.css +14 -13
- package/dist/ui/components/DateInput.js +1 -0
- package/dist/ui/components/DynamicBgDiv.js +8 -6
- package/dist/ui/components/FormField.js +1 -1
- package/dist/ui/components/HorizontalScroller.js +35 -16
- package/dist/ui/components/Iframe.d.ts +8 -1
- package/dist/ui/components/Layout.d.ts +4 -4
- package/dist/ui/components/Layout.js +8 -8
- package/dist/ui/components/ListingToolbar.js +4 -4
- package/dist/ui/components/Motion.js +4 -1
- package/dist/ui/components/OtpInput.js +1 -1
- package/dist/ui/components/PaginatedSelect.js +29 -3
- package/dist/ui/components/Pagination.js +10 -5
- package/dist/ui/components/RichTextEditor.js +64 -3
- package/dist/ui/components/Select.js +1 -0
- package/dist/ui/components/Semantic.d.ts +10 -10
- package/dist/ui/components/Semantic.js +25 -25
- package/dist/ui/components/SideDrawer.js +13 -11
- package/dist/ui/components/SideModal.js +9 -7
- package/dist/ui/components/SlottedListingView.d.ts +13 -2
- package/dist/ui/components/StickyToolbar.js +9 -5
- package/dist/ui/components/TagInput.js +15 -2
- package/dist/ui/components/Textarea.js +1 -0
- package/dist/ui/components/Toggle.js +8 -1
- package/dist/ui/components/UnsavedChangesModal.js +12 -1
- package/dist/ui/forms/ColorPickerField.js +7 -0
- package/dist/ui/forms/FieldCheckbox.js +1 -1
- package/dist/utils/id-generators.d.ts +7 -4
- package/dist/utils/id-generators.js +53 -31
- package/dist/utils/number.formatter.js +19 -6
- package/dist/utils/string.formatter.js +4 -1
- package/package.json +1 -1
|
@@ -21,7 +21,8 @@ export interface BaseListingCardRootProps {
|
|
|
21
21
|
onTouchEnd?: () => void;
|
|
22
22
|
}
|
|
23
23
|
export interface BaseListingCardHeroProps {
|
|
24
|
-
aspect
|
|
24
|
+
/** Aspect ratio for the hero image — only these two produce a real CSS rule (Tailwind's static scanner can't see a dynamically-interpolated `aspect-[${var}]`). Defaults to `"4/3"`. */
|
|
25
|
+
aspect?: "square" | "4/3";
|
|
25
26
|
variant?: "grid" | "list";
|
|
26
27
|
className?: string;
|
|
27
28
|
children?: ReactNode;
|
|
@@ -25,11 +25,10 @@ function BaseListingCardHero({ aspect, variant = "grid", className = "", childre
|
|
|
25
25
|
.filter(Boolean)
|
|
26
26
|
.join(" "), onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, children: children }));
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
: `aspect-[${aspect}]`;
|
|
28
|
+
// Literal, statically-scannable class names only — never interpolate an
|
|
29
|
+
// arbitrary value into `aspect-[...]`, Tailwind's JIT scanner can't see it
|
|
30
|
+
// and the rule silently never makes it into the compiled CSS.
|
|
31
|
+
const aspectClass = aspect === "square" ? "aspect-square" : "aspect-[4/3]";
|
|
33
32
|
return (_jsx("div", { className: [
|
|
34
33
|
"relative overflow-hidden bg-[var(--appkit-color-surface)] flex-shrink-0",
|
|
35
34
|
aspectClass,
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
|
|
161
161
|
/* dark */
|
|
162
162
|
.dark .appkit-bulk-bar {
|
|
163
|
-
border-color:
|
|
163
|
+
border-color: color-mix(in srgb, var(--appkit-color-primary) 30%, transparent);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
.dark .appkit-bulk-bar__stripe {
|
|
@@ -168,13 +168,13 @@
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
.dark .appkit-bulk-bar__count-pill {
|
|
171
|
-
border-color:
|
|
172
|
-
background:
|
|
171
|
+
border-color: color-mix(in srgb, var(--appkit-color-primary) 35%, transparent);
|
|
172
|
+
background: color-mix(in srgb, var(--appkit-color-primary) 10%, transparent);
|
|
173
173
|
color: var(--appkit-color-primary-300);
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
.dark .appkit-bulk-bar__count-pill:hover {
|
|
177
|
-
background:
|
|
177
|
+
background: color-mix(in srgb, var(--appkit-color-primary) 18%, transparent);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
.dark .appkit-bulk-bar__picker-trigger {
|
|
@@ -116,6 +116,16 @@ const ACTION_KIND_VARIANT = {
|
|
|
116
116
|
};
|
|
117
117
|
export function Button({ variant, size = "md", className = "", isLoading = false, disabled, children, gap, textSize, paddingX, paddingY, weight, textColor, border, rounded, shadow, justify, asChild = false, action, ...props }) {
|
|
118
118
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
119
|
+
// React 17+ no longer pools SyntheticEvents, so it's safe to hold onto the
|
|
120
|
+
// original triggering event across the confirmation dialog's async gap and
|
|
121
|
+
// forward it to the consumer's onClick once confirmed — not the confirm
|
|
122
|
+
// button's own click event (a different DOM element, with none of the
|
|
123
|
+
// data-* attributes the caller may have put on the real trigger). Per the
|
|
124
|
+
// DOM/React event spec, `currentTarget` itself is reset to null once the
|
|
125
|
+
// original event's own synchronous dispatch finishes, so it's captured
|
|
126
|
+
// separately here and restored onto the stored event before forwarding.
|
|
127
|
+
const pendingEventRef = React.useRef(null);
|
|
128
|
+
const pendingTargetRef = React.useRef(null);
|
|
119
129
|
// Resolve defaults from action registry
|
|
120
130
|
const resolvedVariant = variant ?? (action ? (ACTION_KIND_VARIANT[action.kind] ?? "primary") : "primary");
|
|
121
131
|
const resolvedChildren = children ?? (action ? action.label : undefined);
|
|
@@ -159,37 +169,57 @@ export function Button({ variant, size = "md", className = "", isLoading = false
|
|
|
159
169
|
}
|
|
160
170
|
if (action?.confirmation) {
|
|
161
171
|
event.preventDefault();
|
|
172
|
+
pendingEventRef.current = event;
|
|
173
|
+
pendingTargetRef.current = event.currentTarget;
|
|
162
174
|
setConfirmOpen(true);
|
|
163
175
|
return;
|
|
164
176
|
}
|
|
165
177
|
wrapAsync(userOnClick)(event);
|
|
166
178
|
}, [disabled, isLoading, action, userOnClick, wrapAsync]);
|
|
167
|
-
const handleConfirm = useCallback((
|
|
179
|
+
const handleConfirm = useCallback(() => {
|
|
168
180
|
setConfirmOpen(false);
|
|
169
|
-
|
|
181
|
+
const originalEvent = pendingEventRef.current;
|
|
182
|
+
const originalTarget = pendingTargetRef.current;
|
|
183
|
+
pendingEventRef.current = null;
|
|
184
|
+
pendingTargetRef.current = null;
|
|
185
|
+
if (originalEvent) {
|
|
186
|
+
originalEvent.currentTarget = originalTarget;
|
|
187
|
+
wrapAsync(userOnClick)(originalEvent);
|
|
188
|
+
}
|
|
170
189
|
}, [userOnClick, wrapAsync]);
|
|
171
|
-
if (asChild && React.isValidElement(resolvedChildren)) {
|
|
172
|
-
const child = resolvedChildren;
|
|
173
|
-
return React.cloneElement(child, {
|
|
174
|
-
...props,
|
|
175
|
-
className: twMerge(String(child.props.className ?? ""), classes),
|
|
176
|
-
...(disabled ? { "aria-disabled": true } : {}),
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
190
|
const confirmDef = action?.confirmation;
|
|
180
191
|
const confirmVariant = confirmDef?.confirmKind
|
|
181
192
|
? (ACTION_KIND_VARIANT[confirmDef.confirmKind] ?? "primary")
|
|
182
193
|
: "primary";
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
194
|
+
// Built once and rendered alongside EITHER the asChild clone or the plain
|
|
195
|
+
// <button> below — this must never live inside an early return for only
|
|
196
|
+
// one of those two branches, or the other branch can never open a
|
|
197
|
+
// confirmation dialog at all (this was the actual asChild bug: the early
|
|
198
|
+
// return happened before this JSX was ever reached).
|
|
199
|
+
const confirmPortal = confirmOpen && confirmDef && typeof document !== "undefined" && createPortal(_jsx("div", { role: "dialog", "aria-modal": "true", "aria-labelledby": "appkit-action-confirm-title", style: {
|
|
200
|
+
position: "fixed", inset: 0, zIndex: "var(--appkit-z-confirm, 1000)",
|
|
201
|
+
display: "flex", alignItems: "center", justifyContent: "center",
|
|
202
|
+
background: "rgba(0,0,0,0.45)", backdropFilter: "blur(2px)",
|
|
203
|
+
}, onClick: (e) => { if (e.target === e.currentTarget)
|
|
204
|
+
setConfirmOpen(false); }, children: _jsxs("div", { style: {
|
|
205
|
+
background: "var(--appkit-color-surface)",
|
|
206
|
+
borderRadius: "var(--appkit-radius-lg, 12px)",
|
|
207
|
+
padding: "1.5rem",
|
|
208
|
+
maxWidth: "380px", width: "calc(100% - 2rem)",
|
|
209
|
+
boxShadow: "var(--appkit-shadow-lg, 0 20px 60px rgba(0,0,0,0.2))",
|
|
210
|
+
}, children: [_jsx("p", { id: "appkit-action-confirm-title", style: { fontWeight: 600, marginBottom: "0.5rem", fontSize: "1rem" }, children: confirmDef.title }), _jsx("p", { style: { fontSize: "0.875rem", color: "var(--appkit-color-text-muted)", marginBottom: "1.25rem" }, children: confirmDef.body }), _jsxs("div", { style: { display: "flex", gap: "0.5rem", justifyContent: "flex-end" }, children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setConfirmOpen(false), children: confirmDef.cancelLabel ?? "Cancel" }), _jsx(Button, { variant: confirmVariant, size: "sm", onClick: handleConfirm, children: confirmDef.confirmLabel })] })] }) }), document.body);
|
|
211
|
+
if (asChild && React.isValidElement(resolvedChildren)) {
|
|
212
|
+
const child = resolvedChildren;
|
|
213
|
+
// Never spread the caller's raw onClick here — it must go through
|
|
214
|
+
// handleClick so a configured action.confirmation dialog still gates
|
|
215
|
+
// the click even when rendering as a cloned child (e.g. next/link).
|
|
216
|
+
const { onClick: _rawOnClick, ...restProps } = props;
|
|
217
|
+
return (_jsxs(_Fragment, { children: [React.cloneElement(child, {
|
|
218
|
+
...restProps,
|
|
219
|
+
className: twMerge(String(child.props.className ?? ""), classes),
|
|
220
|
+
...(disabled ? { "aria-disabled": true } : {}),
|
|
221
|
+
onClick: handleClick,
|
|
222
|
+
}), confirmPortal] }));
|
|
223
|
+
}
|
|
224
|
+
return (_jsxs(_Fragment, { children: [_jsxs("button", { className: classes, disabled: disabled || isLoading, "aria-busy": isLoading || undefined, ...props, "aria-label": resolvedAriaLabel, onClick: handleClick, children: [isLoading && (_jsx(Loader2, { className: "appkit-button__spinner", "aria-hidden": "true" })), isLoading ? (_jsx("span", { className: "appkit-button__content appkit-button__content--loading", children: resolvedChildren })) : (_jsx("span", { className: "appkit-button__content", children: resolvedChildren }))] }), confirmPortal] }));
|
|
195
225
|
}
|
|
@@ -115,60 +115,61 @@
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
.appkit-button.appkit-button--secondary {
|
|
118
|
-
background: var(--appkit-color-
|
|
118
|
+
background: var(--appkit-color-primary-50);
|
|
119
119
|
border-color: var(--appkit-color-primary-700);
|
|
120
120
|
color: var(--appkit-color-primary-700);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
.appkit-button.appkit-button--secondary:hover:not(:disabled) {
|
|
124
|
-
background:
|
|
124
|
+
background: var(--appkit-color-primary-100);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
.dark .appkit-button.appkit-button--secondary {
|
|
128
|
-
background: transparent;
|
|
128
|
+
background: color-mix(in srgb, var(--appkit-color-secondary) 16%, transparent);
|
|
129
129
|
border-color: var(--appkit-color-secondary);
|
|
130
130
|
color: var(--appkit-color-secondary);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
.dark .appkit-button.appkit-button--secondary:hover:not(:disabled) {
|
|
134
|
-
background: color-mix(in srgb, var(--appkit-color-secondary)
|
|
134
|
+
background: color-mix(in srgb, var(--appkit-color-secondary) 26%, transparent);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
.appkit-button--outline {
|
|
138
|
-
background: var(--appkit-color-surface);
|
|
139
|
-
border-color: var(--appkit-color-
|
|
138
|
+
background: color-mix(in srgb, var(--appkit-color-primary-700) 6%, var(--appkit-color-surface));
|
|
139
|
+
border-color: var(--appkit-color-primary-300);
|
|
140
140
|
color: var(--appkit-color-text);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
.appkit-button--outline:hover:not(:disabled) {
|
|
144
|
-
background: var(--appkit-color-
|
|
144
|
+
background: color-mix(in srgb, var(--appkit-color-primary-700) 12%, var(--appkit-color-surface));
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
.dark .appkit-button--outline {
|
|
148
|
-
background: transparent;
|
|
149
|
-
border-color: var(--appkit-color-
|
|
148
|
+
background: color-mix(in srgb, var(--appkit-color-secondary) 10%, transparent);
|
|
149
|
+
border-color: color-mix(in srgb, var(--appkit-color-secondary) 40%, transparent);
|
|
150
150
|
color: var(--appkit-color-text);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
.dark .appkit-button--outline:hover:not(:disabled) {
|
|
154
|
-
background:
|
|
154
|
+
background: color-mix(in srgb, var(--appkit-color-secondary) 18%, transparent);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
.appkit-button--ghost {
|
|
158
|
-
background:
|
|
158
|
+
background: var(--appkit-color-border-subtle);
|
|
159
159
|
color: var(--appkit-color-text);
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
.appkit-button--ghost:hover:not(:disabled) {
|
|
163
|
-
background: var(--appkit-color-border
|
|
163
|
+
background: var(--appkit-color-border);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
.dark .appkit-button--ghost {
|
|
167
|
+
background: rgba(255, 255, 255, 0.06);
|
|
167
168
|
color: var(--appkit-color-text-muted);
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
.dark .appkit-button--ghost:hover:not(:disabled) {
|
|
171
|
-
background: rgba(
|
|
172
|
+
background: rgba(255, 255, 255, 0.12);
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
.appkit-button.appkit-button--danger {
|
|
@@ -7,12 +7,14 @@ export function DynamicBgDiv({ color, background, textColor, className = "", chi
|
|
|
7
7
|
const el = ref.current;
|
|
8
8
|
if (!el)
|
|
9
9
|
return;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
// Explicitly clear every property this component owns before
|
|
11
|
+
// (re-)applying — otherwise a value set on a previous render (e.g. a
|
|
12
|
+
// `background` gradient) stays in the inline style forever once that
|
|
13
|
+
// prop becomes undefined, even though a sibling prop like `color` is
|
|
14
|
+
// now supposed to take over.
|
|
15
|
+
el.style.background = background ?? "";
|
|
16
|
+
el.style.backgroundColor = !background && color ? color : "";
|
|
17
|
+
el.style.color = textColor ?? "";
|
|
16
18
|
}, [background, color, textColor]);
|
|
17
19
|
return (_jsx("div", { ref: ref, className: className, "aria-hidden": ariaHidden, children: children }));
|
|
18
20
|
}
|
|
@@ -17,7 +17,7 @@ export function FormField({ label, name, card = false, type = "text", value = ""
|
|
|
17
17
|
const errorId = `${inputId}-error`;
|
|
18
18
|
const describedBy = showError ? errorId : undefined;
|
|
19
19
|
if (type === "image" && onUpload) {
|
|
20
|
-
return (_jsxs("div", { className: card ? CARD_CLASS : BASE_CLASS, "data-section": "formfield-div-505", children: [_jsx(ImageUpload, { currentImage: value || undefined, onUpload: onUpload, onChange: (url) => onChange?.(url), label: label ? `${label}${required ? " *" : ""}` : undefined, helperText: hint ?? helpText, captureSource: captureSource ?? "file-only", accept: accept, maxSizeMB: maxSizeMB }), showError ? (_jsx(Text, { id: errorId, size: "sm", variant: "error", className: "appkit-form-field__error", role: "alert", children: error })) : null] }));
|
|
20
|
+
return (_jsxs("div", { className: card ? CARD_CLASS : BASE_CLASS, "data-section": "formfield-div-505", children: [_jsx("div", { "aria-disabled": disabled || undefined, className: disabled ? "pointer-events-none opacity-60" : undefined, children: _jsx(ImageUpload, { currentImage: value || undefined, onUpload: onUpload, onChange: (url) => onChange?.(url), label: label ? `${label}${required ? " *" : ""}` : undefined, helperText: hint ?? helpText, captureSource: captureSource ?? "file-only", accept: accept, maxSizeMB: maxSizeMB }) }), showError ? (_jsx(Text, { id: errorId, size: "sm", variant: "error", className: "appkit-form-field__error", role: "alert", children: error })) : null] }));
|
|
21
21
|
}
|
|
22
22
|
if (type === "media" && onUpload) {
|
|
23
23
|
return (_jsxs("div", { className: card ? CARD_CLASS : BASE_CLASS, "data-section": "formfield-div-506", children: [_jsx(MediaUploadField, { label: `${label || name}${required ? " *" : ""}`, value: value, onChange: (url) => onChange?.(url), onUpload: onUpload, disabled: disabled, helperText: hint ?? helpText, captureSource: captureSource ?? "file-only", captureMode: captureMode ?? "both", accept: accept, maxSizeMB: maxSizeMB }), showError ? (_jsx(Text, { id: errorId, size: "sm", variant: "error", className: "appkit-form-field__error", role: "alert", children: error })) : null] }));
|
|
@@ -194,49 +194,68 @@ export function HorizontalScroller({ children, className = "", gap = 16, snapToI
|
|
|
194
194
|
return () => clearInterval(autoScrollTimer.current);
|
|
195
195
|
}, [autoScroll, isPaused, autoScrollInterval, containerRef]);
|
|
196
196
|
useEffect(() => {
|
|
197
|
-
if (!perView)
|
|
197
|
+
if (!perView && !loop)
|
|
198
198
|
return;
|
|
199
199
|
const el = containerRef.current;
|
|
200
200
|
if (!el)
|
|
201
201
|
return;
|
|
202
202
|
const observer = new ResizeObserver(([entry]) => {
|
|
203
203
|
const w = entry.contentRect.width;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
204
|
+
if (perView) {
|
|
205
|
+
const count = resolvePerView(perView, w);
|
|
206
|
+
if (count > 0) {
|
|
207
|
+
setColCount(count);
|
|
208
|
+
setItemWidth((w - (count - 1) * gap) / count);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// loop is true but no perView hint was given: items render at their
|
|
213
|
+
// natural width, so measure a real rendered item to get the scroll
|
|
214
|
+
// stride the clone-buffer offset / edge teleporter need — without
|
|
215
|
+
// this, itemWidth never resolves and loop mode never initializes.
|
|
216
|
+
const item = el.querySelector(".appkit-hscroller__item");
|
|
217
|
+
if (item)
|
|
218
|
+
setItemWidth(item.getBoundingClientRect().width);
|
|
208
219
|
}
|
|
209
220
|
updateExtents();
|
|
210
221
|
});
|
|
211
222
|
observer.observe(el);
|
|
212
223
|
return () => observer.disconnect();
|
|
213
|
-
}, [perView, gap, containerRef, updateExtents]);
|
|
224
|
+
}, [perView, loop, gap, containerRef, updateExtents]);
|
|
214
225
|
// Recompute extents when content size changes (itemWidth resolved, items count changes).
|
|
215
226
|
useEffect(() => {
|
|
216
227
|
updateExtents();
|
|
217
228
|
}, [updateExtents, itemWidth, itemCount]);
|
|
218
229
|
const content = itemsMode ? (rows > 1 ? (
|
|
219
|
-
// Grid mode: group items into slides of (rows × colCount) cards
|
|
230
|
+
// Grid mode: group items into slides of (rows × colCount) cards.
|
|
231
|
+
// Loop mode prepends a clone of the last slide and appends a clone of the
|
|
232
|
+
// first slide; handleGridScrollLoop teleports across a full cycle at the
|
|
233
|
+
// boundary so the strip appears to scroll infinitely.
|
|
220
234
|
(() => {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
const slideCount = Math.ceil(normalizedItems.length / cardsPerSlide);
|
|
224
|
-
return Array.from({ length: slideCount }, (_, slideIndex) => {
|
|
225
|
-
const slideItems = normalizedItems.slice(slideIndex * cardsPerSlide, (slideIndex + 1) * cardsPerSlide);
|
|
235
|
+
const renderSlide = (slideIndex, key, isClone) => {
|
|
236
|
+
const slideItems = normalizedItems.slice(slideIndex * gridCardsPerSlide, (slideIndex + 1) * gridCardsPerSlide);
|
|
226
237
|
return (_jsx("div", { className: "appkit-hscroller__slide", style: {
|
|
227
238
|
display: "grid",
|
|
228
|
-
gridTemplateColumns: `repeat(${
|
|
239
|
+
gridTemplateColumns: `repeat(${gridCols}, 1fr)`,
|
|
229
240
|
gap: `${gap}px`,
|
|
230
241
|
width: "100%",
|
|
231
242
|
flexShrink: 0,
|
|
232
|
-
}, children: slideItems.map((item, idx) => (_jsx("div", { className: [
|
|
243
|
+
}, "aria-hidden": isClone ? true : undefined, children: slideItems.map((item, idx) => (_jsx("div", { className: [
|
|
233
244
|
"appkit-hscroller__item",
|
|
234
245
|
snapToItems ? "appkit-hscroller__item--snap" : "",
|
|
235
246
|
itemClassName,
|
|
236
247
|
]
|
|
237
248
|
.filter(Boolean)
|
|
238
|
-
.join(" "), style: minItemWidth ? { minWidth: minItemWidth } : undefined, children: renderItem(item, slideIndex *
|
|
239
|
-
}
|
|
249
|
+
.join(" "), style: minItemWidth ? { minWidth: minItemWidth } : undefined, children: renderItem(item, slideIndex * gridCardsPerSlide + idx) }, keyExtractor ? keyExtractor(item, slideIndex * gridCardsPerSlide + idx) : slideIndex * gridCardsPerSlide + idx))) }, key));
|
|
250
|
+
};
|
|
251
|
+
const realSlides = Array.from({ length: gridSlideCount }, (_, slideIndex) => renderSlide(slideIndex, `slide-${slideIndex}`, false));
|
|
252
|
+
if (!gridLoopActive)
|
|
253
|
+
return realSlides;
|
|
254
|
+
return [
|
|
255
|
+
renderSlide(gridSlideCount - 1, "slide-clone-start", true),
|
|
256
|
+
...realSlides,
|
|
257
|
+
renderSlide(0, "slide-clone-end", true),
|
|
258
|
+
];
|
|
240
259
|
})()) : loop && itemCount > 0 ? (
|
|
241
260
|
// Circular loop: fixed (itemCount + 2×loopCloneCount) DOM slots.
|
|
242
261
|
// Each slot maps to a real item via modulo — no array cloning, no list growth.
|
|
@@ -15,7 +15,14 @@ export interface IframeProps extends Omit<IframeHTMLAttributes<HTMLIFrameElement
|
|
|
15
15
|
aspect?: IframeAspect;
|
|
16
16
|
/** Rounded corners preset. Default `"lg"`. */
|
|
17
17
|
rounded?: IframeRounded;
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Sandbox attribute. Default is `"allow-same-origin allow-scripts
|
|
20
|
+
* allow-popups allow-forms"` — permissive enough for trusted third-party
|
|
21
|
+
* embeds (payment checkout panels, YouTube). Combining `allow-same-origin`
|
|
22
|
+
* with `allow-scripts` lets same-origin framed content remove its own
|
|
23
|
+
* sandbox restrictions, so pass a tighter override for any `src` that
|
|
24
|
+
* isn't a fully-trusted origin.
|
|
25
|
+
*/
|
|
19
26
|
sandbox?: string;
|
|
20
27
|
}
|
|
21
28
|
export declare function Iframe({ src, title, aspect, rounded, sandbox, loading, ...rest }: IframeProps): import("react").JSX.Element;
|
|
@@ -184,7 +184,7 @@ export interface ContainerProps extends React.HTMLAttributes<HTMLElement>, Surfa
|
|
|
184
184
|
as?: React.ElementType;
|
|
185
185
|
children?: React.ReactNode;
|
|
186
186
|
}
|
|
187
|
-
export declare function Container({ size, as, surface, padding, rounded, border, shadow, className, children, ...props }: ContainerProps): React.JSX.Element;
|
|
187
|
+
export declare function Container({ size, as, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, className, children, ...props }: ContainerProps): React.JSX.Element;
|
|
188
188
|
/**
|
|
189
189
|
* Vertical flex column. Use instead of `<div className="flex flex-col gap-4" data-section="layout-div-535">`.
|
|
190
190
|
*
|
|
@@ -245,7 +245,7 @@ export interface StackProps extends React.HTMLAttributes<HTMLElement>, SurfacePr
|
|
|
245
245
|
as?: React.ElementType;
|
|
246
246
|
children?: React.ReactNode;
|
|
247
247
|
}
|
|
248
|
-
export declare function Stack({ gap, centered, align, justify, textSize, textWeight, color, divide, direction, wrap, smAlign, as, surface, padding, paddingX, paddingY, rounded, border, shadow, className, children, ...props }: StackProps): React.JSX.Element;
|
|
248
|
+
export declare function Stack({ gap, centered, align, justify, textSize, textWeight, color, divide, direction, wrap, smAlign, as, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, className, children, ...props }: StackProps): React.JSX.Element;
|
|
249
249
|
/**
|
|
250
250
|
* Horizontal flex row. Use instead of `<div className="flex items-center gap-3" data-section="layout-div-536">`.
|
|
251
251
|
*
|
|
@@ -315,7 +315,7 @@ declare const TEXT_COLOR_MAP: {
|
|
|
315
315
|
readonly info: "appkit-color--info";
|
|
316
316
|
};
|
|
317
317
|
type TextColorKey = keyof typeof TEXT_COLOR_MAP;
|
|
318
|
-
export declare function Row({ gap, centered, align, justify, textSize, textWeight, color, wrap, oddEven, divide, as, surface, padding, paddingX, paddingY, rounded, border, shadow, className, children, ...props }: RowProps): React.JSX.Element;
|
|
318
|
+
export declare function Row({ gap, centered, align, justify, textSize, textWeight, color, wrap, oddEven, divide, as, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, className, children, ...props }: RowProps): React.JSX.Element;
|
|
319
319
|
/**
|
|
320
320
|
* Responsive CSS grid. Use instead of `<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4" data-section="layout-div-537">`.
|
|
321
321
|
*
|
|
@@ -355,5 +355,5 @@ export interface GridProps extends React.HTMLAttributes<HTMLElement>, SurfacePro
|
|
|
355
355
|
as?: React.ElementType;
|
|
356
356
|
children?: React.ReactNode;
|
|
357
357
|
}
|
|
358
|
-
export declare function Grid({ cols, gap, align, justify, as, surface, padding, rounded, border, shadow, className, children, ...props }: GridProps): React.JSX.Element;
|
|
358
|
+
export declare function Grid({ cols, gap, align, justify, as, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, className, children, ...props }: GridProps): React.JSX.Element;
|
|
359
359
|
export {};
|
|
@@ -154,9 +154,9 @@ const JUSTIFY_MAP = {
|
|
|
154
154
|
around: "appkit-justify--around",
|
|
155
155
|
evenly: "appkit-justify--evenly",
|
|
156
156
|
};
|
|
157
|
-
export function Container({ size = "2xl", as, surface, padding, rounded, border, shadow, className = "", children, ...props }) {
|
|
157
|
+
export function Container({ size = "2xl", as, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, className = "", children, ...props }) {
|
|
158
158
|
const Tag = (as ?? "div");
|
|
159
|
-
return (_jsx(Tag, { className: ["appkit-container", CONTAINER_MAP[size], buildSurfaceClasses({ surface, padding, rounded, border, shadow }), className]
|
|
159
|
+
return (_jsx(Tag, { className: ["appkit-container", CONTAINER_MAP[size], buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow }), className]
|
|
160
160
|
.filter(Boolean)
|
|
161
161
|
.join(" "), ...props, children: children }));
|
|
162
162
|
}
|
|
@@ -178,7 +178,7 @@ function resolveDivideClass(divide, axis) {
|
|
|
178
178
|
const base = `appkit-${axis}--divide`;
|
|
179
179
|
return divide === "subtle" ? `${base}-subtle` : base;
|
|
180
180
|
}
|
|
181
|
-
export function Stack({ gap = "md", centered = false, align = "stretch", justify = "start", textSize, textWeight, color, divide, direction, wrap = false, smAlign, as, surface, padding, paddingX, paddingY, rounded, border, shadow, className = "", children, ...props }) {
|
|
181
|
+
export function Stack({ gap = "md", centered = false, align = "stretch", justify = "start", textSize, textWeight, color, divide, direction, wrap = false, smAlign, as, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, className = "", children, ...props }) {
|
|
182
182
|
const Tag = (as ?? "div");
|
|
183
183
|
const classes = [
|
|
184
184
|
"appkit-stack",
|
|
@@ -193,7 +193,7 @@ export function Stack({ gap = "md", centered = false, align = "stretch", justify
|
|
|
193
193
|
wrap ? "flex-wrap" : "",
|
|
194
194
|
smAlign ? STACK_SM_ALIGN_MAP[smAlign] : "",
|
|
195
195
|
resolveDivideClass(divide, "stack"),
|
|
196
|
-
buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, border, shadow }),
|
|
196
|
+
buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow }),
|
|
197
197
|
className,
|
|
198
198
|
]
|
|
199
199
|
.filter(Boolean)
|
|
@@ -231,7 +231,7 @@ const ROW_ODD_EVEN_MAP = {
|
|
|
231
231
|
none: "",
|
|
232
232
|
zebra: "even:bg-[var(--appkit-color-surface-input)] dark:even:bg-[var(--appkit-color-bg)]",
|
|
233
233
|
};
|
|
234
|
-
export function Row({ gap = "md", centered = false, align = "center", justify = "start", textSize, textWeight, color, wrap = false, oddEven, divide, as, surface, padding, paddingX, paddingY, rounded, border, shadow, className = "", children, ...props }) {
|
|
234
|
+
export function Row({ gap = "md", centered = false, align = "center", justify = "start", textSize, textWeight, color, wrap = false, oddEven, divide, as, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, className = "", children, ...props }) {
|
|
235
235
|
const Tag = (as ?? "div");
|
|
236
236
|
const classes = [
|
|
237
237
|
"appkit-row",
|
|
@@ -244,14 +244,14 @@ export function Row({ gap = "md", centered = false, align = "center", justify =
|
|
|
244
244
|
textWeight ? TEXT_WEIGHT_MAP[textWeight] : "",
|
|
245
245
|
color ? TEXT_COLOR_MAP[color] : "",
|
|
246
246
|
resolveDivideClass(divide, "row"),
|
|
247
|
-
buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, border, shadow }),
|
|
247
|
+
buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow }),
|
|
248
248
|
className,
|
|
249
249
|
]
|
|
250
250
|
.filter(Boolean)
|
|
251
251
|
.join(" ");
|
|
252
252
|
return (_jsx(Tag, { className: classes, ...props, children: children }));
|
|
253
253
|
}
|
|
254
|
-
export function Grid({ cols, gap = "md", align, justify, as, surface, padding, rounded, border, shadow, className = "", children, ...props }) {
|
|
254
|
+
export function Grid({ cols, gap = "md", align, justify, as, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, className = "", children, ...props }) {
|
|
255
255
|
const Tag = (as ?? "div");
|
|
256
256
|
const baseClass = cols !== undefined ? GRID_MAP[cols] : "appkit-grid";
|
|
257
257
|
const classes = [
|
|
@@ -259,7 +259,7 @@ export function Grid({ cols, gap = "md", align, justify, as, surface, padding, r
|
|
|
259
259
|
GAP_MAP[gap],
|
|
260
260
|
align ? ITEMS_MAP[align] : "",
|
|
261
261
|
justify ? JUSTIFY_MAP[justify] : "",
|
|
262
|
-
buildSurfaceClasses({ surface, padding, rounded, border, shadow }),
|
|
262
|
+
buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow }),
|
|
263
263
|
className,
|
|
264
264
|
]
|
|
265
265
|
.filter(Boolean)
|
|
@@ -20,7 +20,7 @@ const DEFAULT_LABELS = {
|
|
|
20
20
|
clearSelection: "Clear",
|
|
21
21
|
};
|
|
22
22
|
const VIEW_BTN_BASE = "p-1.5 sm:p-2 transition-colors";
|
|
23
|
-
const VIEW_BTN_ACTIVE = "bg-[var(--appkit-color-primary
|
|
23
|
+
const VIEW_BTN_ACTIVE = "bg-[var(--appkit-color-primary)] text-white";
|
|
24
24
|
const VIEW_BTN_INACTIVE = "text-[var(--appkit-color-text-muted)] hover:bg-zinc-50 hover:bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]";
|
|
25
25
|
export function ListingToolbar({ filterCount = 0, onFiltersClick, searchValue = "", searchPlaceholder = "Search…", onSearchChange, onSearchCommit, onSearchKeyDown, sortValue, sortOptions, onSortChange, view = "grid", onViewChange, hideViewToggle = false, showTableView = false, onResetAll, hasActiveState = false, bulkMode = false, bulkSelectedCount = 0, bulkTotalCount = 0, onBulkSelectAll, onBulkClear, toggles, extra, labels, className = "", }) {
|
|
26
26
|
const l = { ...DEFAULT_LABELS, ...labels };
|
|
@@ -34,8 +34,8 @@ export function ListingToolbar({ filterCount = 0, onFiltersClick, searchValue =
|
|
|
34
34
|
};
|
|
35
35
|
const allSelected = bulkTotalCount > 0 && bulkSelectedCount === bulkTotalCount;
|
|
36
36
|
return (_jsx("div", { "data-testid": "listing-toolbar", className: `sticky top-[var(--header-height,0px)] z-20 border-b border-[var(--appkit-color-border)] bg-white/95 bg-[var(--appkit-color-surface)]/95 backdrop-blur-sm py-2 px-3 sm:py-2.5 sm:px-4 ${className}`, children: _jsxs("div", { className: "flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-2.5", children: [bulkMode ? (_jsxs("div", { className: "flex flex-1 items-center gap-2", children: [_jsxs("button", { type: "button", onClick: onBulkSelectAll, className: "flex items-center gap-1.5 rounded-lg border border-[var(--appkit-color-border)] px-3 py-1.5 text-sm font-medium text-[var(--appkit-color-text-muted)] hover:bg-zinc-50 hover:bg-[var(--appkit-color-surface-elevated)] transition-colors", children: [allSelected
|
|
37
|
-
? _jsx(CheckSquare, { className: "h-4 w-4 text-[var(--appkit-color-primary
|
|
38
|
-
: _jsx(Square, { className: "h-4 w-4" }), allSelected ? l.deselectAll : l.selectAll(bulkTotalCount)] }), _jsx(Span, { size: "sm", color: "muted", children: l.selected(bulkSelectedCount) }), _jsx("button", { type: "button", onClick: onBulkClear, className: CLS_CLEAR_LINK, children: l.clearSelection })] })) : onSearchChange ? (_jsxs("div", { className: "flex flex-1 items-center overflow-hidden rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] min-w-0", children: [_jsx("input", { type: "text", value: searchValue, onChange: (e) => onSearchChange(e.target.value), onKeyDown: handleKeyDown, placeholder: searchPlaceholder, className: "min-w-0 flex-1 bg-transparent px-3 py-2 text-sm text-[var(--appkit-color-text)] placeholder-zinc-400 outline-none" }), _jsx("button", { type: "button", onClick: onSearchCommit, className: "flex shrink-0 items-center justify-center px-2.5 py-2 text-zinc-400 hover:text-[var(--appkit-color-primary
|
|
39
|
-
? "border-transparent bg-[var(--appkit-color-primary
|
|
37
|
+
? _jsx(CheckSquare, { className: "h-4 w-4 text-[var(--appkit-color-primary)]" })
|
|
38
|
+
: _jsx(Square, { className: "h-4 w-4" }), allSelected ? l.deselectAll : l.selectAll(bulkTotalCount)] }), _jsx(Span, { size: "sm", color: "muted", children: l.selected(bulkSelectedCount) }), _jsx("button", { type: "button", onClick: onBulkClear, className: CLS_CLEAR_LINK, children: l.clearSelection })] })) : onSearchChange ? (_jsxs("div", { className: "flex flex-1 items-center overflow-hidden rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] min-w-0", children: [_jsx("input", { type: "text", value: searchValue, onChange: (e) => onSearchChange(e.target.value), onKeyDown: handleKeyDown, placeholder: searchPlaceholder, className: "min-w-0 flex-1 bg-transparent px-3 py-2 text-sm text-[var(--appkit-color-text)] placeholder-zinc-400 outline-none" }), _jsx("button", { type: "button", onClick: onSearchCommit, className: "flex shrink-0 items-center justify-center px-2.5 py-2 text-zinc-400 hover:text-[var(--appkit-color-primary)] transition-colors", "aria-label": l.search, children: _jsx(Search, { className: "h-4 w-4" }) })] })) : null, _jsxs("div", { className: "flex items-center gap-1.5 sm:gap-2 shrink-0", children: [onFiltersClick && (_jsxs("button", { type: "button", onClick: onFiltersClick, className: "relative flex shrink-0 items-center gap-1.5 rounded-lg border border-[var(--appkit-color-border)] px-2.5 py-1.5 sm:px-3.5 sm:py-2 text-sm font-medium text-[var(--appkit-color-text-muted)] hover:bg-zinc-50 hover:bg-[var(--appkit-color-surface-elevated)] transition-colors", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: l.filters }), filterCount > 0 && (_jsx(Span, { weight: "bold", className: "absolute -top-1.5 -right-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-[var(--appkit-color-primary)] text-[10px] text-white", children: filterCount }))] })), sortOptions && sortValue !== undefined && onSortChange && (_jsxs(Div, { className: "flex items-center gap-1.5 text-[var(--appkit-color-text-muted)]", children: [_jsx(Span, { size: "xs", className: "hidden md:inline whitespace-nowrap", children: l.sort }), _jsx(SortDropdown, { value: sortValue, onChange: onSortChange, options: sortOptions })] })), !hideViewToggle && onViewChange && (_jsxs("div", { className: "flex items-center rounded-lg border border-[var(--appkit-color-border)] overflow-hidden", children: [_jsx("button", { type: "button", onClick: () => onViewChange("grid"), "aria-label": l.gridView, className: `${VIEW_BTN_BASE} ${view === "grid" ? VIEW_BTN_ACTIVE : VIEW_BTN_INACTIVE}`, children: _jsx(LayoutGrid, { className: "h-4 w-4" }) }), _jsx("button", { type: "button", onClick: () => onViewChange("list"), "aria-label": l.listView, className: `${VIEW_BTN_BASE} ${view === "list" ? VIEW_BTN_ACTIVE : VIEW_BTN_INACTIVE}`, children: _jsx(List, { className: "h-4 w-4" }) }), showTableView && (_jsx("button", { type: "button", onClick: () => onViewChange("table"), "aria-label": l.tableView, className: `${VIEW_BTN_BASE} ${view === "table" ? VIEW_BTN_ACTIVE : VIEW_BTN_INACTIVE}`, children: _jsx(Table2, { className: "h-4 w-4" }) }))] })), onResetAll && hasActiveState && (_jsx("button", { type: "button", onClick: onResetAll, "aria-label": l.resetAll, title: l.resetAll, className: CLS_CLEAR_ICON_BTN, children: _jsx(RotateCcw, { className: "h-4 w-4" }) })), toggles && toggles.length > 0 && (_jsx("div", { className: "flex items-center gap-1", children: toggles.map((t) => (_jsx("button", { type: "button", role: "switch", "aria-checked": t.active, onClick: () => t.onChange(!t.active), className: `rounded-full border px-2.5 py-1 text-xs font-medium transition-colors whitespace-nowrap ${t.active
|
|
39
|
+
? "border-transparent bg-[var(--appkit-color-primary)] text-white"
|
|
40
40
|
: "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] hover:bg-zinc-50 hover:bg-[var(--appkit-color-surface-elevated)]"}`, children: t.label }, t.label))) })), extra] })] }) }));
|
|
41
41
|
}
|
|
@@ -108,7 +108,10 @@ export function AnimatedRow({ delay = 0, children, className, ...props }) {
|
|
|
108
108
|
}
|
|
109
109
|
export function Draggable({ axis, constraints, dragElastic = 0.1, children, ...props }) {
|
|
110
110
|
const reduced = useReducedMotion();
|
|
111
|
-
|
|
111
|
+
// `axis` defaults to unrestricted (true) when omitted, but an explicit
|
|
112
|
+
// `axis={false}` must actually disable dragging — `axis || true` always
|
|
113
|
+
// collapsed a falsy axis back to `true`, making `axis={false}` a no-op.
|
|
114
|
+
const drag = reduced ? false : (axis ?? true);
|
|
112
115
|
return (_jsx(motion.div, { drag: drag, dragConstraints: constraints, dragElastic: dragElastic, ...props, children: children }));
|
|
113
116
|
}
|
|
114
117
|
export function Swipeable({ onSwipeLeft, onSwipeRight, threshold = 50, children, ...props }) {
|
|
@@ -47,7 +47,7 @@ export function OtpInput({ length = 6, value, onChange, label, error, helperText
|
|
|
47
47
|
const lastFilled = Math.min(sanitized.length, length - 1);
|
|
48
48
|
focusAt(lastFilled);
|
|
49
49
|
}
|
|
50
|
-
return (_jsxs("div", { className: "w-full", children: [label && (_jsx(Label, { htmlFor: `${inputId}-0`, className: "appkit-form-field__label mb-2", children: label })), _jsx("div", { className: "flex gap-2", role: "group", "aria-labelledby": label ? `${inputId}-label` : undefined, children: digits.map((digit, i) => (_jsx("input", { ref: (el) => { inputsRef.current[i] = el; }, id: i === 0 ? `${inputId}-0` : undefined, type: "text", inputMode: inputMode, maxLength: 1, value: digit, disabled: disabled, autoFocus: autoFocus && i === 0, autoComplete: "one-time-code", "aria-label": `Digit ${i + 1} of ${length}`, className: [
|
|
50
|
+
return (_jsxs("div", { className: "w-full", children: [label && (_jsx(Label, { id: `${inputId}-label`, htmlFor: `${inputId}-0`, className: "appkit-form-field__label mb-2", children: label })), _jsx("div", { className: "flex gap-2", role: "group", "aria-labelledby": label ? `${inputId}-label` : undefined, children: digits.map((digit, i) => (_jsx("input", { ref: (el) => { inputsRef.current[i] = el; }, id: i === 0 ? `${inputId}-0` : undefined, type: "text", inputMode: inputMode, maxLength: 1, value: digit, disabled: disabled, autoFocus: autoFocus && i === 0, autoComplete: "one-time-code", "aria-label": `Digit ${i + 1} of ${length}`, className: [
|
|
51
51
|
"appkit-input",
|
|
52
52
|
"appkit-otp-input",
|
|
53
53
|
error ? "appkit-input--error" : "",
|
|
@@ -64,15 +64,26 @@ export function PaginatedSelect(props) {
|
|
|
64
64
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
65
65
|
const containerRef = useRef(null);
|
|
66
66
|
const labelMap = useRef(new Map());
|
|
67
|
+
// Monotonic counter guarding against out-of-order async responses: a
|
|
68
|
+
// slower earlier request (e.g. for an already-abandoned search query)
|
|
69
|
+
// must never overwrite state after a faster, more recent request resolved.
|
|
70
|
+
const requestIdRef = useRef(0);
|
|
71
|
+
const wasOpenRef = useRef(false);
|
|
67
72
|
const hasCreate = Boolean(createLabel && (renderCreateForm ?? (createFields && onCreateSubmit)));
|
|
68
73
|
const resolvedOptions = options ?? asyncOptions;
|
|
69
74
|
resolvedOptions.forEach((o) => labelMap.current.set(o.value, o.label));
|
|
70
75
|
const load = useCallback(async (search, nextPage, reset = false) => {
|
|
71
76
|
if (!loadOptions)
|
|
72
77
|
return;
|
|
78
|
+
const requestId = ++requestIdRef.current;
|
|
73
79
|
setLoading(true);
|
|
74
80
|
try {
|
|
75
81
|
const response = await loadOptions(search, nextPage);
|
|
82
|
+
// A newer request (new search, or the dropdown reopened) was issued
|
|
83
|
+
// while this one was in flight — its response is stale, discard it
|
|
84
|
+
// rather than clobbering state a more recent request already set.
|
|
85
|
+
if (requestId !== requestIdRef.current)
|
|
86
|
+
return;
|
|
76
87
|
setAsyncOptions((prev) => {
|
|
77
88
|
const merged = reset ? response.items : [...prev, ...response.items];
|
|
78
89
|
merged.forEach((o) => labelMap.current.set(o.value, o.label));
|
|
@@ -82,13 +93,28 @@ export function PaginatedSelect(props) {
|
|
|
82
93
|
setHasMore(response.hasMore);
|
|
83
94
|
}
|
|
84
95
|
finally {
|
|
85
|
-
|
|
96
|
+
if (requestId === requestIdRef.current)
|
|
97
|
+
setLoading(false);
|
|
86
98
|
}
|
|
87
99
|
}, [loadOptions]);
|
|
88
100
|
useEffect(() => {
|
|
89
|
-
if (!open || !loadOptions)
|
|
101
|
+
if (!open || !loadOptions) {
|
|
102
|
+
wasOpenRef.current = false;
|
|
90
103
|
return;
|
|
91
|
-
|
|
104
|
+
}
|
|
105
|
+
// Load immediately the moment the dropdown opens; debounce subsequent
|
|
106
|
+
// reloads triggered by the user typing so every keystroke doesn't fire
|
|
107
|
+
// a new request.
|
|
108
|
+
const isFreshOpen = !wasOpenRef.current;
|
|
109
|
+
wasOpenRef.current = true;
|
|
110
|
+
if (isFreshOpen) {
|
|
111
|
+
void load(query, 1, true);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const timer = setTimeout(() => {
|
|
115
|
+
void load(query, 1, true);
|
|
116
|
+
}, 300);
|
|
117
|
+
return () => clearTimeout(timer);
|
|
92
118
|
}, [open, query, load, loadOptions]);
|
|
93
119
|
useEffect(() => {
|
|
94
120
|
if (!open || !containerRef.current)
|