@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.
Files changed (70) hide show
  1. package/dist/features/homepage/components/CustomCardsSection.js +1 -1
  2. package/dist/features/homepage/components/SectionCarousel.js +1 -1
  3. package/dist/features/seller/components/CategoryInlineSelect.d.ts +1 -1
  4. package/dist/features/seller/components/CategoryInlineSelect.js +1 -1
  5. package/dist/react/hooks/useBulkSelection.js +12 -3
  6. package/dist/react/hooks/useGesture.js +48 -4
  7. package/dist/react/hooks/useLongPress.d.ts +1 -0
  8. package/dist/react/hooks/useLongPress.js +5 -0
  9. package/dist/react/hooks/usePullToRefresh.js +12 -1
  10. package/dist/react/hooks/useRealtimeEvent.js +18 -1
  11. package/dist/react/hooks/useSwipe.js +14 -0
  12. package/dist/seed/addresses-seed-data.js +8 -99
  13. package/dist/seed/categories-seed-data.js +103 -2082
  14. package/dist/seed/homepage-sections-seed-data.js +9 -9
  15. package/dist/seed/products-art-seed-data.d.ts +1 -10
  16. package/dist/seed/products-art-seed-data.js +13 -122
  17. package/dist/seed/products-auctions-seed-data.js +39 -747
  18. package/dist/seed/products-classifieds-seed-data.d.ts +1 -9
  19. package/dist/seed/products-classifieds-seed-data.js +13 -195
  20. package/dist/seed/products-digital-codes-seed-data.d.ts +1 -9
  21. package/dist/seed/products-digital-codes-seed-data.js +13 -202
  22. package/dist/seed/products-live-items-seed-data.d.ts +1 -12
  23. package/dist/seed/products-live-items-seed-data.js +13 -188
  24. package/dist/seed/products-preorders-seed-data.js +18 -149
  25. package/dist/seed/products-prize-draws-seed-data.d.ts +1 -16
  26. package/dist/seed/products-prize-draws-seed-data.js +13 -385
  27. package/dist/seed/products-standard-seed-data.js +152 -1331
  28. package/dist/seed/products-stickers-seed-data.d.ts +1 -10
  29. package/dist/seed/products-stickers-seed-data.js +13 -122
  30. package/dist/seed/site-settings-seed-data.js +106 -108
  31. package/dist/seed/store-addresses-seed-data.js +14 -46
  32. package/dist/seed/stores-seed-data.js +13 -205
  33. package/dist/styles.css +19 -18
  34. package/dist/tailwind-utilities.css +1 -1
  35. package/dist/ui/components/BaseListingCard.d.ts +2 -1
  36. package/dist/ui/components/BaseListingCard.js +4 -5
  37. package/dist/ui/components/BulkActionBar.style.css +4 -4
  38. package/dist/ui/components/Button.js +52 -22
  39. package/dist/ui/components/Button.style.css +14 -13
  40. package/dist/ui/components/DateInput.js +1 -0
  41. package/dist/ui/components/DynamicBgDiv.js +8 -6
  42. package/dist/ui/components/FormField.js +1 -1
  43. package/dist/ui/components/HorizontalScroller.js +35 -16
  44. package/dist/ui/components/Iframe.d.ts +8 -1
  45. package/dist/ui/components/Layout.d.ts +4 -4
  46. package/dist/ui/components/Layout.js +8 -8
  47. package/dist/ui/components/ListingToolbar.js +4 -4
  48. package/dist/ui/components/Motion.js +4 -1
  49. package/dist/ui/components/OtpInput.js +1 -1
  50. package/dist/ui/components/PaginatedSelect.js +29 -3
  51. package/dist/ui/components/Pagination.js +10 -5
  52. package/dist/ui/components/RichTextEditor.js +64 -3
  53. package/dist/ui/components/Select.js +1 -0
  54. package/dist/ui/components/Semantic.d.ts +10 -10
  55. package/dist/ui/components/Semantic.js +25 -25
  56. package/dist/ui/components/SideDrawer.js +13 -11
  57. package/dist/ui/components/SideModal.js +9 -7
  58. package/dist/ui/components/SlottedListingView.d.ts +13 -2
  59. package/dist/ui/components/StickyToolbar.js +9 -5
  60. package/dist/ui/components/TagInput.js +15 -2
  61. package/dist/ui/components/Textarea.js +1 -0
  62. package/dist/ui/components/Toggle.js +8 -1
  63. package/dist/ui/components/UnsavedChangesModal.js +12 -1
  64. package/dist/ui/forms/ColorPickerField.js +7 -0
  65. package/dist/ui/forms/FieldCheckbox.js +1 -1
  66. package/dist/utils/id-generators.d.ts +7 -4
  67. package/dist/utils/id-generators.js +53 -31
  68. package/dist/utils/number.formatter.js +19 -6
  69. package/dist/utils/string.formatter.js +4 -1
  70. 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?: string;
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
- const aspectClass = aspect === "square"
29
- ? "aspect-square"
30
- : aspect === "4/3" || !aspect
31
- ? "aspect-[4/3]"
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: rgba(233, 30, 140, 0.3);
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: rgba(233, 30, 140, 0.35);
172
- background: rgba(233, 30, 140, 0.1);
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: rgba(233, 30, 140, 0.18);
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((event) => {
179
+ const handleConfirm = useCallback(() => {
168
180
  setConfirmOpen(false);
169
- wrapAsync(userOnClick)(event);
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
- 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 }))] }), confirmOpen && confirmDef && typeof document !== "undefined" && createPortal(_jsx("div", { role: "dialog", "aria-modal": "true", "aria-labelledby": "appkit-action-confirm-title", style: {
184
- position: "fixed", inset: 0, zIndex: "var(--appkit-z-confirm, 1000)",
185
- display: "flex", alignItems: "center", justifyContent: "center",
186
- background: "rgba(0,0,0,0.45)", backdropFilter: "blur(2px)",
187
- }, onClick: (e) => { if (e.target === e.currentTarget)
188
- setConfirmOpen(false); }, children: _jsxs("div", { style: {
189
- background: "var(--appkit-color-surface)",
190
- borderRadius: "var(--appkit-radius-lg, 12px)",
191
- padding: "1.5rem",
192
- maxWidth: "380px", width: "calc(100% - 2rem)",
193
- boxShadow: "var(--appkit-shadow-lg, 0 20px 60px rgba(0,0,0,0.2))",
194
- }, 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)] }));
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-surface);
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: color-mix(in srgb, var(--appkit-color-primary-700) 8%, transparent);
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) 12%, transparent);
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-border);
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-bg);
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-border);
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: rgba(30, 41, 59, 0.5);
154
+ background: color-mix(in srgb, var(--appkit-color-secondary) 18%, transparent);
155
155
  }
156
156
 
157
157
  .appkit-button--ghost {
158
- background: transparent;
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-subtle);
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(30, 41, 59, 0.5);
172
+ background: rgba(255, 255, 255, 0.12);
172
173
  }
173
174
 
174
175
  .appkit-button.appkit-button--danger {
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import React from "react";
3
4
  import { Label, Span, Text } from "./Typography";
@@ -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
- if (background)
11
- el.style.background = background;
12
- else if (color)
13
- el.style.backgroundColor = color;
14
- if (textColor)
15
- el.style.color = textColor;
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
- const count = resolvePerView(perView, w);
205
- if (count > 0) {
206
- setColCount(count);
207
- setItemWidth((w - (count - 1) * gap) / count);
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 cols = colCount > 0 ? colCount : 3;
222
- const cardsPerSlide = rows * cols;
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(${cols}, 1fr)`,
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 * cardsPerSlide + idx) }, keyExtractor ? keyExtractor(item, slideIndex * cardsPerSlide + idx) : slideIndex * cardsPerSlide + idx))) }, `slide-${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
- /** Sandbox attribute. Default permits same-origin only. */
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,theme(colors.violet.600))] text-white";
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,theme(colors.violet.600))]" })
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,theme(colors.violet.600))] 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,theme(colors.violet.600))] 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,theme(colors.violet.600))] text-white"
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
- const drag = reduced ? false : (axis === true ? true : axis || true);
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
- setLoading(false);
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
- void load(query, 1, true);
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)