@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
@@ -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> {
@@ -59,9 +59,9 @@ export function sectionBackgroundStyle(bg) {
59
59
  return {};
60
60
  }
61
61
  }
62
- export const Section = React.forwardRef(({ tone = "plain", background, color, layout, gap, align, justify, className = "", surface, padding, paddingX, paddingY, rounded, border, shadow, children, ...props }, ref) => (_jsxs("section", { className: [
62
+ export const Section = React.forwardRef(({ tone = "plain", background, color, layout, gap, align, justify, className = "", surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, children, ...props }, ref) => (_jsxs("section", { className: [
63
63
  SECTION_TONE_MAP[tone],
64
- buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, border, shadow }),
64
+ buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow }),
65
65
  color ? SEMANTIC_COLOR_MAP[color] : "",
66
66
  layout ? SECTION_LAYOUT_MAP[layout] : "",
67
67
  gap ? SECTION_GAP_MAP[gap] : "",
@@ -73,13 +73,13 @@ export const Section = React.forwardRef(({ tone = "plain", background, color, la
73
73
  .filter(Boolean)
74
74
  .join(" "), ref: ref, ...props, children: [background?.value && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, className: "absolute inset-0 -z-10", style: sectionBackgroundStyle(background) }), background.overlay?.enabled && (_jsx("span", { "aria-hidden": true, className: "absolute inset-0 -z-10", style: { backgroundColor: background.overlay.color, opacity: background.overlay.opacity } }))] })), children] })));
75
75
  Section.displayName = "Section";
76
- export function Article({ className = "", surface, padding, rounded, border, shadow, color, children, ...props }) {
77
- return (_jsx("article", { className: [buildSurfaceClasses({ surface, padding, rounded, border, shadow }), color ? SEMANTIC_COLOR_MAP[color] : "", className].filter(Boolean).join(" "), ...props, children: children }));
76
+ export function Article({ className = "", surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, color, children, ...props }) {
77
+ return (_jsx("article", { className: [buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow }), color ? SEMANTIC_COLOR_MAP[color] : "", className].filter(Boolean).join(" "), ...props, children: children }));
78
78
  }
79
- export function Main({ className = "", surface, padding, paddingX, paddingY, rounded, border, shadow, color, children, ...props }) {
80
- return (_jsx("main", { className: [buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, border, shadow }), color ? SEMANTIC_COLOR_MAP[color] : "", className].filter(Boolean).join(" "), ...props, children: children }));
79
+ export function Main({ className = "", surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, color, children, ...props }) {
80
+ return (_jsx("main", { className: [buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow }), color ? SEMANTIC_COLOR_MAP[color] : "", className].filter(Boolean).join(" "), ...props, children: children }));
81
81
  }
82
- export const Aside = React.forwardRef(({ className = "", surface, padding, rounded, border, shadow, color, children, ...props }, ref) => (_jsx("aside", { className: [buildSurfaceClasses({ surface, padding, rounded, border, shadow }), color ? SEMANTIC_COLOR_MAP[color] : "", className].filter(Boolean).join(" "), ref: ref, ...props, children: children })));
82
+ export const Aside = React.forwardRef(({ className = "", surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, color, children, ...props }, ref) => (_jsx("aside", { className: [buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow }), color ? SEMANTIC_COLOR_MAP[color] : "", className].filter(Boolean).join(" "), ref: ref, ...props, children: children })));
83
83
  Aside.displayName = "Aside";
84
84
  const NAV_TEXT_SIZE_MAP = {
85
85
  xs: "text-xs",
@@ -97,8 +97,8 @@ const NAV_SPACING_MAP = {
97
97
  };
98
98
  const NAV_GAP_MAP = {
99
99
  none: "",
100
- "2xs": "gap-1.5",
101
- xs: "gap-1",
100
+ "2xs": "gap-1",
101
+ xs: "gap-1.5",
102
102
  sm: "gap-2",
103
103
  md: "gap-3",
104
104
  lg: "gap-4",
@@ -114,9 +114,9 @@ const NAV_COLOR_MAP = {
114
114
  muted: "appkit-color--muted",
115
115
  faint: "appkit-color--faint",
116
116
  };
117
- export function Nav({ surface, padding, rounded, border, shadow, spacing, gap, layout, color, textSize, className = "", children, ...props }) {
117
+ export function Nav({ surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, spacing, gap, layout, color, textSize, className = "", children, ...props }) {
118
118
  return (_jsx("nav", { className: [
119
- buildSurfaceClasses({ surface, padding, rounded, border, shadow }),
119
+ buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow }),
120
120
  spacing ? NAV_SPACING_MAP[spacing] : "",
121
121
  gap ? NAV_GAP_MAP[gap] : "",
122
122
  layout ? NAV_LAYOUT_MAP[layout] : "",
@@ -125,11 +125,11 @@ export function Nav({ surface, padding, rounded, border, shadow, spacing, gap, l
125
125
  className,
126
126
  ].filter(Boolean).join(" "), ...props, children: children }));
127
127
  }
128
- export function BlockHeader({ className = "", surface, padding, rounded, border, shadow, children, ...props }) {
129
- return (_jsx("header", { className: [buildSurfaceClasses({ surface, padding, rounded, border, shadow }), className].filter(Boolean).join(" "), ...props, children: children }));
128
+ export function BlockHeader({ className = "", surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, children, ...props }) {
129
+ return (_jsx("header", { className: [buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow }), className].filter(Boolean).join(" "), ...props, children: children }));
130
130
  }
131
- export function BlockFooter({ className = "", surface, padding, rounded, border, shadow, children, ...props }) {
132
- return (_jsx("footer", { className: [buildSurfaceClasses({ surface, padding, rounded, border, shadow }), className].filter(Boolean).join(" "), ...props, children: children }));
131
+ export function BlockFooter({ className = "", surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, children, ...props }) {
132
+ return (_jsx("footer", { className: [buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow }), className].filter(Boolean).join(" "), ...props, children: children }));
133
133
  }
134
134
  const LIST_MARKER_MAP = {
135
135
  disc: "appkit-list--marker-disc",
@@ -263,8 +263,8 @@ const TABLE_SIZE_MAP = {
263
263
  md: "appkit-table--md",
264
264
  lg: "appkit-table--lg",
265
265
  };
266
- export function Table({ variant = "default", size = "md", stickyHeader = false, className = "", surface, padding, rounded, border, shadow, children, ...props }) {
267
- return (_jsx("table", { className: ["appkit-table", TABLE_VARIANT_MAP[variant], TABLE_SIZE_MAP[size], stickyHeader ? "appkit-table--sticky-header" : "", buildSurfaceClasses({ surface, padding, rounded, border, shadow }), className].filter(Boolean).join(" "), ...props, children: children }));
266
+ export function Table({ variant = "default", size = "md", stickyHeader = false, className = "", surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, children, ...props }) {
267
+ return (_jsx("table", { className: ["appkit-table", TABLE_VARIANT_MAP[variant], TABLE_SIZE_MAP[size], stickyHeader ? "appkit-table--sticky-header" : "", buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow }), className].filter(Boolean).join(" "), ...props, children: children }));
268
268
  }
269
269
  const THEAD_SURFACE_MAP = {
270
270
  none: "",
@@ -444,8 +444,8 @@ export function Code({ color = "default", weight, size, padding, rounded, surfac
444
444
  className,
445
445
  ].filter(Boolean).join(" "), ...props, children: children }));
446
446
  }
447
- export function Pre({ className = "", surface, padding, rounded, border, shadow, children, ...props }) {
448
- return (_jsx("pre", { className: ["appkit-pre", buildSurfaceClasses({ surface, padding, rounded, border, shadow }), className].filter(Boolean).join(" "), ...props, children: children }));
447
+ export function Pre({ className = "", surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, children, ...props }) {
448
+ return (_jsx("pre", { className: ["appkit-pre", buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow }), className].filter(Boolean).join(" "), ...props, children: children }));
449
449
  }
450
450
  const BLOCKQUOTE_COLOR_MAP = {
451
451
  default: "appkit-blockquote--default",
@@ -453,22 +453,22 @@ const BLOCKQUOTE_COLOR_MAP = {
453
453
  info: "appkit-blockquote--info",
454
454
  warning: "appkit-blockquote--warning",
455
455
  };
456
- export function Blockquote({ color = "default", className = "", surface, padding, paddingX, paddingY, rounded, border, shadow, children, ...props }) {
457
- return (_jsx("blockquote", { className: ["appkit-blockquote", BLOCKQUOTE_COLOR_MAP[color], buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, border, shadow }), className].filter(Boolean).join(" "), ...props, children: children }));
456
+ export function Blockquote({ color = "default", className = "", surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, children, ...props }) {
457
+ return (_jsx("blockquote", { className: ["appkit-blockquote", BLOCKQUOTE_COLOR_MAP[color], buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow }), className].filter(Boolean).join(" "), ...props, children: children }));
458
458
  }
459
- export function Figure({ className = "", surface, padding, rounded, border, shadow, children, ...props }) {
460
- return (_jsx("figure", { className: ["appkit-figure", buildSurfaceClasses({ surface, padding, rounded, border, shadow }), className].filter(Boolean).join(" "), ...props, children: children }));
459
+ export function Figure({ className = "", surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, children, ...props }) {
460
+ return (_jsx("figure", { className: ["appkit-figure", buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow }), className].filter(Boolean).join(" "), ...props, children: children }));
461
461
  }
462
462
  export function Figcaption({ className = "", children, ...props }) {
463
463
  return (_jsx("figcaption", { className: ["appkit-figcaption", className].filter(Boolean).join(" "), ...props, children: children }));
464
464
  }
465
- export function Dl({ variant = "stacked", divide, className = "", surface, padding, rounded, border, shadow, children, ...props }) {
465
+ export function Dl({ variant = "stacked", divide, className = "", surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, children, ...props }) {
466
466
  const divideCls = divide
467
467
  ? divide === "subtle"
468
468
  ? "appkit-stack--divide-subtle"
469
469
  : "appkit-stack--divide"
470
470
  : "";
471
- return (_jsx("dl", { className: ["appkit-dl", variant === "inline" ? "appkit-dl--inline" : "appkit-dl--stacked", divideCls, buildSurfaceClasses({ surface, padding, rounded, border, shadow }), className].filter(Boolean).join(" "), ...props, children: children }));
471
+ return (_jsx("dl", { className: ["appkit-dl", variant === "inline" ? "appkit-dl--inline" : "appkit-dl--stacked", divideCls, buildSurfaceClasses({ surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow }), className].filter(Boolean).join(" "), ...props, children: children }));
472
472
  }
473
473
  const DT_DD_COLOR_MAP = {
474
474
  default: "",
@@ -124,18 +124,20 @@ export function SideDrawer({ isOpen, onClose, title, children, footer, mode = "v
124
124
  }, [isOpen]);
125
125
  // Prevent body scroll when drawer is open
126
126
  useEffect(() => {
127
- if (isOpen) {
128
- const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
129
- document.body.style.overflow = "hidden";
130
- document.body.style.paddingRight = `${scrollbarWidth}px`;
131
- }
132
- else {
133
- document.body.style.overflow = "";
134
- document.body.style.paddingRight = "";
135
- }
127
+ if (!isOpen)
128
+ return;
129
+ // Capture and restore the PRIOR values rather than hard-coding ""
130
+ // otherwise closing this drawer while another overlay (Modal,
131
+ // SideModal, a second SideDrawer) is still open underneath wipes out
132
+ // that overlay's own scroll lock too.
133
+ const previousOverflow = document.body.style.overflow;
134
+ const previousPaddingRight = document.body.style.paddingRight;
135
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
136
+ document.body.style.overflow = "hidden";
137
+ document.body.style.paddingRight = `${scrollbarWidth}px`;
136
138
  return () => {
137
- document.body.style.overflow = "";
138
- document.body.style.paddingRight = "";
139
+ document.body.style.overflow = previousOverflow;
140
+ document.body.style.paddingRight = previousPaddingRight;
139
141
  };
140
142
  }, [isOpen]);
141
143
  useSwipe(drawerRef, {
@@ -40,14 +40,16 @@ export function SideModal({ isOpen, onClose, title, side = "right", children, cl
40
40
  }, [isOpen]);
41
41
  // Prevent body scroll while open
42
42
  useEffect(() => {
43
- if (isOpen) {
44
- document.body.style.overflow = "hidden";
45
- }
46
- else {
47
- document.body.style.overflow = "";
48
- }
43
+ if (!isOpen)
44
+ return;
45
+ // Capture and restore the PRIOR value rather than hard-coding "" —
46
+ // otherwise closing this modal while another overlay is still open
47
+ // underneath (Modal, SideDrawer, a second SideModal) wipes out that
48
+ // overlay's own scroll lock too.
49
+ const previousOverflow = document.body.style.overflow;
50
+ document.body.style.overflow = "hidden";
49
51
  return () => {
50
- document.body.style.overflow = "";
52
+ document.body.style.overflow = previousOverflow;
51
53
  };
52
54
  }, [isOpen]);
53
55
  const reduced = useReducedMotion();
@@ -46,11 +46,22 @@ export interface SlottedListingViewProps {
46
46
  * - `public`: no managed state by default
47
47
  */
48
48
  portal?: ViewPortal;
49
- /** Enable managed search state (default driven by portal, otherwise false). */
49
+ /**
50
+ * Currently unused — `renderSearch` is always invoked when provided,
51
+ * regardless of this flag. Kept as a no-op accepted prop because real
52
+ * consumers (CategoryProductsView, StoreProductsView, FAQPageContent, …)
53
+ * already pass their own self-contained `renderSearch` closures that
54
+ * manage search state externally and would lose their search UI if this
55
+ * ever started gating the render call. Do not wire this up to gate
56
+ * `renderSearch` without auditing every consumer's `renderSearch` first.
57
+ */
50
58
  manageSearch?: boolean;
51
59
  /** Enable managed selection state (default driven by portal, otherwise false). */
52
60
  manageSelection?: boolean;
53
- /** Enable managed sort state (default empty string). */
61
+ /**
62
+ * Currently unused — see `manageSearch` above; `renderSort` is always
63
+ * invoked when provided, regardless of this flag, for the same reason.
64
+ */
54
65
  manageSort?: boolean;
55
66
  /** Wrap search + sort slots in a flex row. */
56
67
  inlineToolbar?: boolean;
@@ -12,21 +12,25 @@ const PADDING_CLS = {
12
12
  lg: "px-4 py-3",
13
13
  toolbar: "px-3 py-1.5",
14
14
  };
15
- function resolveOffset(offset) {
15
+ function resolveOffsetClass(offset) {
16
16
  if (offset === "header") {
17
17
  return "top-[var(--header-height,0px)]";
18
18
  }
19
19
  if (offset === "header+nav") {
20
20
  return "top-[calc(var(--header-height,0px)+var(--appkit-navbar-height,2.5rem))]";
21
21
  }
22
- // Numeric pixel offset.
23
- return `top-[${offset}px]`;
22
+ // Numeric offsets are applied via inline style instead (see below) —
23
+ // Tailwind's static scanner can never see a dynamically-interpolated
24
+ // `top-[${offset}px]` arbitrary value, so no CSS rule would ever be
25
+ // generated for it in the compiled stylesheet.
26
+ return "";
24
27
  }
25
28
  export function StickyToolbar({ children, offset = "header", tone = "translucent", border = true, padding = "toolbar", z = "above-content", role, }) {
26
- const offsetCls = resolveOffset(offset);
29
+ const offsetCls = resolveOffsetClass(offset);
30
+ const offsetStyle = typeof offset === "number" ? { top: `${offset}px` } : undefined;
27
31
  const borderCls = border ? "border-b border-[var(--appkit-color-border)]" : "";
28
32
  const zCls = z === "above-content" ? "z-10" : "z-[5]";
29
- return (_jsx("div", { role: role,
33
+ return (_jsx("div", { role: role, style: offsetStyle,
30
34
  // for the translucent sticky-toolbar pattern. The header offset is
31
35
  // sourced from --header-height (set by AppLayoutShell at runtime).
32
36
  className: `sticky ${offsetCls} ${zCls} ${TONE_CLS[tone]} ${borderCls} ${PADDING_CLS[padding]}`, children: children }));
@@ -5,8 +5,9 @@ import { Label, Span } from "./Typography";
5
5
  export function TagInput({ value, onChange, disabled = false, label, placeholder = "Add a tag...", className = "", helperText = "Press Enter or comma to add a tag", }) {
6
6
  const [draft, setDraft] = useState("");
7
7
  const inputRef = useRef(null);
8
+ const normalizeTag = (raw) => raw.trim().replace(/,+$/, "").trim();
8
9
  const addTag = (raw) => {
9
- const tag = raw.trim().replace(/,+$/, "").trim();
10
+ const tag = normalizeTag(raw);
10
11
  if (!tag || value.includes(tag)) {
11
12
  setDraft("");
12
13
  return;
@@ -32,7 +33,19 @@ export function TagInput({ value, onChange, disabled = false, label, placeholder
32
33
  if (inputValue.includes(",")) {
33
34
  const parts = inputValue.split(",");
34
35
  const last = parts.pop() ?? "";
35
- parts.forEach((part) => addTag(part));
36
+ // Accumulate locally instead of calling addTag() in a loop — addTag
37
+ // always computes `[...value, tag]` off the current render's `value`
38
+ // prop, so multiple synchronous calls in the same paste each started
39
+ // from the SAME stale array and every onChange but the last one got
40
+ // silently overwritten (only the final comma-segment ever survived).
41
+ let next = value;
42
+ for (const part of parts) {
43
+ const tag = normalizeTag(part);
44
+ if (tag && !next.includes(tag))
45
+ next = [...next, tag];
46
+ }
47
+ if (next !== value)
48
+ onChange(next);
36
49
  setDraft(last);
37
50
  return;
38
51
  }
@@ -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, Text } from "./Typography";
@@ -43,5 +43,12 @@ export function Toggle({ checked: checkedProp, defaultChecked = false, onChange,
43
43
  .join(" "), children: _jsx(Span, { "aria-hidden": "true", className: [
44
44
  UI_TOGGLE.thumb,
45
45
  checked ? UI_TOGGLE.thumbOn : UI_TOGGLE.thumbOff,
46
- ].join(" ") }) }), label && (_jsx(Label, { id: `${toggleId}-label`, htmlFor: toggleId, className: disabled ? "cursor-not-allowed opacity-60" : "cursor-pointer", onClick: disabled ? undefined : handleToggle, children: label }))] }));
46
+ ].join(" ") }) }), label && (
47
+ // No onClick here — `htmlFor` on a `<label>` already makes the
48
+ // browser forward a click on the label to the labelable `<button>`
49
+ // above natively. Also attaching handleToggle here fired it TWICE
50
+ // per click (once from this handler, once from the native
51
+ // label→button forwarding), both reading the same stale `checked`
52
+ // closure, so onChange(true) could fire twice for one click.
53
+ _jsx(Label, { id: `${toggleId}-label`, htmlFor: toggleId, className: disabled ? "cursor-not-allowed opacity-60" : "cursor-pointer", children: label }))] }));
47
54
  }
@@ -18,7 +18,18 @@ export function UnsavedChangesModal({ labels = {} }) {
18
18
  useEffect(() => {
19
19
  const subscription = eventBus.on(UNSAVED_CHANGES_EVENT, (...args) => {
20
20
  const resolveFn = args[0];
21
- setResolve(() => resolveFn);
21
+ setResolve((prevResolve) => {
22
+ // A second UNSAVED_CHANGES_EVENT arrived before the user answered
23
+ // the first — resolve that abandoned promise with `false` instead
24
+ // of silently overwriting it and leaving it pending forever.
25
+ prevResolve?.(false);
26
+ // Returning `resolveFn` directly (not `() => resolveFn`) is
27
+ // correct here — the double-wrap trick is only needed when
28
+ // passing setState a bare function *value* (to stop React from
29
+ // treating it as an updater); inside an updater callback the
30
+ // return value already becomes the next state as-is.
31
+ return resolveFn;
32
+ });
22
33
  setIsOpen(true);
23
34
  });
24
35
  return () => subscription.unsubscribe();
@@ -31,6 +31,13 @@ export function ColorPickerField({ name, label, hint, required, swatchWidth = "f
31
31
  }
32
32
  function handleChange(e) {
33
33
  onChange?.(e.target.value);
34
+ // FieldInput/FieldTextarea/FieldSelect/FieldCheckbox all clear the
35
+ // FormShell error for this field on change — this was the only Field*
36
+ // wrapper that omitted it, so a picked (valid) color never cleared a
37
+ // previously-set error until something else called clearErrors/
38
+ // setFieldError.
39
+ if (showError)
40
+ ctx?.clearFieldError(name);
34
41
  }
35
42
  return (_jsxs(Span, { layout: "flex", gap: "sm", className: "flex-col", children: [label != null && (_jsx(Label, { htmlFor: inputId, required: required, children: label })), _jsx("input", { id: inputId, type: "color", name: name, className: `h-10 ${SWATCH_WIDTH_MAP[swatchWidth]} rounded border border-[var(--appkit-color-border)] cursor-pointer ${className ?? ""}`, "aria-invalid": showError ? true : undefined, "aria-describedby": showError ? errorId : undefined, onBlur: handleBlur, onChange: handleChange, ...inputProps }), hint != null && !showError && (_jsx(Text, { size: "xs", color: "muted", children: hint })), showError && (_jsx(Text, { id: errorId, role: "alert", size: "xs", color: "error", children: error }))] }));
36
43
  }
@@ -21,5 +21,5 @@ export function FieldCheckbox({ name, label, hint, checked, onChange, onBlur, di
21
21
  ctx?.setFieldTouched(name);
22
22
  onBlur?.();
23
23
  }
24
- return (_jsxs("div", { className: ["appkit-form-field", className].filter(Boolean).join(" "), children: [_jsx(Checkbox, { id: inputId, name: name, checked: checked, onChange: handleChange, onBlur: handleBlur, label: label, disabled: disabled, error: showError ? error : undefined, "aria-describedby": showError ? errorId : undefined }), !showError && hint && (_jsx(Text, { size: "sm", variant: "secondary", className: "appkit-form-field__hint", children: hint })), showError && (_jsx(Text, { id: errorId, size: "sm", variant: "error", className: "appkit-form-field__error", role: "alert", children: error }))] }));
24
+ return (_jsxs("div", { className: ["appkit-form-field", className].filter(Boolean).join(" "), children: [_jsx(Checkbox, { id: inputId, name: name, checked: checked, onChange: handleChange, onBlur: handleBlur, label: label, disabled: disabled, "aria-describedby": showError ? errorId : undefined }), !showError && hint && (_jsx(Text, { size: "sm", variant: "secondary", className: "appkit-form-field__hint", children: hint })), showError && (_jsx(Text, { id: errorId, size: "sm", variant: "error", className: "appkit-form-field__error", role: "alert", children: error }))] }));
25
25
  }
@@ -270,10 +270,13 @@ export type MediaFilenameContext = ({
270
270
  export declare function generateMediaFilename(ctx: MediaFilenameContext): string;
271
271
  export declare function validateMediaFilename(filename: string): boolean;
272
272
  /**
273
- * Derives the `MediaFilenameContext["type"]` a filename was generated under
274
- * (e.g. "catalogue-image") by matching against the same prefix list
275
- * {@link validateMediaFilename} uses the longest matching prefix wins so
276
- * "blog-content-image" isn't mis-derived as "blog-cover"'s sibling "blog-".
273
+ * Derives the `MediaFilenameContext["type"]` family a filename was
274
+ * generated under (e.g. "catalogue-image") by matching against the same
275
+ * shape patterns {@link validateMediaFilename} uses. Sibling context types
276
+ * that share one generator (e.g. blog-cover/blog-content-image/
277
+ * blog-additional-image all produce the same shape as blog-image) resolve
278
+ * to that shared family name — the dispatcher itself can't distinguish them
279
+ * from the filename alone, since it never encoded the sub-type into the name.
277
280
  * Used at /api/media/finalize time to stamp `MediaAssetDocument.contextType`
278
281
  * without changing the finalize request contract.
279
282
  */