@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.
Files changed (140) hide show
  1. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +1 -1
  2. package/dist/_internal/client/features/seller/print-center/InventoryLabel.d.ts +12 -0
  3. package/dist/_internal/client/features/seller/print-center/InventoryLabel.js +49 -0
  4. package/dist/_internal/client/features/seller/print-center/LabelDesignPicker.d.ts +7 -0
  5. package/dist/_internal/client/features/seller/print-center/LabelDesignPicker.js +32 -0
  6. package/dist/_internal/client/features/seller/print-center/OrderPackingLabel.d.ts +28 -0
  7. package/dist/_internal/client/features/seller/print-center/OrderPackingLabel.js +33 -0
  8. package/dist/_internal/client/features/seller/print-center/PhysicalLocationModal.d.ts +12 -0
  9. package/dist/_internal/client/features/seller/print-center/PhysicalLocationModal.js +30 -0
  10. package/dist/_internal/client/features/seller/print-center/PrintCenterView.d.ts +12 -0
  11. package/dist/_internal/client/features/seller/print-center/PrintCenterView.js +102 -0
  12. package/dist/_internal/client/features/seller/print-center/PrintGrid.d.ts +12 -0
  13. package/dist/_internal/client/features/seller/print-center/PrintGrid.js +13 -0
  14. package/dist/_internal/client/features/seller/print-center/StoreCard.d.ts +19 -0
  15. package/dist/_internal/client/features/seller/print-center/StoreCard.js +30 -0
  16. package/dist/_internal/client/features/seller/print-center/WebsiteCard.d.ts +11 -0
  17. package/dist/_internal/client/features/seller/print-center/WebsiteCard.js +25 -0
  18. package/dist/_internal/client/features/seller/print-center/index.d.ts +11 -0
  19. package/dist/_internal/client/features/seller/print-center/index.js +10 -0
  20. package/dist/_internal/client/features/seller/print-center/types.d.ts +20 -0
  21. package/dist/_internal/client/features/seller/print-center/types.js +20 -0
  22. package/dist/_internal/client/features/seller/print-center/useInventoryPdf.d.ts +18 -0
  23. package/dist/_internal/client/features/seller/print-center/useInventoryPdf.js +76 -0
  24. package/dist/_internal/server/jobs/core/draftPrune.d.ts +2 -0
  25. package/dist/_internal/server/jobs/core/draftPrune.js +15 -0
  26. package/dist/_internal/server/jobs/core/onBidPlaced.js +1 -1
  27. package/dist/_internal/server/jobs/core/promotions.d.ts +1 -0
  28. package/dist/_internal/server/jobs/core/promotions.js +21 -6
  29. package/dist/_internal/server/jobs/handlers/assignSpinPrize.js +18 -1
  30. package/dist/_internal/server/jobs/handlers/draftPrune.d.ts +2 -0
  31. package/dist/_internal/server/jobs/handlers/draftPrune.js +2 -0
  32. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
  33. package/dist/_internal/server/jobs/handlers/index.js +2 -0
  34. package/dist/_internal/server/jobs/handlers/triggerEventRaffle.js +15 -1
  35. package/dist/_internal/shared/actions/action-registry.js +32 -0
  36. package/dist/_internal/shared/features/blog/schema.d.ts +8 -8
  37. package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +10 -10
  38. package/dist/_internal/shared/features/orders/schema.d.ts +8 -8
  39. package/dist/_internal/shared/features/products/schema.d.ts +8 -8
  40. package/dist/_internal/shared/features/promotions/schema.d.ts +6 -6
  41. package/dist/_internal/shared/features/reviews/schema.d.ts +2 -2
  42. package/dist/client.d.ts +11 -1
  43. package/dist/client.js +6 -1
  44. package/dist/configs/next.js +1 -0
  45. package/dist/constants/api-endpoints.d.ts +6 -0
  46. package/dist/constants/api-endpoints.js +2 -0
  47. package/dist/core/server-action.d.ts +4 -0
  48. package/dist/errors/error-codes.d.ts +18 -0
  49. package/dist/errors/error-codes.js +18 -0
  50. package/dist/errors/index.d.ts +1 -1
  51. package/dist/errors/index.js +1 -1
  52. package/dist/errors/messages.d.ts +1 -0
  53. package/dist/errors/messages.js +1 -0
  54. package/dist/features/account/components/UserSidebar.js +1 -1
  55. package/dist/features/account/schemas/index.d.ts +12 -12
  56. package/dist/features/admin/components/AdminSidebar.js +1 -1
  57. package/dist/features/auctions/actions/bid-actions.js +13 -6
  58. package/dist/features/auctions/components/PlaceBidFormClient.js +10 -2
  59. package/dist/features/auctions/repository/bid.repository.d.ts +7 -0
  60. package/dist/features/auctions/repository/bid.repository.js +16 -0
  61. package/dist/features/auctions/schemas/index.d.ts +18 -18
  62. package/dist/features/auth/schemas/index.d.ts +2 -2
  63. package/dist/features/before-after/schemas/index.d.ts +2 -2
  64. package/dist/features/blog/actions/blog-actions.d.ts +32 -32
  65. package/dist/features/blog/components/BlogPostForm.js +1 -1
  66. package/dist/features/blog/schemas/index.d.ts +38 -38
  67. package/dist/features/categories/schemas/index.d.ts +10 -10
  68. package/dist/features/collections/schemas/index.d.ts +4 -4
  69. package/dist/features/consultation/schemas/index.d.ts +9 -9
  70. package/dist/features/corporate/schemas/index.d.ts +4 -4
  71. package/dist/features/events/schemas/index.d.ts +50 -50
  72. package/dist/features/faq/actions/faq-actions.d.ts +6 -6
  73. package/dist/features/faq/schemas/index.d.ts +8 -8
  74. package/dist/features/homepage/actions/homepage-section-actions.d.ts +4 -4
  75. package/dist/features/layout/AppLayoutShell.js +1 -1
  76. package/dist/features/layout/AutoBreadcrumbs.js +1 -1
  77. package/dist/features/media/AvatarUpload.js +1 -1
  78. package/dist/features/media/types/index.d.ts +4 -4
  79. package/dist/features/media/upload/ImageUpload.js +18 -9
  80. package/dist/features/media/upload/MediaUploadList.js +2 -2
  81. package/dist/features/orders/schemas/firestore.d.ts +6 -0
  82. package/dist/features/orders/schemas/index.d.ts +10 -10
  83. package/dist/features/orders/types/index.d.ts +5 -0
  84. package/dist/features/payments/schemas/index.d.ts +4 -4
  85. package/dist/features/pre-orders/schemas/index.d.ts +8 -8
  86. package/dist/features/products/components/PrizeDrawItemsEditor.js +1 -1
  87. package/dist/features/products/components/ProductForm.js +20 -2
  88. package/dist/features/products/repository/products.repository.d.ts +3 -1
  89. package/dist/features/products/repository/products.repository.js +12 -1
  90. package/dist/features/products/schemas/firestore.d.ts +7 -0
  91. package/dist/features/products/schemas/index.d.ts +34 -34
  92. package/dist/features/products/schemas/product-features.validators.d.ts +6 -6
  93. package/dist/features/products/types/index.d.ts +6 -0
  94. package/dist/features/promotions/schemas/index.d.ts +14 -14
  95. package/dist/features/reviews/schemas/index.d.ts +18 -18
  96. package/dist/features/seller/actions/offer-actions.js +10 -1
  97. package/dist/features/seller/components/PhysicalLocationModal.d.ts +12 -0
  98. package/dist/features/seller/components/PhysicalLocationModal.js +30 -0
  99. package/dist/features/seller/components/PrintCenterView.d.ts +40 -0
  100. package/dist/features/seller/components/PrintCenterView.js +6 -0
  101. package/dist/features/seller/components/SellerOrdersView.js +48 -3
  102. package/dist/features/seller/components/SellerProductShell.d.ts +1 -0
  103. package/dist/features/seller/components/SellerProductShell.js +26 -4
  104. package/dist/features/seller/components/SellerProductsView.js +54 -13
  105. package/dist/features/seller/components/SellerSidebar.js +1 -1
  106. package/dist/features/seller/components/index.d.ts +1 -0
  107. package/dist/features/seller/components/index.js +1 -0
  108. package/dist/features/seller/schemas/firestore.d.ts +2 -0
  109. package/dist/features/seller/schemas/firestore.js +1 -0
  110. package/dist/features/seller/schemas/index.d.ts +22 -22
  111. package/dist/features/shell/FormShell.js +1 -1
  112. package/dist/features/shell/StepForm.d.ts +3 -1
  113. package/dist/features/shell/StepForm.js +3 -3
  114. package/dist/features/stores/schemas/index.d.ts +4 -4
  115. package/dist/index.d.ts +9 -1
  116. package/dist/index.js +6 -1
  117. package/dist/next/routing/route-map.d.ts +8 -0
  118. package/dist/next/routing/route-map.js +3 -0
  119. package/dist/tailwind-utilities.css +1 -1
  120. package/dist/ui/components/RowActionMenu.js +45 -10
  121. package/dist/ui/components/SideDrawer.style.css +2 -2
  122. package/dist/ui/forms/FieldCheckbox.d.ts +13 -0
  123. package/dist/ui/forms/FieldCheckbox.js +25 -0
  124. package/dist/ui/forms/FieldInput.d.ts +11 -0
  125. package/dist/ui/forms/FieldInput.js +25 -0
  126. package/dist/ui/forms/FieldSelect.d.ts +17 -0
  127. package/dist/ui/forms/FieldSelect.js +25 -0
  128. package/dist/ui/forms/FormShell.d.ts +48 -0
  129. package/dist/ui/forms/FormShell.js +176 -0
  130. package/dist/ui/forms/FormShell.style.css +287 -0
  131. package/dist/ui/forms/index.d.ts +8 -0
  132. package/dist/ui/forms/index.js +4 -0
  133. package/dist/ui/index.d.ts +9 -0
  134. package/dist/ui/index.js +5 -0
  135. package/dist/utils/action-response.d.ts +6 -0
  136. package/dist/utils/action-response.js +22 -0
  137. package/dist/utils/index.d.ts +1 -0
  138. package/dist/utils/index.js +1 -0
  139. package/dist/validation/schemas.d.ts +10 -10
  140. 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 { useClickOutside, useKeyPress } from "../../react";
8
+ import { useKeyPress } from "../../react";
8
9
  export function RowActionMenu({ actions, align = "right", }) {
9
10
  const [open, setOpen] = useState(false);
10
- const ref = useRef(null);
11
- useClickOutside(ref, () => setOpen(false), { enabled: open });
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
- return (_jsxs("div", { ref: ref, 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: () => setOpen((v) => !v), className: "appkit-row-action-menu__trigger", children: _jsx(MoreHorizontal, { className: "h-4 w-4", strokeWidth: 1.5 }) }), open ? (_jsx("div", { role: "menu", className: `appkit-row-action-menu__dropdown ${align === "right" ? "appkit-row-action-menu__dropdown--right" : "appkit-row-action-menu__dropdown--left"}`, "data-section": "rowactionmenu-div-585", children: actions.map((action, idx) => (_jsxs("div", { "data-section": "rowactionmenu-div-586", 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: () => {
14
- if (action.disabled)
15
- return;
16
- action.onClick();
17
- setOpen(false);
18
- }, 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}`))) })) : null] }));
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 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
+ }