@mohasinac/appkit 2.7.36 → 2.7.37
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/_internal/client/features/layout/DashboardLayoutClient.js +1 -1
- package/dist/_internal/client/features/seller/print-center/InventoryLabel.d.ts +12 -0
- package/dist/_internal/client/features/seller/print-center/InventoryLabel.js +49 -0
- package/dist/_internal/client/features/seller/print-center/LabelDesignPicker.d.ts +7 -0
- package/dist/_internal/client/features/seller/print-center/LabelDesignPicker.js +32 -0
- package/dist/_internal/client/features/seller/print-center/OrderPackingLabel.d.ts +28 -0
- package/dist/_internal/client/features/seller/print-center/OrderPackingLabel.js +33 -0
- package/dist/_internal/client/features/seller/print-center/PhysicalLocationModal.d.ts +12 -0
- package/dist/_internal/client/features/seller/print-center/PhysicalLocationModal.js +30 -0
- package/dist/_internal/client/features/seller/print-center/PrintCenterView.d.ts +12 -0
- package/dist/_internal/client/features/seller/print-center/PrintCenterView.js +102 -0
- package/dist/_internal/client/features/seller/print-center/PrintGrid.d.ts +12 -0
- package/dist/_internal/client/features/seller/print-center/PrintGrid.js +13 -0
- package/dist/_internal/client/features/seller/print-center/StoreCard.d.ts +19 -0
- package/dist/_internal/client/features/seller/print-center/StoreCard.js +30 -0
- package/dist/_internal/client/features/seller/print-center/WebsiteCard.d.ts +11 -0
- package/dist/_internal/client/features/seller/print-center/WebsiteCard.js +25 -0
- package/dist/_internal/client/features/seller/print-center/index.d.ts +11 -0
- package/dist/_internal/client/features/seller/print-center/index.js +10 -0
- package/dist/_internal/client/features/seller/print-center/types.d.ts +20 -0
- package/dist/_internal/client/features/seller/print-center/types.js +20 -0
- package/dist/_internal/client/features/seller/print-center/useInventoryPdf.d.ts +18 -0
- package/dist/_internal/client/features/seller/print-center/useInventoryPdf.js +76 -0
- package/dist/_internal/server/jobs/core/draftPrune.d.ts +2 -0
- package/dist/_internal/server/jobs/core/draftPrune.js +15 -0
- package/dist/_internal/server/jobs/core/onBidPlaced.js +1 -1
- package/dist/_internal/server/jobs/core/promotions.d.ts +1 -0
- package/dist/_internal/server/jobs/core/promotions.js +21 -6
- package/dist/_internal/server/jobs/handlers/assignSpinPrize.js +18 -1
- package/dist/_internal/server/jobs/handlers/draftPrune.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/draftPrune.js +2 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +2 -0
- package/dist/_internal/server/jobs/handlers/triggerEventRaffle.js +15 -1
- package/dist/_internal/shared/actions/action-registry.js +32 -0
- package/dist/_internal/shared/features/blog/schema.d.ts +8 -8
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +10 -10
- package/dist/_internal/shared/features/orders/schema.d.ts +8 -8
- package/dist/_internal/shared/features/products/schema.d.ts +8 -8
- package/dist/_internal/shared/features/promotions/schema.d.ts +6 -6
- package/dist/_internal/shared/features/reviews/schema.d.ts +2 -2
- package/dist/client.d.ts +11 -1
- package/dist/client.js +6 -1
- package/dist/configs/next.js +1 -0
- package/dist/constants/api-endpoints.d.ts +6 -0
- package/dist/constants/api-endpoints.js +2 -0
- package/dist/core/server-action.d.ts +4 -0
- package/dist/errors/error-codes.d.ts +18 -0
- package/dist/errors/error-codes.js +18 -0
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/errors/messages.d.ts +1 -0
- package/dist/errors/messages.js +1 -0
- package/dist/features/account/components/UserSidebar.js +1 -1
- package/dist/features/account/schemas/index.d.ts +12 -12
- package/dist/features/admin/components/AdminSidebar.js +1 -1
- package/dist/features/auctions/actions/bid-actions.js +13 -6
- package/dist/features/auctions/components/PlaceBidFormClient.js +10 -2
- package/dist/features/auctions/repository/bid.repository.d.ts +7 -0
- package/dist/features/auctions/repository/bid.repository.js +16 -0
- package/dist/features/auctions/schemas/index.d.ts +18 -18
- package/dist/features/auth/schemas/index.d.ts +2 -2
- package/dist/features/before-after/schemas/index.d.ts +2 -2
- package/dist/features/blog/actions/blog-actions.d.ts +32 -32
- package/dist/features/blog/components/BlogPostForm.js +1 -1
- package/dist/features/blog/schemas/index.d.ts +38 -38
- package/dist/features/categories/schemas/index.d.ts +10 -10
- package/dist/features/collections/schemas/index.d.ts +4 -4
- package/dist/features/consultation/schemas/index.d.ts +9 -9
- package/dist/features/corporate/schemas/index.d.ts +4 -4
- package/dist/features/events/schemas/index.d.ts +50 -50
- package/dist/features/faq/actions/faq-actions.d.ts +6 -6
- package/dist/features/faq/schemas/index.d.ts +8 -8
- package/dist/features/homepage/actions/homepage-section-actions.d.ts +4 -4
- package/dist/features/layout/AppLayoutShell.js +1 -1
- package/dist/features/layout/AutoBreadcrumbs.js +1 -1
- package/dist/features/media/AvatarUpload.js +1 -1
- package/dist/features/media/types/index.d.ts +4 -4
- package/dist/features/media/upload/ImageUpload.js +18 -9
- package/dist/features/media/upload/MediaUploadList.js +2 -2
- package/dist/features/orders/schemas/firestore.d.ts +6 -0
- package/dist/features/orders/schemas/index.d.ts +10 -10
- package/dist/features/orders/types/index.d.ts +5 -0
- package/dist/features/payments/schemas/index.d.ts +4 -4
- package/dist/features/pre-orders/schemas/index.d.ts +8 -8
- package/dist/features/products/components/PrizeDrawItemsEditor.js +1 -1
- package/dist/features/products/components/ProductForm.js +20 -2
- package/dist/features/products/repository/products.repository.d.ts +3 -1
- package/dist/features/products/repository/products.repository.js +12 -1
- package/dist/features/products/schemas/firestore.d.ts +7 -0
- package/dist/features/products/schemas/index.d.ts +34 -34
- package/dist/features/products/schemas/product-features.validators.d.ts +6 -6
- package/dist/features/products/types/index.d.ts +6 -0
- package/dist/features/promotions/schemas/index.d.ts +14 -14
- package/dist/features/reviews/schemas/index.d.ts +18 -18
- package/dist/features/seller/actions/offer-actions.js +10 -1
- package/dist/features/seller/components/PhysicalLocationModal.d.ts +12 -0
- package/dist/features/seller/components/PhysicalLocationModal.js +30 -0
- package/dist/features/seller/components/PrintCenterView.d.ts +40 -0
- package/dist/features/seller/components/PrintCenterView.js +6 -0
- package/dist/features/seller/components/SellerOrdersView.js +48 -3
- package/dist/features/seller/components/SellerProductShell.d.ts +1 -0
- package/dist/features/seller/components/SellerProductShell.js +26 -4
- package/dist/features/seller/components/SellerProductsView.js +54 -13
- package/dist/features/seller/components/SellerSidebar.js +1 -1
- package/dist/features/seller/components/index.d.ts +1 -0
- package/dist/features/seller/components/index.js +1 -0
- package/dist/features/seller/schemas/firestore.d.ts +2 -0
- package/dist/features/seller/schemas/firestore.js +1 -0
- package/dist/features/seller/schemas/index.d.ts +22 -22
- package/dist/features/shell/FormShell.js +1 -1
- package/dist/features/shell/StepForm.d.ts +3 -1
- package/dist/features/shell/StepForm.js +3 -3
- package/dist/features/stores/schemas/index.d.ts +4 -4
- package/dist/index.d.ts +9 -1
- package/dist/index.js +6 -1
- package/dist/next/routing/route-map.d.ts +8 -0
- package/dist/next/routing/route-map.js +3 -0
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/RowActionMenu.js +45 -10
- package/dist/ui/components/SideDrawer.style.css +2 -2
- package/dist/ui/forms/FieldCheckbox.d.ts +13 -0
- package/dist/ui/forms/FieldCheckbox.js +25 -0
- package/dist/ui/forms/FieldInput.d.ts +11 -0
- package/dist/ui/forms/FieldInput.js +25 -0
- package/dist/ui/forms/FieldSelect.d.ts +17 -0
- package/dist/ui/forms/FieldSelect.js +25 -0
- package/dist/ui/forms/FormShell.d.ts +48 -0
- package/dist/ui/forms/FormShell.js +176 -0
- package/dist/ui/forms/FormShell.style.css +287 -0
- package/dist/ui/forms/index.d.ts +8 -0
- package/dist/ui/forms/index.js +4 -0
- package/dist/ui/index.d.ts +9 -0
- package/dist/ui/index.js +5 -0
- package/dist/utils/action-response.d.ts +6 -0
- package/dist/utils/action-response.js +22 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/validation/schemas.d.ts +10 -10
- package/package.json +2 -2
|
@@ -1,19 +1,54 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useRef, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
4
5
|
import { MoreHorizontal } from "lucide-react";
|
|
5
6
|
import { Button } from "./Button";
|
|
6
7
|
import { Span } from "./Typography";
|
|
7
|
-
import {
|
|
8
|
+
import { useKeyPress } from "../../react";
|
|
8
9
|
export function RowActionMenu({ actions, align = "right", }) {
|
|
9
10
|
const [open, setOpen] = useState(false);
|
|
10
|
-
const
|
|
11
|
-
|
|
11
|
+
const [pos, setPos] = useState(null);
|
|
12
|
+
const wrapperRef = useRef(null);
|
|
13
|
+
const dropdownRef = useRef(null);
|
|
14
|
+
const [mounted, setMounted] = useState(false);
|
|
15
|
+
useEffect(() => setMounted(true), []);
|
|
12
16
|
useKeyPress("Escape", () => setOpen(false), { enabled: open });
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// Close on outside click — exclude both trigger wrapper and portal dropdown
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!open)
|
|
20
|
+
return;
|
|
21
|
+
const handler = (e) => {
|
|
22
|
+
const t = e.target;
|
|
23
|
+
if (!wrapperRef.current?.contains(t) && !dropdownRef.current?.contains(t)) {
|
|
24
|
+
setOpen(false);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
document.addEventListener("mousedown", handler);
|
|
28
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
29
|
+
}, [open]);
|
|
30
|
+
const handleToggle = useCallback(() => {
|
|
31
|
+
if (!open && wrapperRef.current) {
|
|
32
|
+
const rect = wrapperRef.current.getBoundingClientRect();
|
|
33
|
+
const top = rect.bottom + 4;
|
|
34
|
+
setPos(align === "right"
|
|
35
|
+
? { top, right: window.innerWidth - rect.right }
|
|
36
|
+
: { top, left: rect.left });
|
|
37
|
+
}
|
|
38
|
+
setOpen((v) => !v);
|
|
39
|
+
}, [open, align]);
|
|
40
|
+
const dropdown = open && pos && mounted
|
|
41
|
+
? createPortal(_jsx("div", { ref: dropdownRef, role: "menu", className: "appkit-row-action-menu__dropdown", style: {
|
|
42
|
+
position: "fixed",
|
|
43
|
+
zIndex: "var(--appkit-z-modal)",
|
|
44
|
+
top: pos.top,
|
|
45
|
+
...(pos.right !== undefined ? { right: pos.right } : { left: pos.left }),
|
|
46
|
+
}, onClick: (e) => e.stopPropagation(), children: actions.map((action, idx) => (_jsxs("div", { children: [action.separator && idx > 0 ? (_jsx("div", { className: "appkit-row-action-menu__separator" })) : null, _jsxs(Button, { type: "button", role: "menuitem", variant: "ghost", disabled: action.disabled, onClick: () => {
|
|
47
|
+
if (action.disabled)
|
|
48
|
+
return;
|
|
49
|
+
action.onClick();
|
|
50
|
+
setOpen(false);
|
|
51
|
+
}, className: `appkit-row-action-menu__item ${action.destructive ? "appkit-row-action-menu__item--destructive" : ""} ${action.disabled ? "opacity-40 cursor-not-allowed" : ""}`, children: [action.icon ? (_jsx(Span, { className: "appkit-row-action-menu__icon", children: action.icon })) : null, action.label] })] }, `${action.label}-${idx}`))) }), document.body)
|
|
52
|
+
: null;
|
|
53
|
+
return (_jsxs("div", { ref: wrapperRef, className: "appkit-row-action-menu", onClick: (e) => e.stopPropagation(), children: [_jsx(Button, { type: "button", variant: "ghost", "aria-label": "Row actions", "aria-haspopup": "menu", "aria-expanded": open, onClick: handleToggle, className: "appkit-row-action-menu__trigger", children: _jsx(MoreHorizontal, { className: "h-4 w-4", strokeWidth: 1.5 }) }), dropdown] }));
|
|
19
54
|
}
|
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
.appkit-side-drawer__content {
|
|
159
159
|
flex: 1;
|
|
160
160
|
overflow-y: auto;
|
|
161
|
-
padding: 1rem;
|
|
161
|
+
padding: 1rem 1.25rem;
|
|
162
162
|
}
|
|
163
163
|
@media (min-width: 640px) {
|
|
164
164
|
.appkit-side-drawer__content { padding: 1.5rem; }
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
/* Footer */
|
|
168
168
|
.appkit-side-drawer__footer {
|
|
169
169
|
flex-shrink: 0;
|
|
170
|
-
padding: 1rem
|
|
170
|
+
padding: 1rem 1.25rem;
|
|
171
171
|
border-top: 1px solid var(--appkit-color-border);
|
|
172
172
|
background-color: var(--appkit-color-bg);
|
|
173
173
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface FieldCheckboxProps {
|
|
2
|
+
name: string;
|
|
3
|
+
label: string;
|
|
4
|
+
hint?: string;
|
|
5
|
+
checked?: boolean;
|
|
6
|
+
onChange?: (checked: boolean) => void;
|
|
7
|
+
onBlur?: () => void;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
/** Override error — falls back to FormShell context error for this field */
|
|
10
|
+
error?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function FieldCheckbox({ name, label, hint, checked, onChange, onBlur, disabled, error: errorProp, className, }: FieldCheckboxProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useContext } from "react";
|
|
4
|
+
import { FormShellContext } from "./FormShell";
|
|
5
|
+
import { Checkbox } from "../components/Checkbox";
|
|
6
|
+
import { Text } from "../components/Typography";
|
|
7
|
+
export function FieldCheckbox({ name, label, hint, checked, onChange, onBlur, disabled, error: errorProp, className, }) {
|
|
8
|
+
const ctx = useContext(FormShellContext);
|
|
9
|
+
const contextError = ctx?.errors[name];
|
|
10
|
+
const error = errorProp ?? contextError;
|
|
11
|
+
const touched = ctx?.touched[name];
|
|
12
|
+
const showError = !!error && (touched != null ? touched : true);
|
|
13
|
+
const inputId = `field-${name}`;
|
|
14
|
+
const errorId = `${inputId}-error`;
|
|
15
|
+
function handleChange(e) {
|
|
16
|
+
onChange?.(e.target.checked);
|
|
17
|
+
if (showError)
|
|
18
|
+
ctx?.clearFieldError(name);
|
|
19
|
+
}
|
|
20
|
+
function handleBlur() {
|
|
21
|
+
ctx?.setFieldTouched(name);
|
|
22
|
+
onBlur?.();
|
|
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 }))] }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface FieldInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange"> {
|
|
3
|
+
name: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
hint?: string;
|
|
6
|
+
required?: boolean;
|
|
7
|
+
onChange?: (value: string) => void;
|
|
8
|
+
/** Override error — falls back to FormShell context error for this field */
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function FieldInput({ name, label, hint, required, onChange, error: errorProp, onBlur, className, ...inputProps }: FieldInputProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useContext } from "react";
|
|
4
|
+
import { FormShellContext } from "./FormShell";
|
|
5
|
+
import { Input } from "../components/Input";
|
|
6
|
+
import { Label, Text, Span } from "../components/Typography";
|
|
7
|
+
export function FieldInput({ name, label, hint, required, onChange, error: errorProp, onBlur, className, ...inputProps }) {
|
|
8
|
+
const ctx = useContext(FormShellContext);
|
|
9
|
+
const contextError = ctx?.errors[name];
|
|
10
|
+
const error = errorProp ?? contextError;
|
|
11
|
+
const touched = ctx?.touched[name];
|
|
12
|
+
const showError = !!error && (touched != null ? touched : true);
|
|
13
|
+
const inputId = `field-${name}`;
|
|
14
|
+
const errorId = `${inputId}-error`;
|
|
15
|
+
function handleBlur(e) {
|
|
16
|
+
ctx?.setFieldTouched(name);
|
|
17
|
+
onBlur?.(e);
|
|
18
|
+
}
|
|
19
|
+
function handleChange(e) {
|
|
20
|
+
onChange?.(e.target.value);
|
|
21
|
+
if (showError)
|
|
22
|
+
ctx?.clearFieldError(name);
|
|
23
|
+
}
|
|
24
|
+
return (_jsxs("div", { className: ["appkit-form-field", className].filter(Boolean).join(" "), children: [label && (_jsxs(Label, { htmlFor: inputId, className: "appkit-form-field__label", children: [label, required && _jsx(Span, { className: "appkit-form-field__required", children: " *" })] })), _jsx(Input, { id: inputId, name: name, "aria-required": required || undefined, "aria-invalid": showError || undefined, "aria-describedby": showError ? errorId : undefined, onBlur: handleBlur, onChange: handleChange, ...inputProps }), !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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type SelectOption } from "../components/Select";
|
|
2
|
+
export interface FieldSelectProps {
|
|
3
|
+
name: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
hint?: string;
|
|
6
|
+
required?: boolean;
|
|
7
|
+
value?: string;
|
|
8
|
+
onChange?: (value: string) => void;
|
|
9
|
+
onBlur?: () => void;
|
|
10
|
+
options: SelectOption[];
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
/** Override error — falls back to FormShell context error for this field */
|
|
14
|
+
error?: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function FieldSelect({ name, label, hint, required, value, onChange, onBlur, options, disabled, placeholder, error: errorProp, className, }: FieldSelectProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useContext } from "react";
|
|
4
|
+
import { FormShellContext } from "./FormShell";
|
|
5
|
+
import { Select } from "../components/Select";
|
|
6
|
+
import { Label, Text, Span } from "../components/Typography";
|
|
7
|
+
export function FieldSelect({ name, label, hint, required, value, onChange, onBlur, options, disabled, placeholder, error: errorProp, className, }) {
|
|
8
|
+
const ctx = useContext(FormShellContext);
|
|
9
|
+
const contextError = ctx?.errors[name];
|
|
10
|
+
const error = errorProp ?? contextError;
|
|
11
|
+
const touched = ctx?.touched[name];
|
|
12
|
+
const showError = !!error && (touched != null ? touched : true);
|
|
13
|
+
const inputId = `field-${name}`;
|
|
14
|
+
const errorId = `${inputId}-error`;
|
|
15
|
+
function handleBlur() {
|
|
16
|
+
ctx?.setFieldTouched(name);
|
|
17
|
+
onBlur?.();
|
|
18
|
+
}
|
|
19
|
+
function handleChange(e) {
|
|
20
|
+
onChange?.(e.target.value);
|
|
21
|
+
if (showError)
|
|
22
|
+
ctx?.clearFieldError(name);
|
|
23
|
+
}
|
|
24
|
+
return (_jsxs("div", { className: ["appkit-form-field", className].filter(Boolean).join(" "), children: [label && (_jsxs(Label, { htmlFor: inputId, className: "appkit-form-field__label", children: [label, required && _jsx(Span, { className: "appkit-form-field__required", children: " *" })] })), _jsx(Select, { id: inputId, name: name, value: value, onChange: handleChange, onBlur: handleBlur, options: options, disabled: disabled, placeholder: placeholder, "aria-required": required || undefined, "aria-invalid": showError || 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 }))] }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface FormShellStep {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** Field names required in this step — drives publish gate + error badge */
|
|
6
|
+
requiredFields?: string[];
|
|
7
|
+
content: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
export interface FormShellContextValue {
|
|
10
|
+
errors: Record<string, string>;
|
|
11
|
+
touched: Record<string, boolean>;
|
|
12
|
+
setFieldError: (name: string, error: string | null) => void;
|
|
13
|
+
setFieldTouched: (name: string) => void;
|
|
14
|
+
clearFieldError: (name: string) => void;
|
|
15
|
+
steps: FormShellStep[];
|
|
16
|
+
currentStep: number;
|
|
17
|
+
goToStep: (n: number) => void;
|
|
18
|
+
nextStep: () => void;
|
|
19
|
+
prevStep: () => void;
|
|
20
|
+
isPublishReady: boolean;
|
|
21
|
+
isDirty: boolean;
|
|
22
|
+
isSubmitting: boolean;
|
|
23
|
+
stepErrorCounts: number[];
|
|
24
|
+
}
|
|
25
|
+
export interface FormShellProps {
|
|
26
|
+
steps: FormShellStep[];
|
|
27
|
+
/** Called on debounced auto-save and "Save Draft" click */
|
|
28
|
+
onSaveDraft?: (values: Record<string, unknown>) => Promise<{
|
|
29
|
+
id?: string;
|
|
30
|
+
} | void>;
|
|
31
|
+
/** Called on Publish / Save */
|
|
32
|
+
onPublish: (values: Record<string, unknown>) => Promise<void>;
|
|
33
|
+
/** Current form values — caller owns state */
|
|
34
|
+
values: Record<string, unknown>;
|
|
35
|
+
/** Whether any values differ from the persisted state */
|
|
36
|
+
isDirty?: boolean;
|
|
37
|
+
draftId?: string;
|
|
38
|
+
autoSaveDelayMs?: number;
|
|
39
|
+
publishLabel?: string;
|
|
40
|
+
saveDraftLabel?: string;
|
|
41
|
+
title?: string;
|
|
42
|
+
className?: string;
|
|
43
|
+
/** If true, show steps as vertical accordion on mobile instead of wizard tabs */
|
|
44
|
+
mobileAccordion?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare const FormShellContext: React.Context<FormShellContextValue | null>;
|
|
47
|
+
export declare function useFormShell(): FormShellContextValue;
|
|
48
|
+
export declare function FormShell({ steps, onSaveDraft, onPublish, values, isDirty, autoSaveDelayMs, publishLabel, saveDraftLabel, title, className, mobileAccordion, }: FormShellProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
4
|
+
import { Button } from "../components/Button";
|
|
5
|
+
import { Heading, Text } from "../components/Typography";
|
|
6
|
+
// ─── Context ──────────────────────────────────────────────────────────────────
|
|
7
|
+
export const FormShellContext = createContext(null);
|
|
8
|
+
export function useFormShell() {
|
|
9
|
+
const ctx = useContext(FormShellContext);
|
|
10
|
+
if (!ctx)
|
|
11
|
+
throw new Error("useFormShell must be used inside <FormShell>");
|
|
12
|
+
return ctx;
|
|
13
|
+
}
|
|
14
|
+
function StepIndicator({ steps, currentStep, stepErrorCounts, onGoTo, }) {
|
|
15
|
+
return (_jsx("nav", { className: "appkit-formshell__step-indicator", "aria-label": "Form steps", children: steps.map((step, i) => {
|
|
16
|
+
const isActive = i === currentStep;
|
|
17
|
+
const hasErrors = stepErrorCounts[i] > 0;
|
|
18
|
+
return (_jsxs("button", { type: "button", className: [
|
|
19
|
+
"appkit-formshell__step-btn",
|
|
20
|
+
isActive ? "appkit-formshell__step-btn--active" : "",
|
|
21
|
+
hasErrors ? "appkit-formshell__step-btn--error" : "",
|
|
22
|
+
]
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.join(" "), onClick: () => onGoTo(i), "aria-current": isActive ? "step" : undefined, "aria-label": `${step.label}${hasErrors ? ` (${stepErrorCounts[i]} error${stepErrorCounts[i] !== 1 ? "s" : ""})` : ""}`, children: [_jsxs("span", { className: "appkit-formshell__step-dot", children: [hasErrors && (_jsx("span", { className: "appkit-formshell__step-error-badge", "aria-hidden": "true" })), _jsx("span", { className: "appkit-formshell__step-num", children: i + 1 })] }), _jsx("span", { className: "appkit-formshell__step-label", children: step.label })] }, step.id));
|
|
25
|
+
}) }));
|
|
26
|
+
}
|
|
27
|
+
function ErrorSummary({ errors, visible }) {
|
|
28
|
+
const entries = Object.entries(errors).filter(([, v]) => v);
|
|
29
|
+
if (!visible || entries.length === 0)
|
|
30
|
+
return null;
|
|
31
|
+
return (_jsxs("div", { className: "appkit-formshell__error-summary", role: "alert", "aria-live": "assertive", children: [_jsxs(Text, { size: "sm", variant: "error", className: "appkit-formshell__error-summary__title", children: ["Please fix ", entries.length, " error", entries.length !== 1 ? "s" : "", " before publishing."] }), _jsx("ul", { className: "appkit-formshell__error-summary__list", children: entries.map(([field, msg]) => (_jsx("li", { children: _jsxs(Text, { size: "sm", variant: "error", children: [_jsx("strong", { children: field }), ": ", msg] }) }, field))) })] }));
|
|
32
|
+
}
|
|
33
|
+
// ─── FormShell ────────────────────────────────────────────────────────────────
|
|
34
|
+
export function FormShell({ steps, onSaveDraft, onPublish, values, isDirty = false, autoSaveDelayMs = 2000, publishLabel = "Publish", saveDraftLabel = "Save Draft", title, className = "", mobileAccordion = true, }) {
|
|
35
|
+
const [errors, setErrors] = useState({});
|
|
36
|
+
const [touched, setTouched] = useState({});
|
|
37
|
+
const [currentStep, setCurrentStep] = useState(0);
|
|
38
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
39
|
+
const [showErrorSummary, setShowErrorSummary] = useState(false);
|
|
40
|
+
// Auto-save
|
|
41
|
+
const autoSaveTimer = useRef(null);
|
|
42
|
+
const valuesRef = useRef(values);
|
|
43
|
+
valuesRef.current = values;
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!onSaveDraft || !isDirty)
|
|
46
|
+
return;
|
|
47
|
+
if (autoSaveTimer.current)
|
|
48
|
+
clearTimeout(autoSaveTimer.current);
|
|
49
|
+
autoSaveTimer.current = setTimeout(() => {
|
|
50
|
+
onSaveDraft(valuesRef.current).catch(() => { });
|
|
51
|
+
}, autoSaveDelayMs);
|
|
52
|
+
return () => {
|
|
53
|
+
if (autoSaveTimer.current)
|
|
54
|
+
clearTimeout(autoSaveTimer.current);
|
|
55
|
+
};
|
|
56
|
+
}, [values, isDirty, onSaveDraft, autoSaveDelayMs]);
|
|
57
|
+
// Per-step error counts
|
|
58
|
+
const stepErrorCounts = useMemo(() => {
|
|
59
|
+
return steps.map((step) => {
|
|
60
|
+
const fields = step.requiredFields ?? [];
|
|
61
|
+
return fields.filter((f) => errors[f]).length;
|
|
62
|
+
});
|
|
63
|
+
}, [steps, errors]);
|
|
64
|
+
// Publish gate: all required fields across all steps must be valid
|
|
65
|
+
const isPublishReady = useMemo(() => {
|
|
66
|
+
return steps.every((step) => (step.requiredFields ?? []).every((f) => !errors[f]));
|
|
67
|
+
}, [steps, errors]);
|
|
68
|
+
const setFieldError = useCallback((name, error) => {
|
|
69
|
+
setErrors((prev) => {
|
|
70
|
+
if (!error) {
|
|
71
|
+
const next = { ...prev };
|
|
72
|
+
delete next[name];
|
|
73
|
+
return next;
|
|
74
|
+
}
|
|
75
|
+
if (prev[name] === error)
|
|
76
|
+
return prev;
|
|
77
|
+
return { ...prev, [name]: error };
|
|
78
|
+
});
|
|
79
|
+
}, []);
|
|
80
|
+
const clearFieldError = useCallback((name) => {
|
|
81
|
+
setErrors((prev) => {
|
|
82
|
+
if (!prev[name])
|
|
83
|
+
return prev;
|
|
84
|
+
const next = { ...prev };
|
|
85
|
+
delete next[name];
|
|
86
|
+
return next;
|
|
87
|
+
});
|
|
88
|
+
}, []);
|
|
89
|
+
const setFieldTouched = useCallback((name) => {
|
|
90
|
+
setTouched((prev) => (prev[name] ? prev : { ...prev, [name]: true }));
|
|
91
|
+
}, []);
|
|
92
|
+
const goToStep = useCallback((n) => {
|
|
93
|
+
if (n >= 0 && n < steps.length)
|
|
94
|
+
setCurrentStep(n);
|
|
95
|
+
}, [steps.length]);
|
|
96
|
+
const nextStep = useCallback(() => setCurrentStep((s) => Math.min(s + 1, steps.length - 1)), [steps.length]);
|
|
97
|
+
const prevStep = useCallback(() => setCurrentStep((s) => Math.max(s - 1, 0)), []);
|
|
98
|
+
const handleSaveDraft = useCallback(async () => {
|
|
99
|
+
if (!onSaveDraft)
|
|
100
|
+
return;
|
|
101
|
+
setIsSubmitting(true);
|
|
102
|
+
try {
|
|
103
|
+
await onSaveDraft(values);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
setIsSubmitting(false);
|
|
107
|
+
}
|
|
108
|
+
}, [onSaveDraft, values]);
|
|
109
|
+
const handlePublish = useCallback(async () => {
|
|
110
|
+
setShowErrorSummary(true);
|
|
111
|
+
if (!isPublishReady)
|
|
112
|
+
return;
|
|
113
|
+
setIsSubmitting(true);
|
|
114
|
+
try {
|
|
115
|
+
await onPublish(values);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
setIsSubmitting(false);
|
|
119
|
+
}
|
|
120
|
+
}, [isPublishReady, onPublish, values]);
|
|
121
|
+
const ctx = useMemo(() => ({
|
|
122
|
+
errors,
|
|
123
|
+
touched,
|
|
124
|
+
setFieldError,
|
|
125
|
+
setFieldTouched,
|
|
126
|
+
clearFieldError,
|
|
127
|
+
steps,
|
|
128
|
+
currentStep,
|
|
129
|
+
goToStep,
|
|
130
|
+
nextStep,
|
|
131
|
+
prevStep,
|
|
132
|
+
isPublishReady,
|
|
133
|
+
isDirty,
|
|
134
|
+
isSubmitting,
|
|
135
|
+
stepErrorCounts,
|
|
136
|
+
}), [
|
|
137
|
+
errors,
|
|
138
|
+
touched,
|
|
139
|
+
setFieldError,
|
|
140
|
+
setFieldTouched,
|
|
141
|
+
clearFieldError,
|
|
142
|
+
steps,
|
|
143
|
+
currentStep,
|
|
144
|
+
goToStep,
|
|
145
|
+
nextStep,
|
|
146
|
+
prevStep,
|
|
147
|
+
isPublishReady,
|
|
148
|
+
isDirty,
|
|
149
|
+
isSubmitting,
|
|
150
|
+
stepErrorCounts,
|
|
151
|
+
]);
|
|
152
|
+
const isMultiStep = steps.length > 1;
|
|
153
|
+
return (_jsx(FormShellContext.Provider, { value: ctx, children: _jsxs("div", { className: [
|
|
154
|
+
"appkit-formshell",
|
|
155
|
+
mobileAccordion ? "appkit-formshell--accordion-mobile" : "",
|
|
156
|
+
className,
|
|
157
|
+
]
|
|
158
|
+
.filter(Boolean)
|
|
159
|
+
.join(" "), children: [(title || isMultiStep) && (_jsxs("div", { className: "appkit-formshell__header", children: [title && (_jsx(Heading, { level: 2, className: "appkit-formshell__title", children: title })), isMultiStep && (_jsx(StepIndicator, { steps: steps, currentStep: currentStep, stepErrorCounts: stepErrorCounts, onGoTo: goToStep }))] })), _jsx(ErrorSummary, { errors: errors, visible: showErrorSummary }), _jsx("div", { className: "appkit-formshell__body", children: isMultiStep ? (mobileAccordion ? (
|
|
160
|
+
// Mobile: vertical accordion; Desktop: show only current step
|
|
161
|
+
_jsx("div", { className: "appkit-formshell__steps", children: steps.map((step, i) => {
|
|
162
|
+
const isActive = i === currentStep;
|
|
163
|
+
const hasErrors = stepErrorCounts[i] > 0;
|
|
164
|
+
return (_jsxs("div", { className: [
|
|
165
|
+
"appkit-formshell__step",
|
|
166
|
+
isActive ? "appkit-formshell__step--active" : "",
|
|
167
|
+
hasErrors ? "appkit-formshell__step--error" : "",
|
|
168
|
+
]
|
|
169
|
+
.filter(Boolean)
|
|
170
|
+
.join(" "), children: [_jsxs("button", { type: "button", className: "appkit-formshell__step-accordion-btn", onClick: () => goToStep(i), "aria-expanded": isActive, children: [_jsxs("span", { className: "appkit-formshell__step-accordion-label", children: [i + 1, ". ", step.label] }), hasErrors && (_jsx("span", { className: "appkit-formshell__step-error-badge appkit-formshell__step-error-badge--inline", children: stepErrorCounts[i] }))] }), _jsx("div", { className: "appkit-formshell__step-content", hidden: !isActive, "aria-hidden": !isActive, children: step.content })] }, step.id));
|
|
171
|
+
}) })) : (
|
|
172
|
+
// Non-accordion: show only the active step
|
|
173
|
+
_jsx("div", { className: "appkit-formshell__step-content", children: steps[currentStep]?.content }))) : (
|
|
174
|
+
// Single step: render content directly
|
|
175
|
+
_jsx("div", { className: "appkit-formshell__step-content", children: steps[0]?.content })) }), _jsxs("div", { className: "appkit-formshell__footer", children: [isMultiStep && (_jsxs("div", { className: "appkit-formshell__step-nav", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: prevStep, disabled: currentStep === 0 || isSubmitting, children: "Previous" }), currentStep < steps.length - 1 && (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: nextStep, disabled: isSubmitting, children: "Next" }))] })), _jsxs("div", { className: "appkit-formshell__actions", children: [onSaveDraft && (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: handleSaveDraft, isLoading: isSubmitting, disabled: isSubmitting, children: saveDraftLabel })), _jsx(Button, { type: "button", size: "sm", onClick: handlePublish, isLoading: isSubmitting, disabled: isSubmitting || (showErrorSummary && !isPublishReady), children: publishLabel })] })] })] }) }));
|
|
176
|
+
}
|