@mohasinac/appkit 3.5.3 → 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 (44) hide show
  1. package/dist/features/seller/components/CategoryInlineSelect.d.ts +1 -1
  2. package/dist/features/seller/components/CategoryInlineSelect.js +1 -1
  3. package/dist/react/hooks/useBulkSelection.js +12 -3
  4. package/dist/react/hooks/useGesture.js +48 -4
  5. package/dist/react/hooks/useLongPress.d.ts +1 -0
  6. package/dist/react/hooks/useLongPress.js +5 -0
  7. package/dist/react/hooks/usePullToRefresh.js +12 -1
  8. package/dist/react/hooks/useRealtimeEvent.js +18 -1
  9. package/dist/react/hooks/useSwipe.js +14 -0
  10. package/dist/styles.css +1 -1
  11. package/dist/tailwind-utilities.css +1 -1
  12. package/dist/ui/components/BaseListingCard.d.ts +2 -1
  13. package/dist/ui/components/BaseListingCard.js +4 -5
  14. package/dist/ui/components/Button.js +52 -22
  15. package/dist/ui/components/DateInput.js +1 -0
  16. package/dist/ui/components/DynamicBgDiv.js +8 -6
  17. package/dist/ui/components/FormField.js +1 -1
  18. package/dist/ui/components/HorizontalScroller.js +17 -6
  19. package/dist/ui/components/Iframe.d.ts +8 -1
  20. package/dist/ui/components/Layout.d.ts +4 -4
  21. package/dist/ui/components/Layout.js +8 -8
  22. package/dist/ui/components/Motion.js +4 -1
  23. package/dist/ui/components/OtpInput.js +1 -1
  24. package/dist/ui/components/PaginatedSelect.js +29 -3
  25. package/dist/ui/components/Pagination.js +10 -5
  26. package/dist/ui/components/RichTextEditor.js +64 -3
  27. package/dist/ui/components/Select.js +1 -0
  28. package/dist/ui/components/Semantic.d.ts +10 -10
  29. package/dist/ui/components/Semantic.js +25 -25
  30. package/dist/ui/components/SideDrawer.js +13 -11
  31. package/dist/ui/components/SideModal.js +9 -7
  32. package/dist/ui/components/SlottedListingView.d.ts +13 -2
  33. package/dist/ui/components/StickyToolbar.js +9 -5
  34. package/dist/ui/components/TagInput.js +15 -2
  35. package/dist/ui/components/Textarea.js +1 -0
  36. package/dist/ui/components/Toggle.js +8 -1
  37. package/dist/ui/components/UnsavedChangesModal.js +12 -1
  38. package/dist/ui/forms/ColorPickerField.js +7 -0
  39. package/dist/ui/forms/FieldCheckbox.js +1 -1
  40. package/dist/utils/id-generators.d.ts +7 -4
  41. package/dist/utils/id-generators.js +53 -31
  42. package/dist/utils/number.formatter.js +19 -6
  43. package/dist/utils/string.formatter.js +4 -1
  44. 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,
@@ -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
  }
@@ -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,23 +194,34 @@ 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();
@@ -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)
@@ -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)
@@ -6,12 +6,17 @@ function getPageNumbers(currentPage, totalPages, maxVisible) {
6
6
  return Array.from({ length: totalPages }, (_, i) => i + 1);
7
7
  }
8
8
  const pages = [];
9
- const halfVisible = Math.floor(maxVisible / 2);
10
- let startPage = Math.max(1, currentPage - halfVisible);
11
- let endPage = Math.min(totalPages, currentPage + halfVisible);
12
- if (currentPage <= halfVisible)
9
+ // Split unevenly (floor/ceil) around (maxVisible - 1) rather than using a
10
+ // single halved value on both sides — `Math.floor(maxVisible / 2)` on
11
+ // both sides produces a window of `2*halfVisible + 1`, which is one page
12
+ // too many whenever maxVisible is even.
13
+ const before = Math.floor((maxVisible - 1) / 2);
14
+ const after = Math.ceil((maxVisible - 1) / 2);
15
+ let startPage = Math.max(1, currentPage - before);
16
+ let endPage = Math.min(totalPages, currentPage + after);
17
+ if (currentPage <= before)
13
18
  endPage = maxVisible;
14
- if (currentPage >= totalPages - halfVisible)
19
+ if (currentPage >= totalPages - after)
15
20
  startPage = totalPages - maxVisible + 1;
16
21
  if (startPage > 1) {
17
22
  pages.push(1);
@@ -1,12 +1,69 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useMemo, useRef } from "react";
4
+ // Matches exactly what this editor's own toolbar (bold/italic/underline/
5
+ // lists/link/clear-formatting) and typing can legitimately produce.
6
+ // Anything else — <script>, <img onerror=...>, <iframe>, <svg>, <style>,
7
+ // <form>, event-handler attributes — can only have arrived via paste, and is
8
+ // dropped entirely rather than risk it either executing live in the editor
9
+ // or being persisted and re-rendered elsewhere as stored XSS.
10
+ const ALLOWED_TAGS = new Set([
11
+ "B", "STRONG", "I", "EM", "U", "S", "STRIKE",
12
+ "UL", "OL", "LI", "A", "BR", "DIV", "SPAN", "P",
13
+ ]);
14
+ function isSafeHref(href) {
15
+ const trimmed = href.trim();
16
+ // Same rule as string.formatter.ts's applyMark: allow http(s)/mailto, a
17
+ // single leading "/" (same-site relative path, NOT "//host" protocol-
18
+ // relative), or "#" — reject everything else including javascript:.
19
+ return /^(https?:\/\/|mailto:|\/(?!\/)|#)/i.test(trimmed);
20
+ }
21
+ function sanitizeRichTextHtml(html) {
22
+ if (typeof document === "undefined" || !html)
23
+ return "";
24
+ const template = document.createElement("template");
25
+ template.innerHTML = html;
26
+ const walk = (root) => {
27
+ Array.from(root.childNodes).forEach((child) => {
28
+ if (child.nodeType === Node.ELEMENT_NODE) {
29
+ const el = child;
30
+ if (!ALLOWED_TAGS.has(el.tagName)) {
31
+ root.removeChild(el);
32
+ return;
33
+ }
34
+ Array.from(el.attributes).forEach((attr) => {
35
+ if (el.tagName === "A" && attr.name.toLowerCase() === "href") {
36
+ if (!isSafeHref(attr.value))
37
+ el.setAttribute("href", "#");
38
+ return;
39
+ }
40
+ el.removeAttribute(attr.name);
41
+ });
42
+ walk(el);
43
+ }
44
+ else if (child.nodeType !== Node.TEXT_NODE) {
45
+ root.removeChild(child);
46
+ }
47
+ });
48
+ };
49
+ walk(template.content);
50
+ return template.innerHTML;
51
+ }
4
52
  export function RichTextEditor({ value, onChange, disabled = false, className = "", minHeightClassName = "min-h-[180px]", placeholder = "Write formatted content...", }) {
5
53
  const editorRef = useRef(null);
6
54
  const emitChange = () => {
7
55
  if (!editorRef.current)
8
56
  return;
9
- onChange(editorRef.current.innerHTML);
57
+ const raw = editorRef.current.innerHTML;
58
+ const sanitized = sanitizeRichTextHtml(raw);
59
+ // Only touch the live DOM (which would reset caret position) when
60
+ // sanitization actually changed something — a no-op for every normal
61
+ // keystroke/toolbar action, since those only ever produce allowlisted
62
+ // markup; only a malicious paste triggers this branch.
63
+ if (sanitized !== raw) {
64
+ editorRef.current.innerHTML = sanitized;
65
+ }
66
+ onChange(sanitized);
10
67
  };
11
68
  const exec = (command, commandValue) => {
12
69
  if (disabled || typeof document === "undefined")
@@ -51,8 +108,12 @@ export function RichTextEditor({ value, onChange, disabled = false, className =
51
108
  const editor = editorRef.current;
52
109
  if (!editor)
53
110
  return;
54
- if (editor.innerHTML !== value) {
55
- editor.innerHTML = value;
111
+ // Sanitize on every incoming `value` too — this is the stored-XSS path:
112
+ // previously-saved content (e.g. loaded from Firestore) must not be
113
+ // trusted just because it was already persisted.
114
+ const safeValue = sanitizeRichTextHtml(value);
115
+ if (editor.innerHTML !== safeValue) {
116
+ editor.innerHTML = safeValue;
56
117
  }
57
118
  }, [value]);
58
119
  return (_jsxs("div", { className: `appkit-rich-text-editor rounded-lg border border-zinc-200 bg-[var(--appkit-color-surface)] border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] ${className}`, "data-section": "richtexteditor-div-582", children: [_jsx("div", { className: "appkit-rich-text-editor__toolbar flex flex-wrap gap-1 border-b border-zinc-200 p-2 border-[var(--appkit-color-border)]", "data-section": "richtexteditor-div-583", children: toolbarActions.map((action) => (_jsx("button", { type: "button", title: action.title, onClick: action.run, disabled: disabled, className: "appkit-rich-text-editor__btn rounded px-2 py-1 text-xs font-medium text-zinc-700 transition hover:bg-zinc-50 disabled:cursor-not-allowed disabled:opacity-50 text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface-elevated)]", children: action.label }, action.title))) }), _jsx("div", { ref: editorRef, role: "textbox", "aria-label": "Rich text editor", contentEditable: !disabled, suppressContentEditableWarning: true, "data-placeholder": placeholder, onInput: emitChange, onBlur: emitChange, className: `prose prose-sm max-w-none p-3 focus:outline-none dark:prose-invert ${minHeightClassName} ${disabled ? "cursor-not-allowed opacity-70" : ""}` })] }));
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import React from "react";
3
4
  import { Div } from "./Div";
@@ -87,7 +87,7 @@ export interface ArticleProps extends React.HTMLAttributes<HTMLElement>, Surface
87
87
  color?: SemanticColor;
88
88
  children?: React.ReactNode;
89
89
  }
90
- export declare function Article({ className, surface, padding, rounded, border, shadow, color, children, ...props }: ArticleProps): React.JSX.Element;
90
+ export declare function Article({ className, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, color, children, ...props }: ArticleProps): React.JSX.Element;
91
91
  /**
92
92
  * Semantic `<main>` element.
93
93
  * Wraps the primary content of the document. Should appear only once per page.
@@ -96,7 +96,7 @@ export interface MainProps extends React.HTMLAttributes<HTMLElement>, SurfacePro
96
96
  color?: SemanticColor;
97
97
  children: React.ReactNode;
98
98
  }
99
- export declare function Main({ className, surface, padding, paddingX, paddingY, rounded, border, shadow, color, children, ...props }: MainProps): React.JSX.Element;
99
+ export declare function Main({ className, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, color, children, ...props }: MainProps): React.JSX.Element;
100
100
  /**
101
101
  * Semantic `<aside>` element.
102
102
  * Use for supplementary content tangentially related to the main content:
@@ -134,7 +134,7 @@ export interface NavProps extends React.HTMLAttributes<HTMLElement>, SurfaceProp
134
134
  textSize?: "xs" | "sm" | "base" | "lg";
135
135
  children: React.ReactNode;
136
136
  }
137
- export declare function Nav({ surface, padding, rounded, border, shadow, spacing, gap, layout, color, textSize, className, children, ...props }: NavProps): React.JSX.Element;
137
+ export declare function Nav({ surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, spacing, gap, layout, color, textSize, className, children, ...props }: NavProps): React.JSX.Element;
138
138
  /**
139
139
  * Semantic `<header>` element for block-level component headers.
140
140
  * Use inside `Section`, `Article`, or card bodies — NOT as the page-level header.
@@ -151,7 +151,7 @@ export declare function Nav({ surface, padding, rounded, border, shadow, spacing
151
151
  export interface BlockHeaderProps extends React.HTMLAttributes<HTMLElement>, SurfaceProps {
152
152
  children: React.ReactNode;
153
153
  }
154
- export declare function BlockHeader({ className, surface, padding, rounded, border, shadow, children, ...props }: BlockHeaderProps): React.JSX.Element;
154
+ export declare function BlockHeader({ className, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, children, ...props }: BlockHeaderProps): React.JSX.Element;
155
155
  /**
156
156
  * Semantic `<footer>` element for block-level component footers.
157
157
  * Use inside `Section`, `Article`, or card bodies — NOT as the page-level footer.
@@ -159,7 +159,7 @@ export declare function BlockHeader({ className, surface, padding, rounded, bord
159
159
  export interface BlockFooterProps extends React.HTMLAttributes<HTMLElement>, SurfaceProps {
160
160
  children: React.ReactNode;
161
161
  }
162
- export declare function BlockFooter({ className, surface, padding, rounded, border, shadow, children, ...props }: BlockFooterProps): React.JSX.Element;
162
+ export declare function BlockFooter({ className, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, children, ...props }: BlockFooterProps): React.JSX.Element;
163
163
  /**
164
164
  * Semantic `<ul>` (unordered list) element.
165
165
  *
@@ -266,7 +266,7 @@ export interface TableProps extends Omit<React.TableHTMLAttributes<HTMLTableElem
266
266
  stickyHeader?: boolean;
267
267
  children: React.ReactNode;
268
268
  }
269
- export declare function Table({ variant, size, stickyHeader, className, surface, padding, rounded, border, shadow, children, ...props }: TableProps): React.JSX.Element;
269
+ export declare function Table({ variant, size, stickyHeader, className, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, children, ...props }: TableProps): React.JSX.Element;
270
270
  type TheadSurface = "none" | "default" | "muted" | "subtle";
271
271
  export interface TheadProps extends React.HTMLAttributes<HTMLTableSectionElement> {
272
272
  /** Background tone for the header row. */
@@ -347,16 +347,16 @@ export declare function Code({ color, weight, size, padding, rounded, surface, c
347
347
  export interface PreProps extends React.HTMLAttributes<HTMLPreElement>, SurfaceProps {
348
348
  children: React.ReactNode;
349
349
  }
350
- export declare function Pre({ className, surface, padding, rounded, border, shadow, children, ...props }: PreProps): React.JSX.Element;
350
+ export declare function Pre({ className, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, children, ...props }: PreProps): React.JSX.Element;
351
351
  export interface BlockquoteProps extends React.BlockquoteHTMLAttributes<HTMLQuoteElement>, SurfaceProps {
352
352
  color?: "default" | "primary" | "info" | "warning";
353
353
  children: React.ReactNode;
354
354
  }
355
- export declare function Blockquote({ color, className, surface, padding, paddingX, paddingY, rounded, border, shadow, children, ...props }: BlockquoteProps): React.JSX.Element;
355
+ export declare function Blockquote({ color, className, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, children, ...props }: BlockquoteProps): React.JSX.Element;
356
356
  export interface FigureProps extends React.HTMLAttributes<HTMLElement>, SurfaceProps {
357
357
  children: React.ReactNode;
358
358
  }
359
- export declare function Figure({ className, surface, padding, rounded, border, shadow, children, ...props }: FigureProps): React.JSX.Element;
359
+ export declare function Figure({ className, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, children, ...props }: FigureProps): React.JSX.Element;
360
360
  export interface FigcaptionProps extends React.HTMLAttributes<HTMLElement> {
361
361
  children: React.ReactNode;
362
362
  }
@@ -372,7 +372,7 @@ export interface DlProps extends React.HTMLAttributes<HTMLDListElement>, Surface
372
372
  divide?: boolean | "default" | "subtle";
373
373
  children: React.ReactNode;
374
374
  }
375
- export declare function Dl({ variant, divide, className, surface, padding, rounded, border, shadow, children, ...props }: DlProps): React.JSX.Element;
375
+ export declare function Dl({ variant, divide, className, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, children, ...props }: DlProps): React.JSX.Element;
376
376
  type DtDdColor = "default" | "primary" | "muted" | "faint";
377
377
  type DtDdWeight = "normal" | "medium" | "semibold" | "bold";
378
378
  export interface DtProps extends React.HTMLAttributes<HTMLElement> {