@mohasinac/appkit 3.5.3 → 3.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/features/seller/components/CategoryInlineSelect.d.ts +1 -1
- package/dist/features/seller/components/CategoryInlineSelect.js +1 -1
- package/dist/react/hooks/useBulkSelection.js +12 -3
- package/dist/react/hooks/useGesture.js +48 -4
- package/dist/react/hooks/useLongPress.d.ts +1 -0
- package/dist/react/hooks/useLongPress.js +5 -0
- package/dist/react/hooks/usePullToRefresh.js +12 -1
- package/dist/react/hooks/useRealtimeEvent.js +18 -1
- package/dist/react/hooks/useSwipe.js +14 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/BaseListingCard.d.ts +2 -1
- package/dist/ui/components/BaseListingCard.js +4 -5
- package/dist/ui/components/Button.js +52 -22
- package/dist/ui/components/DateInput.js +1 -0
- package/dist/ui/components/DynamicBgDiv.js +8 -6
- package/dist/ui/components/FormField.js +1 -1
- package/dist/ui/components/HorizontalScroller.js +17 -6
- package/dist/ui/components/Iframe.d.ts +8 -1
- package/dist/ui/components/Layout.d.ts +4 -4
- package/dist/ui/components/Layout.js +8 -8
- package/dist/ui/components/Motion.js +4 -1
- package/dist/ui/components/OtpInput.js +1 -1
- package/dist/ui/components/PaginatedSelect.js +29 -3
- package/dist/ui/components/Pagination.js +10 -5
- package/dist/ui/components/RichTextEditor.js +68 -3
- package/dist/ui/components/Select.js +1 -0
- package/dist/ui/components/Semantic.d.ts +10 -10
- package/dist/ui/components/Semantic.js +25 -25
- package/dist/ui/components/SideDrawer.js +13 -11
- package/dist/ui/components/SideModal.js +9 -7
- package/dist/ui/components/SlottedListingView.d.ts +13 -2
- package/dist/ui/components/StickyToolbar.js +9 -5
- package/dist/ui/components/TagInput.js +15 -2
- package/dist/ui/components/Textarea.js +1 -0
- package/dist/ui/components/Toggle.js +8 -1
- package/dist/ui/components/UnsavedChangesModal.js +12 -1
- package/dist/ui/forms/ColorPickerField.js +7 -0
- package/dist/ui/forms/FieldCheckbox.js +1 -1
- package/dist/utils/id-generators.d.ts +7 -4
- package/dist/utils/id-generators.js +53 -31
- package/dist/utils/number.formatter.js +19 -6
- package/dist/utils/string.formatter.js +4 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
23
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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 && (
|
|
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(() =>
|
|
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,
|
|
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
|
|
274
|
-
* (e.g. "catalogue-image") by matching against the same
|
|
275
|
-
* {@link validateMediaFilename} uses
|
|
276
|
-
*
|
|
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
|
*/
|
|
@@ -74,7 +74,8 @@ export function generateOrderId(input) {
|
|
|
74
74
|
export function generateFAQId(input) {
|
|
75
75
|
if (input.customId?.trim())
|
|
76
76
|
return input.customId.trim();
|
|
77
|
-
|
|
77
|
+
const questionSlug = slugify(input.question).substring(0, 50).replace(/-+$/, "");
|
|
78
|
+
return `faq-${slugify(input.category)}-${questionSlug}`;
|
|
78
79
|
}
|
|
79
80
|
// ─── Coupon ───────────────────────────────────────────────────────────────────
|
|
80
81
|
export function generateCouponId(code, customId) {
|
|
@@ -427,53 +428,74 @@ export function generateMediaFilename(ctx) {
|
|
|
427
428
|
}
|
|
428
429
|
}
|
|
429
430
|
/**
|
|
430
|
-
* W1-51 —
|
|
431
|
-
*
|
|
431
|
+
* W1-51 (regex fixed 2026-08-15 — see bug notes below) — validate that a
|
|
432
|
+
* filename matches the canonical pattern produced by
|
|
433
|
+
* {@link generateMediaFilename}. Called from /api/media/sign before a
|
|
432
434
|
* signed-URL is issued so malformed names can never enter storage.
|
|
433
435
|
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
+
* The generator functions above put the type word (image/video/logo/
|
|
437
|
+
* banner/avatar) at the END of the slug portion, e.g.
|
|
438
|
+
* `product-{name}-{category}-{store}-image-{n}.ext`, NOT as a literal
|
|
439
|
+
* compound prefix like `product-image-{...}`. The previous version of this
|
|
440
|
+
* validator anchored on the compound-prefix shape and rejected real output
|
|
441
|
+
* from every generator except invoice/payout-doc/shipping-proof/
|
|
442
|
+
* refund-proof/catalogue-image — causing /api/media/sign to 500 for
|
|
443
|
+
* product/review/auction/preorder/store/brand/blog/event/category/user/
|
|
444
|
+
* carousel/rich-text uploads. Each pattern below mirrors one generator
|
|
445
|
+
* function's actual output shape. blog-cover/blog-content-image/
|
|
446
|
+
* blog-additional-image all dispatch to the same generator as blog-image
|
|
447
|
+
* (and the four event-* variants likewise share one shape), so one pattern
|
|
448
|
+
* validates all sub-types of that family.
|
|
436
449
|
*
|
|
437
450
|
* Returns true when the filename is acceptable.
|
|
438
451
|
*/
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
"
|
|
444
|
-
"
|
|
445
|
-
"
|
|
446
|
-
|
|
447
|
-
"
|
|
448
|
-
"
|
|
449
|
-
"
|
|
450
|
-
"
|
|
451
|
-
"
|
|
452
|
-
"
|
|
453
|
-
"
|
|
452
|
+
const SLUG_PART = "[a-z0-9]+(?:-[a-z0-9]+)*";
|
|
453
|
+
const EXT_PART = "[a-z0-9]+";
|
|
454
|
+
const DATE_PART = "\\d{8}";
|
|
455
|
+
const MEDIA_FILENAME_PATTERNS = [
|
|
456
|
+
{ context: "product-image", pattern: new RegExp(`^product-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
|
|
457
|
+
{ context: "product-video", pattern: new RegExp(`^product-${SLUG_PART}-video-\\d+\\.${EXT_PART}$`) },
|
|
458
|
+
{ context: "review-image", pattern: new RegExp(`^review-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
|
|
459
|
+
{ context: "review-video", pattern: new RegExp(`^review-${SLUG_PART}-video-1\\.${EXT_PART}$`) },
|
|
460
|
+
{ context: "auction-image", pattern: new RegExp(`^auction-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
|
|
461
|
+
{ context: "preorder-image", pattern: new RegExp(`^preorder-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
|
|
462
|
+
{ context: "store-logo", pattern: new RegExp(`^store-${SLUG_PART}-logo\\.${EXT_PART}$`) },
|
|
463
|
+
{ context: "store-banner", pattern: new RegExp(`^store-${SLUG_PART}-banner\\.${EXT_PART}$`) },
|
|
464
|
+
{ context: "brand-logo", pattern: new RegExp(`^brand-${SLUG_PART}-logo\\.${EXT_PART}$`) },
|
|
465
|
+
{ context: "brand-banner", pattern: new RegExp(`^brand-${SLUG_PART}-banner\\.${EXT_PART}$`) },
|
|
466
|
+
{ context: "blog-image", pattern: new RegExp(`^blog-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
|
|
467
|
+
{ context: "event-image", pattern: new RegExp(`^event-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
|
|
468
|
+
{ context: "rich-text-image", pattern: new RegExp(`^rich-text-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
|
|
469
|
+
{ context: "category-image", pattern: new RegExp(`^category-${SLUG_PART}-image\\.${EXT_PART}$`) },
|
|
470
|
+
{ context: "user-avatar", pattern: new RegExp(`^user-${SLUG_PART}-avatar\\.${EXT_PART}$`) },
|
|
471
|
+
{ context: "carousel-image", pattern: new RegExp(`^carousel-${SLUG_PART}-image\\.${EXT_PART}$`) },
|
|
472
|
+
{ context: "catalogue-image", pattern: new RegExp(`^catalogue-image-${SLUG_PART}-\\d+\\.${EXT_PART}$`) },
|
|
473
|
+
{ context: "invoice", pattern: new RegExp(`^invoice-${SLUG_PART}-${DATE_PART}\\.pdf$`) },
|
|
474
|
+
{ context: "payout-doc", pattern: new RegExp(`^payout-doc-${SLUG_PART}-${DATE_PART}\\.pdf$`) },
|
|
475
|
+
{ context: "shipping-proof", pattern: new RegExp(`^shipping-proof-${SLUG_PART}-${DATE_PART}\\.${EXT_PART}$`) },
|
|
476
|
+
{ context: "refund-proof", pattern: new RegExp(`^refund-proof-${SLUG_PART}-${SLUG_PART}-${DATE_PART}\\.${EXT_PART}$`) },
|
|
477
|
+
{ context: "payment-proof", pattern: new RegExp(`^payment-proof-${SLUG_PART}-${SLUG_PART}-${DATE_PART}\\.${EXT_PART}$`) },
|
|
454
478
|
];
|
|
455
|
-
const MEDIA_FILENAME_REGEX = new RegExp(`^(?:${MEDIA_FILENAME_PREFIXES.join("|")})-[a-z0-9]([a-z0-9-]*[a-z0-9])?(?:-\\d+)?\\.[a-z0-9]+$`);
|
|
456
479
|
export function validateMediaFilename(filename) {
|
|
457
480
|
if (!filename || filename.length > 256)
|
|
458
481
|
return false;
|
|
459
482
|
if (filename.includes("..") || filename.includes("/") || filename.includes("\\"))
|
|
460
483
|
return false;
|
|
461
|
-
return
|
|
484
|
+
return MEDIA_FILENAME_PATTERNS.some(({ pattern }) => pattern.test(filename));
|
|
462
485
|
}
|
|
463
486
|
/**
|
|
464
|
-
* Derives the `MediaFilenameContext["type"]` a filename was
|
|
465
|
-
* (e.g. "catalogue-image") by matching against the same
|
|
466
|
-
* {@link validateMediaFilename} uses
|
|
467
|
-
*
|
|
487
|
+
* Derives the `MediaFilenameContext["type"]` family a filename was
|
|
488
|
+
* generated under (e.g. "catalogue-image") by matching against the same
|
|
489
|
+
* shape patterns {@link validateMediaFilename} uses. Sibling context types
|
|
490
|
+
* that share one generator (e.g. blog-cover/blog-content-image/
|
|
491
|
+
* blog-additional-image all produce the same shape as blog-image) resolve
|
|
492
|
+
* to that shared family name — the dispatcher itself can't distinguish them
|
|
493
|
+
* from the filename alone, since it never encoded the sub-type into the name.
|
|
468
494
|
* Used at /api/media/finalize time to stamp `MediaAssetDocument.contextType`
|
|
469
495
|
* without changing the finalize request contract.
|
|
470
496
|
*/
|
|
471
497
|
export function deriveContextTypeFromFilename(filename) {
|
|
472
|
-
|
|
473
|
-
const matches = MEDIA_FILENAME_PREFIXES.filter((prefix) => stem.startsWith(`${prefix}-`) || stem === prefix);
|
|
474
|
-
if (matches.length === 0)
|
|
475
|
-
return undefined;
|
|
476
|
-
return matches.reduce((longest, candidate) => (candidate.length > longest.length ? candidate : longest));
|
|
498
|
+
return MEDIA_FILENAME_PATTERNS.find(({ pattern }) => pattern.test(filename))?.context;
|
|
477
499
|
}
|
|
478
500
|
/**
|
|
479
501
|
* Generates a scannable barcode identifier for a product listing.
|
|
@@ -40,10 +40,20 @@ export function formatFileSize(bytes) {
|
|
|
40
40
|
export function formatCompactNumber(num) {
|
|
41
41
|
if (num < 1000)
|
|
42
42
|
return num.toString();
|
|
43
|
-
if (num < 1000000)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
if (num < 1000000) {
|
|
44
|
+
const kValue = (num / 1000).toFixed(1);
|
|
45
|
+
// Rounding can push a value right up to the next unit's boundary
|
|
46
|
+
// (e.g. 999950 -> "1000.0K") — escalate instead of displaying "1000.0K".
|
|
47
|
+
if (kValue === "1000.0")
|
|
48
|
+
return `${(num / 1000000).toFixed(1)}M`;
|
|
49
|
+
return `${kValue}K`;
|
|
50
|
+
}
|
|
51
|
+
if (num < 1000000000) {
|
|
52
|
+
const mValue = (num / 1000000).toFixed(1);
|
|
53
|
+
if (mValue === "1000.0")
|
|
54
|
+
return `${(num / 1000000000).toFixed(1)}B`;
|
|
55
|
+
return `${mValue}M`;
|
|
56
|
+
}
|
|
47
57
|
return `${(num / 1000000000).toFixed(1)}B`;
|
|
48
58
|
}
|
|
49
59
|
export function formatDecimal(num, decimals = 2) {
|
|
@@ -73,7 +83,10 @@ export function parseFormattedNumber(str) {
|
|
|
73
83
|
}
|
|
74
84
|
else if (lastDotIndex > -1 && lastCommaIndex === -1) {
|
|
75
85
|
const afterDot = cleaned.substring(lastDotIndex + 1);
|
|
76
|
-
|
|
86
|
+
// A single separator followed by exactly 3 digits is the standard
|
|
87
|
+
// thousands-grouping width (e.g. "3.500" -> 3500), not a decimal — only
|
|
88
|
+
// 1-2 trailing digits are treated as a genuine decimal fraction.
|
|
89
|
+
const isThousands = dotCount > 1 || afterDot.length >= 3;
|
|
77
90
|
if (isThousands)
|
|
78
91
|
cleaned = cleaned.replace(/\./g, "");
|
|
79
92
|
decimalSeparator = ".";
|
|
@@ -81,7 +94,7 @@ export function parseFormattedNumber(str) {
|
|
|
81
94
|
}
|
|
82
95
|
else if (lastCommaIndex > -1 && lastDotIndex === -1) {
|
|
83
96
|
const afterComma = cleaned.substring(lastCommaIndex + 1);
|
|
84
|
-
const commaIsThousands = commaCount > 1 || afterComma.length
|
|
97
|
+
const commaIsThousands = commaCount > 1 || afterComma.length >= 3;
|
|
85
98
|
if (commaIsThousands) {
|
|
86
99
|
cleaned = cleaned.replace(/,/g, "");
|
|
87
100
|
decimalSeparator = ".";
|
|
@@ -76,7 +76,10 @@ function applyMark(text, mark) {
|
|
|
76
76
|
}
|
|
77
77
|
if (mark.type === "link") {
|
|
78
78
|
const rawHref = String(mark.attrs?.href ?? "#").trim();
|
|
79
|
-
|
|
79
|
+
// `\/(?!\/)` allows a single leading slash (same-site relative path) but
|
|
80
|
+
// rejects `//host/...` protocol-relative URLs, which browsers resolve to
|
|
81
|
+
// an arbitrary external origin despite "looking" like a local path.
|
|
82
|
+
const safe = /^(https?:\/\/|mailto:|\/(?!\/)|#)/i.test(rawHref) ? rawHref : "#";
|
|
80
83
|
const href = safe.replace(/[&"<>]/g, (c) => ({ "&": "&", '"': """, "<": "<", ">": ">" })[c] ?? c);
|
|
81
84
|
return `<a href="${href}" rel="noopener noreferrer">${text}</a>`;
|
|
82
85
|
}
|