@mohasinac/appkit 2.7.44 → 2.7.46
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/server/features/checkout/actions.js +22 -3
- package/dist/_internal/server/features/products/actions.js +7 -0
- package/dist/_internal/server/features/refunds/actions.js +44 -0
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -1
- package/dist/features/account/components/AddressForm.js +23 -1
- package/dist/features/admin/components/AdminBlogEditorView.js +18 -11
- package/dist/features/admin/components/AdminBrandEditorView.js +18 -11
- package/dist/features/admin/components/AdminCarouselEditorView.js +18 -8
- package/dist/features/admin/components/AdminCategoryEditorView.js +18 -11
- package/dist/features/admin/components/AdminCouponEditorView.js +18 -11
- package/dist/features/admin/components/AdminFaqEditorView.js +18 -11
- package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +18 -11
- package/dist/features/contact/email.d.ts +8 -0
- package/dist/features/contact/email.js +60 -0
- package/dist/features/seller/components/SellerProductShell.js +27 -3
- package/dist/features/shell/StepForm.d.ts +6 -2
- package/dist/features/shell/StepForm.js +13 -10
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Toast.d.ts +4 -1
- package/dist/ui/components/Toast.js +3 -3
- package/dist/ui/forms/FormShell.d.ts +19 -1
- package/dist/ui/forms/FormShell.js +92 -0
- package/dist/ui/forms/index.d.ts +2 -2
- package/dist/ui/forms/index.js +1 -1
- package/package.json +2 -1
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
5
5
|
import { Button, Form, Input, StackedViewShell, useToast, } from "../../../ui";
|
|
6
|
+
import { FieldInput, FormShellContext, useFormShellState } from "../../../ui/forms";
|
|
6
7
|
import { ImageUpload } from "../../media/upload/ImageUpload";
|
|
7
8
|
import { useMediaUpload } from "../../media";
|
|
8
9
|
import { apiClient } from "../../../http";
|
|
@@ -67,17 +68,23 @@ export function AdminSublistingCategoryEditorView({ categoryId, onSaved, onDelet
|
|
|
67
68
|
});
|
|
68
69
|
const { upload } = useMediaUpload();
|
|
69
70
|
const isSubmitting = saveMutation.isPending || categoryQuery.isLoading;
|
|
71
|
+
const { shellCtx, setFieldError, clearErrors } = useFormShellState();
|
|
70
72
|
return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Sub-listing Category" : "New Sub-listing Category", sections: [
|
|
71
|
-
_jsxs(Form, { onSubmit: (e) => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Form, { onSubmit: (e) => {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
clearErrors();
|
|
76
|
+
if (!name.trim()) {
|
|
77
|
+
setFieldError("name", "Category name is required");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
saveMutation.mutate();
|
|
81
|
+
}, className: "space-y-4", children: [_jsxs("div", { className: "grid sm:grid-cols-2 gap-4", children: [_jsx(FieldInput, { name: "name", label: "Category name", value: name, onChange: (v) => setName(v), required: true, placeholder: "e.g. Base Set Charizard 108/120" }), _jsx(Input, { label: "Item code", value: itemCode, onChange: (e) => setItemCode(e.target.value), placeholder: "e.g. PSA 10, 108/120, STH", helperText: "Grade, card number, or series code. Optional." })] }), _jsx(Input, { label: "Description", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Brief description shown on the public category page" }), _jsx(ImageUpload, { label: "Cover image", currentImage: coverImage, onUpload: (file) => upload(file, "sublisting-categories", true, {
|
|
82
|
+
type: "category-image",
|
|
83
|
+
name: name || "sublisting",
|
|
84
|
+
}), onChange: setCoverImage }), _jsxs("div", { className: "flex gap-3 pt-2", children: [_jsx(Button, { type: "submit", isLoading: isSubmitting, disabled: !name || isSubmitting, children: isEdit ? "Save changes" : "Create category" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", isLoading: deleteMutation.isPending, onClick: () => {
|
|
85
|
+
if (confirm("Delete this category? All linked listings will be unlinked. This cannot be undone.")) {
|
|
86
|
+
deleteMutation.mutate();
|
|
87
|
+
}
|
|
88
|
+
}, children: "Delete" }))] })] }, "sc-editor-form") }, "sc-ctx"),
|
|
82
89
|
] }));
|
|
83
90
|
}
|
|
@@ -49,6 +49,14 @@ export declare function sendContactEmail(params: {
|
|
|
49
49
|
success: boolean;
|
|
50
50
|
data?: unknown;
|
|
51
51
|
}>;
|
|
52
|
+
export declare function sendDigitalCodeClaimedEmail(params: {
|
|
53
|
+
to: string;
|
|
54
|
+
userName: string;
|
|
55
|
+
productTitle: string;
|
|
56
|
+
orderId: string;
|
|
57
|
+
}): Promise<{
|
|
58
|
+
success: boolean;
|
|
59
|
+
}>;
|
|
52
60
|
export declare function sendSiteSettingsChangedEmail(params: {
|
|
53
61
|
adminEmails: string[];
|
|
54
62
|
changedByEmail: string;
|
|
@@ -220,6 +220,66 @@ export async function sendContactEmail(params) {
|
|
|
220
220
|
return { success: false };
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
|
+
export async function sendDigitalCodeClaimedEmail(params) {
|
|
224
|
+
const siteName = getSiteName();
|
|
225
|
+
const siteUrl = getSiteUrl();
|
|
226
|
+
const { to, userName, productTitle, orderId } = params;
|
|
227
|
+
const orderUrl = `${siteUrl}/user/orders/view/${orderId}`;
|
|
228
|
+
try {
|
|
229
|
+
const { error } = await sendConfiguredEmail({
|
|
230
|
+
to,
|
|
231
|
+
subject: `Your digital code for "${productTitle}" is ready`,
|
|
232
|
+
html: `
|
|
233
|
+
<!DOCTYPE html><html>
|
|
234
|
+
<head><meta charset="utf-8"><title>Digital Code Ready</title></head>
|
|
235
|
+
<body style="margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;background-color:#f5f5f5;">
|
|
236
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#f5f5f5;padding:40px 0;">
|
|
237
|
+
<tr><td align="center">
|
|
238
|
+
<table width="600" cellpadding="0" cellspacing="0" border="0" style="background-color:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.1);">
|
|
239
|
+
<tr><td style="background:linear-gradient(135deg,#6366f1 0%,#4f46e5 100%);padding:40px;text-align:center;">
|
|
240
|
+
<p style="font-size:48px;margin:0 0 12px;">🔑</p>
|
|
241
|
+
<h1 style="color:#ffffff;margin:0;font-size:26px;font-weight:600;">Your Digital Code Is Ready</h1>
|
|
242
|
+
</td></tr>
|
|
243
|
+
<tr><td style="padding:40px;">
|
|
244
|
+
<p style="color:#333;font-size:16px;line-height:1.6;margin:0 0 16px;">Hi ${userName},</p>
|
|
245
|
+
<p style="color:#333;font-size:16px;line-height:1.6;margin:0 0 24px;">
|
|
246
|
+
Your digital code for <strong>${productTitle}</strong> has been assigned to your order and is ready to reveal.
|
|
247
|
+
</p>
|
|
248
|
+
<table width="100%" cellpadding="12" cellspacing="0" border="0" style="background:#f8f9fa;border-radius:8px;margin-bottom:24px;">
|
|
249
|
+
<tr><td style="color:#666;font-size:14px;width:40%;">Order ID</td>
|
|
250
|
+
<td style="color:#333;font-size:14px;font-weight:600;">${orderId}</td></tr>
|
|
251
|
+
<tr><td style="color:#666;font-size:14px;">Product</td>
|
|
252
|
+
<td style="color:#333;font-size:14px;">${productTitle}</td></tr>
|
|
253
|
+
</table>
|
|
254
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:24px 0;">
|
|
255
|
+
<tr><td align="center">
|
|
256
|
+
<a href="${orderUrl}" style="display:inline-block;padding:14px 40px;background:linear-gradient(135deg,#6366f1 0%,#4f46e5 100%);color:#ffffff;text-decoration:none;border-radius:6px;font-size:15px;font-weight:600;">
|
|
257
|
+
Reveal Your Code
|
|
258
|
+
</a>
|
|
259
|
+
</td></tr>
|
|
260
|
+
</table>
|
|
261
|
+
<p style="color:#999;font-size:13px;line-height:1.6;margin:0;">For security, the code is only shown inside your ${siteName} account.</p>
|
|
262
|
+
</td></tr>
|
|
263
|
+
<tr><td style="background-color:#f8f9fa;padding:24px;text-align:center;border-top:1px solid #eee;">
|
|
264
|
+
<p style="color:#9ca3af;font-size:12px;margin:0;">© ${currentYear()} ${siteName}. All rights reserved.</p>
|
|
265
|
+
</td></tr>
|
|
266
|
+
</table>
|
|
267
|
+
</td></tr>
|
|
268
|
+
</table>
|
|
269
|
+
</body></html>`,
|
|
270
|
+
text: `Hi ${userName},\n\nYour digital code for "${productTitle}" (Order: ${orderId}) is ready to reveal.\n\nVisit your order: ${orderUrl}\n\nFor security, the code is only shown inside your ${siteName} account.\n\n© ${currentYear()} ${siteName}`,
|
|
271
|
+
});
|
|
272
|
+
if (error) {
|
|
273
|
+
serverLogger.error("Failed to send digital code claimed email", { error });
|
|
274
|
+
return { success: false };
|
|
275
|
+
}
|
|
276
|
+
return { success: true };
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
serverLogger.error("Error sending digital code claimed email", { error });
|
|
280
|
+
return { success: false };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
223
283
|
export async function sendSiteSettingsChangedEmail(params) {
|
|
224
284
|
const siteName = getSiteName();
|
|
225
285
|
const siteUrl = getSiteUrl();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useRef, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { useRouter } from "next/navigation";
|
|
5
5
|
import { FormShell, StepForm, StepFormActions, useFormShell } from "../../shell";
|
|
6
|
+
import { FormShellProvider } from "../../../ui/forms";
|
|
6
7
|
import { Alert, Button, Div, FormField, FormGroup, Heading, Section, Stack, Text, Toggle } from "../../../ui";
|
|
7
8
|
import { ImageUpload, MediaUploadField, MediaUploadList, useMediaUpload } from "../../media";
|
|
8
9
|
import { StoreAddressSelectorCreate } from "../../stores/components/StoreAddressSelectorCreate";
|
|
@@ -146,6 +147,25 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
|
|
|
146
147
|
const [stepError, setStepError] = useState(null);
|
|
147
148
|
const { isDirty, markDirty, markClean } = useFormShell();
|
|
148
149
|
const router = useRouter();
|
|
150
|
+
// Auto-save in create mode — debounce 2s on any draft change
|
|
151
|
+
const autoSaveTimer = useRef(null);
|
|
152
|
+
const draftRef = useRef(draft);
|
|
153
|
+
draftRef.current = draft;
|
|
154
|
+
const onSaveRef = useRef(onSave);
|
|
155
|
+
onSaveRef.current = onSave;
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (mode !== "create" || !isDirty)
|
|
158
|
+
return;
|
|
159
|
+
if (autoSaveTimer.current)
|
|
160
|
+
clearTimeout(autoSaveTimer.current);
|
|
161
|
+
autoSaveTimer.current = setTimeout(() => {
|
|
162
|
+
const result = onSaveRef.current(draftRef.current);
|
|
163
|
+
if (result && typeof result.catch === "function")
|
|
164
|
+
result.catch(() => { });
|
|
165
|
+
}, 2000);
|
|
166
|
+
return () => { if (autoSaveTimer.current)
|
|
167
|
+
clearTimeout(autoSaveTimer.current); };
|
|
168
|
+
}, [draft, isDirty, mode]);
|
|
149
169
|
const update = useCallback((partial) => {
|
|
150
170
|
setDraft((prev) => ({ ...prev, ...partial }));
|
|
151
171
|
markDirty();
|
|
@@ -247,12 +267,16 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
|
|
|
247
267
|
await handlePublish();
|
|
248
268
|
}
|
|
249
269
|
}, [currentStep, steps, draft, handlePublish]);
|
|
270
|
+
// Step error badges — run each step's validate against current draft
|
|
271
|
+
const stepValidationErrors = useMemo(() => steps.map((s) => (s.validate ? Boolean(s.validate(draft)) : false)),
|
|
272
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
273
|
+
[draft]);
|
|
250
274
|
const breadcrumb = mode === "create" ? `Store / ${listingTypeLabel}s / New` : `Store / ${listingTypeLabel}s / Edit`;
|
|
251
275
|
const title = mode === "create"
|
|
252
276
|
? `New ${listingTypeLabel}`
|
|
253
277
|
: draft.title ?? `Edit ${listingTypeLabel}`;
|
|
254
278
|
if (mode === "create") {
|
|
255
|
-
return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, previewSlot: previewSlot, renderBottomBar: () => (_jsxs("div", { className: "flex-shrink-0 border-t border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", children: [_jsx(StepFormActions, { currentStep: currentStep, totalSteps: steps.length, onNext: () => void handleNext(), onPrev: currentStep > 0 ? () => setCurrentStep((c) => c - 1) : undefined, completeLabel: `Publish ${listingTypeLabel}`, isLoading: isLoading && currentStep === steps.length - 1, disabled: isLoading }), stepError && (_jsx(Text, { className: "px-5 pb-3 text-sm text-[var(--appkit-color-error)]", children: stepError }))] })), children: _jsx(StepForm, { steps: steps, values: draft, onChange: update, onComplete: handlePublish, completeLabel: `Publish ${listingTypeLabel}`, currentStep: currentStep, onStepChange: setCurrentStep, isLoading: isLoading, hideActions: true }) }));
|
|
279
|
+
return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, previewSlot: previewSlot, renderBottomBar: () => (_jsxs("div", { className: "flex-shrink-0 border-t border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", children: [_jsx(StepFormActions, { currentStep: currentStep, totalSteps: steps.length, onNext: () => void handleNext(), onPrev: currentStep > 0 ? () => setCurrentStep((c) => c - 1) : undefined, completeLabel: `Publish ${listingTypeLabel}`, isLoading: isLoading && currentStep === steps.length - 1, disabled: isLoading }), stepError && (_jsx(Text, { className: "px-5 pb-3 text-sm text-[var(--appkit-color-error)]", children: stepError }))] })), children: _jsx(FormShellProvider, { isDirty: isDirty, values: draft, children: _jsx(StepForm, { steps: steps, values: draft, onChange: update, onComplete: handlePublish, completeLabel: `Publish ${listingTypeLabel}`, currentStep: currentStep, onStepChange: setCurrentStep, isLoading: isLoading, hideActions: true, stepErrors: stepValidationErrors }) }) }));
|
|
256
280
|
}
|
|
257
281
|
// Edit mode — FormShell with section nav + full form
|
|
258
282
|
const editSections = [
|
|
@@ -263,5 +287,5 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
|
|
|
263
287
|
...(listingType === "digital-code" ? [{ id: "digitalcode", label: "Code Details" }] : []),
|
|
264
288
|
...(listingType === "live" ? [{ id: "live", label: "Live Item" }] : []),
|
|
265
289
|
];
|
|
266
|
-
return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, sections: editSections, onSaveDraft: handleSave, onPublish: handlePublish, saveLabel: "Save Changes", publishLabel: "Update", previewSlot: previewSlot, children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Section, { id: "basic", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Basic Info" }), _jsx(StepBasic, { values: draft, onChange: update, renderCategorySelector: renderCategorySelector, renderBrandSelector: renderBrandSelector, renderTemplateSelector: renderTemplateSelector })] }), _jsxs(Section, { id: "media", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Media" }), _jsx(StepMedia, { values: draft, onChange: update, storeSlug: storeSlug })] }), listingType === "auction" && (_jsxs(Section, { id: "auction", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Auction Settings" }), _jsx(StepAuctionSettings, { values: draft, onChange: update })] })), listingType === "pre-order" && (_jsxs(Section, { id: "preorder", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Pre-Order Settings" }), _jsx(StepPreOrderSettings, { values: draft, onChange: update })] })), listingType === "classified" && (_jsxs(Section, { id: "classified", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Meetup Details" }), _jsx(StepClassifiedSettings, { values: draft, onChange: update })] })), listingType === "digital-code" && (_jsxs(Section, { id: "digitalcode", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Code Details" }), _jsx(StepDigitalCodeSettings, { values: draft, onChange: update })] })), listingType === "live" && (_jsxs(Section, { id: "live", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Live Item Details" }), _jsx(StepLiveItemSettings, { values: draft, onChange: update })] })), _jsxs(Section, { id: "pricing", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Pricing" }), _jsx(StepPricing, { values: draft, onChange: update, listingType: listingType })] }), _jsxs(Section, { id: "shipping", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Shipping" }), _jsx(StepShipping, { values: draft, onChange: update, renderAddressSelector: renderAddressSelector })] }), _jsxs(Section, { id: "publish", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Publish" }), _jsx(StepPublish, { values: draft, onChange: update }), onSaveAsTemplate && (_jsxs(Div, { className: "mt-4 border-t border-[var(--appkit-color-border)] pt-4", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onSaveAsTemplate(draft), children: "Save as Template" }), _jsx(Text, { className: "mt-1 text-xs text-[var(--appkit-color-secondary-text)]", children: "Save these settings as a reusable template for future listings." })] }))] })] }) }));
|
|
290
|
+
return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, sections: editSections, onSaveDraft: handleSave, onPublish: handlePublish, saveLabel: "Save Changes", publishLabel: "Update", previewSlot: previewSlot, children: _jsx(FormShellProvider, { isDirty: isDirty, values: draft, children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Section, { id: "basic", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Basic Info" }), _jsx(StepBasic, { values: draft, onChange: update, renderCategorySelector: renderCategorySelector, renderBrandSelector: renderBrandSelector, renderTemplateSelector: renderTemplateSelector })] }), _jsxs(Section, { id: "media", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Media" }), _jsx(StepMedia, { values: draft, onChange: update, storeSlug: storeSlug })] }), listingType === "auction" && (_jsxs(Section, { id: "auction", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Auction Settings" }), _jsx(StepAuctionSettings, { values: draft, onChange: update })] })), listingType === "pre-order" && (_jsxs(Section, { id: "preorder", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Pre-Order Settings" }), _jsx(StepPreOrderSettings, { values: draft, onChange: update })] })), listingType === "classified" && (_jsxs(Section, { id: "classified", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Meetup Details" }), _jsx(StepClassifiedSettings, { values: draft, onChange: update })] })), listingType === "digital-code" && (_jsxs(Section, { id: "digitalcode", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Code Details" }), _jsx(StepDigitalCodeSettings, { values: draft, onChange: update })] })), listingType === "live" && (_jsxs(Section, { id: "live", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Live Item Details" }), _jsx(StepLiveItemSettings, { values: draft, onChange: update })] })), _jsxs(Section, { id: "pricing", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Pricing" }), _jsx(StepPricing, { values: draft, onChange: update, listingType: listingType })] }), _jsxs(Section, { id: "shipping", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Shipping" }), _jsx(StepShipping, { values: draft, onChange: update, renderAddressSelector: renderAddressSelector })] }), _jsxs(Section, { id: "publish", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Publish" }), _jsx(StepPublish, { values: draft, onChange: update }), onSaveAsTemplate && (_jsxs(Div, { className: "mt-4 border-t border-[var(--appkit-color-border)] pt-4", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onSaveAsTemplate(draft), children: "Save as Template" }), _jsx(Text, { className: "mt-1 text-xs text-[var(--appkit-color-secondary-text)]", children: "Save these settings as a reusable template for future listings." })] }))] })] }) }) }));
|
|
267
291
|
}
|
|
@@ -23,6 +23,8 @@ export interface StepFormProps<T extends object = Record<string, unknown>> {
|
|
|
23
23
|
onStepChange: (step: number) => void;
|
|
24
24
|
/** Suppress the built-in action bar. Use FormShell's renderBottomBar instead. */
|
|
25
25
|
hideActions?: boolean;
|
|
26
|
+
/** Per-step error flags — when true, step button shows a red error badge */
|
|
27
|
+
stepErrors?: boolean[];
|
|
26
28
|
}
|
|
27
29
|
export interface StepFormActionsProps {
|
|
28
30
|
currentStep: number;
|
|
@@ -36,11 +38,13 @@ export interface StepFormActionsProps {
|
|
|
36
38
|
/** Standalone step nav bar — use as `renderBottomBar` in FormShell. */
|
|
37
39
|
export declare function StepFormActions({ currentStep, totalSteps, onNext, onPrev, completeLabel, isLoading, disabled, }: StepFormActionsProps): import("react/jsx-runtime").JSX.Element;
|
|
38
40
|
/** Step indicator bar shown above step content. */
|
|
39
|
-
export declare function StepIndicator({ steps, currentStep, onStepClick, }: {
|
|
41
|
+
export declare function StepIndicator({ steps, currentStep, onStepClick, stepErrors, }: {
|
|
40
42
|
steps: {
|
|
41
43
|
label: string;
|
|
42
44
|
}[];
|
|
43
45
|
currentStep: number;
|
|
44
46
|
onStepClick?: (index: number) => void;
|
|
47
|
+
/** True for each step that has a validation error — renders a red dot badge */
|
|
48
|
+
stepErrors?: boolean[];
|
|
45
49
|
}): import("react/jsx-runtime").JSX.Element;
|
|
46
|
-
export declare function StepForm<T extends object = Record<string, unknown>>({ steps, values, onChange, onComplete, completeLabel, isLoading, formId, currentStep, onStepChange, hideActions, }: StepFormProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
50
|
+
export declare function StepForm<T extends object = Record<string, unknown>>({ steps, values, onChange, onComplete, completeLabel, isLoading, formId, currentStep, onStepChange, hideActions, stepErrors, }: StepFormProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -13,25 +13,28 @@ export function StepFormActions({ currentStep, totalSteps, onNext, onPrev, compl
|
|
|
13
13
|
return (_jsxs("div", { className: "flex items-center justify-between gap-2 px-5 py-3 border-t border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", children: [_jsx(_Fragment, { children: !isFirst && onPrev && (_jsx(Button, { variant: "outline", size: "sm", onClick: onPrev, disabled: disabled || isLoading, children: "\u2190 Back" })) }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("span", { className: "text-xs text-[var(--appkit-color-text-muted)]", children: [currentStep + 1, " / ", totalSteps] }), _jsx(Button, { variant: "primary", size: "sm", onClick: onNext, disabled: disabled || isLoading, isLoading: isLoading && isLast, children: isLast ? completeLabel : nextStepLabel })] })] }));
|
|
14
14
|
}
|
|
15
15
|
/** Step indicator bar shown above step content. */
|
|
16
|
-
export function StepIndicator({ steps, currentStep, onStepClick, }) {
|
|
16
|
+
export function StepIndicator({ steps, currentStep, onStepClick, stepErrors, }) {
|
|
17
17
|
return (_jsx("nav", { "aria-label": "Form steps", className: "flex items-center gap-0 mb-6 overflow-x-auto", children: steps.map((step, i) => {
|
|
18
18
|
const isDone = i < currentStep;
|
|
19
19
|
const isActive = i === currentStep;
|
|
20
|
+
const hasError = stepErrors?.[i] === true;
|
|
20
21
|
return (_jsxs("div", { className: "flex items-center gap-0 flex-shrink-0", children: [_jsxs("button", { type: "button", disabled: !isDone && !isActive, onClick: () => isDone && onStepClick?.(i), className: classNames("flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors", isActive
|
|
21
22
|
? "font-semibold text-[var(--appkit-color-primary)]"
|
|
22
23
|
: isDone
|
|
23
24
|
? "text-[var(--appkit-color-text-muted)] cursor-pointer hover:text-[var(--appkit-color-text)]"
|
|
24
|
-
: "text-[var(--appkit-color-text-faint)] cursor-default"), children: [_jsx("span", { className: classNames("flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold border-2",
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
: "text-[var(--appkit-color-text-faint)] cursor-default"), children: [_jsxs("span", { className: "relative flex-shrink-0", children: [hasError && (_jsx("span", { className: "absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full bg-[var(--appkit-color-error)] z-10", "aria-label": "This step has errors" })), _jsx("span", { className: classNames("flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold border-2", hasError
|
|
26
|
+
? "border-[var(--appkit-color-error)] bg-[var(--appkit-color-error)] text-white"
|
|
27
|
+
: isActive
|
|
28
|
+
? "border-[var(--appkit-color-primary)] bg-[var(--appkit-color-primary)] text-white"
|
|
29
|
+
: isDone
|
|
30
|
+
? "border-[var(--appkit-color-primary)] bg-[var(--appkit-color-primary)] text-white"
|
|
31
|
+
: "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-faint)]"), children: isDone && !hasError ? _jsx(Check, { className: "w-3 h-3" }) : hasError ? "!" : i + 1 })] }), _jsx("span", { className: "hidden sm:inline", children: step.label })] }), i < steps.length - 1 && (_jsx("div", { className: classNames("w-6 h-px flex-shrink-0 mx-1", i < currentStep
|
|
29
32
|
? "bg-[var(--appkit-color-primary)]"
|
|
30
33
|
: "bg-[var(--appkit-color-border)]") }))] }, i));
|
|
31
34
|
}) }));
|
|
32
35
|
}
|
|
33
|
-
export function StepForm({ steps, values, onChange, onComplete, completeLabel = "Publish Now", isLoading = false, formId, currentStep, onStepChange, hideActions = false, }) {
|
|
34
|
-
const [
|
|
36
|
+
export function StepForm({ steps, values, onChange, onComplete, completeLabel = "Publish Now", isLoading = false, formId, currentStep, onStepChange, hideActions = false, stepErrors, }) {
|
|
37
|
+
const [fieldErrors, setFieldErrors] = useState({});
|
|
35
38
|
const [stepError, setStepError] = useState(null);
|
|
36
39
|
// Persist step to localStorage if formId provided
|
|
37
40
|
useEffect(() => {
|
|
@@ -75,8 +78,8 @@ export function StepForm({ steps, values, onChange, onComplete, completeLabel =
|
|
|
75
78
|
}, [currentStep, onStepChange]);
|
|
76
79
|
const currentStepDef = steps[currentStep];
|
|
77
80
|
const isLast = currentStep === steps.length - 1;
|
|
78
|
-
return (_jsxs("div", { className: "flex flex-col", children: [_jsx(StepIndicator, { steps: steps, currentStep: currentStep, onStepClick: (i) => { if (i < currentStep) {
|
|
81
|
+
return (_jsxs("div", { className: "flex flex-col", children: [_jsx(StepIndicator, { steps: steps, currentStep: currentStep, stepErrors: stepErrors, onStepClick: (i) => { if (i < currentStep) {
|
|
79
82
|
setStepError(null);
|
|
80
83
|
onStepChange(i);
|
|
81
|
-
} } }), _jsx("div", { className: "flex-1", children: currentStepDef?.render({ values, onChange, errors:
|
|
84
|
+
} } }), _jsx("div", { className: "flex-1", children: currentStepDef?.render({ values, onChange, errors: fieldErrors }) }), !hideActions && stepError && (_jsx(Text, { className: "mt-3 text-sm text-[var(--appkit-color-error)]", children: stepError })), !hideActions && (_jsx(StepFormActions, { currentStep: currentStep, totalSteps: steps.length, onNext: () => void goNext(), onPrev: currentStep > 0 ? goPrev : undefined, completeLabel: completeLabel, isLoading: isLoading && isLast, disabled: isLoading }))] }));
|
|
82
85
|
}
|