@voyantjs/storefront-ui 0.50.0

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.
@@ -0,0 +1,5 @@
1
+ export interface StorefrontSettingsPageProps {
2
+ className?: string;
3
+ }
4
+ export declare function StorefrontSettingsPage({ className }: StorefrontSettingsPageProps): import("react/jsx-runtime").JSX.Element;
5
+ //# sourceMappingURL=storefront-settings-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storefront-settings-page.d.ts","sourceRoot":"","sources":["../../src/components/storefront-settings-page.tsx"],"names":[],"mappings":"AA+BA,MAAM,WAAW,2BAA2B;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,sBAAsB,CAAC,EAAE,SAAS,EAAE,EAAE,2BAA2B,2CA0EhF"}
@@ -0,0 +1,39 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useAdminStorefrontSettings, useAdminStorefrontSettingsMutation, } from "@voyantjs/storefront-react";
4
+ import { cn } from "@voyantjs/ui/lib/utils";
5
+ import { useEffect, useMemo, useState } from "react";
6
+ import { emptyForm, hasEmptySettings, toFormState, toPayload, validateForm, } from "../internal/storefront-settings-form.js";
7
+ import { PaymentSection, StorefrontSettingsSaveButton, } from "../internal/storefront-settings-payment-section.js";
8
+ import { BrandingSection, LegalLocalizationSection, StorefrontSettingsErrorState, StorefrontSettingsLoadingSections, StorefrontSettingsSaveError, SupportSection, } from "../internal/storefront-settings-sections.js";
9
+ export function StorefrontSettingsPage({ className }) {
10
+ const settingsQuery = useAdminStorefrontSettings();
11
+ const mutation = useAdminStorefrontSettingsMutation();
12
+ const [form, setForm] = useState(emptyForm);
13
+ const [localError, setLocalError] = useState(null);
14
+ useEffect(() => {
15
+ if (settingsQuery.data?.data) {
16
+ setForm(toFormState(settingsQuery.data.data));
17
+ setLocalError(null);
18
+ }
19
+ }, [settingsQuery.data?.data]);
20
+ const isEmpty = useMemo(() => hasEmptySettings(settingsQuery.data?.data), [settingsQuery.data]);
21
+ const isSaving = mutation.isPending;
22
+ const setField = (key, value) => {
23
+ setForm((prev) => ({ ...prev, [key]: value }));
24
+ };
25
+ const updateSupportLink = (rowKey, patch) => {
26
+ setForm((prev) => ({
27
+ ...prev,
28
+ supportLinks: prev.supportLinks.map((link) => link.rowKey === rowKey ? { ...link, ...patch } : link),
29
+ }));
30
+ };
31
+ const save = async () => {
32
+ const validationError = validateForm(form);
33
+ setLocalError(validationError);
34
+ if (validationError)
35
+ return;
36
+ await mutation.mutateAsync(toPayload(form));
37
+ };
38
+ return (_jsxs("div", { "data-slot": "storefront-settings-page", className: cn("flex flex-col gap-6 p-6", className), children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold tracking-tight", children: "Storefront settings" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Manage customer-facing branding, support, legal, localization, and payment defaults." })] }), settingsQuery.isLoading ? (_jsx(StorefrontSettingsLoadingSections, {})) : settingsQuery.isError ? (_jsx(StorefrontSettingsErrorState, { error: settingsQuery.error, refetch: () => void settingsQuery.refetch() })) : (_jsxs(_Fragment, { children: [isEmpty ? (_jsx("p", { className: "rounded-md border border-dashed p-4 text-sm text-muted-foreground", children: "No storefront settings have been saved yet." })) : null, _jsxs("div", { className: "grid grid-cols-1 gap-4 xl:grid-cols-2", children: [_jsx(BrandingSection, { form: form, setField: setField }), _jsx(SupportSection, { form: form, setField: setField, updateSupportLink: updateSupportLink }), _jsx(LegalLocalizationSection, { form: form, setField: setField }), _jsx(PaymentSection, { form: form, setField: setField })] }), _jsx(StorefrontSettingsSaveError, { localError: localError, mutationError: mutation.error }), _jsx(StorefrontSettingsSaveButton, { isSaving: isSaving, save: () => void save() })] }))] }));
39
+ }
@@ -0,0 +1,2 @@
1
+ export { StorefrontSettingsPage, type StorefrontSettingsPageProps, } from "./components/storefront-settings-page.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,KAAK,2BAA2B,GACjC,MAAM,0CAA0C,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { StorefrontSettingsPage, } from "./components/storefront-settings-page.js";
@@ -0,0 +1,46 @@
1
+ import type { StorefrontSettingsPatchInput, StorefrontSettingsRecord } from "@voyantjs/storefront-react";
2
+ export type SupportLinkRow = {
3
+ rowKey: string;
4
+ label: string;
5
+ url: string;
6
+ };
7
+ export type PaymentMethodCode = NonNullable<StorefrontSettingsRecord["payment"]["defaultMethod"]>;
8
+ export type FormState = {
9
+ logoUrl: string;
10
+ faviconUrl: string;
11
+ brandMarkUrl: string;
12
+ primaryColor: string;
13
+ accentColor: string;
14
+ supportedLanguages: string;
15
+ supportEmail: string;
16
+ supportPhone: string;
17
+ supportLinks: SupportLinkRow[];
18
+ termsUrl: string;
19
+ privacyUrl: string;
20
+ cancellationUrl: string;
21
+ defaultContractTemplateId: string;
22
+ defaultLocale: string;
23
+ currencyDisplay: StorefrontSettingsRecord["localization"]["currencyDisplay"];
24
+ defaultMethod: PaymentMethodCode | "none";
25
+ enabledMethods: Record<PaymentMethodCode, boolean>;
26
+ depositPercent: string;
27
+ balanceDueDaysBeforeDeparture: string;
28
+ accountHolder: string;
29
+ bankName: string;
30
+ iban: string;
31
+ bic: string;
32
+ paymentReference: string;
33
+ bankInstructions: string;
34
+ };
35
+ export declare const paymentMethods: Array<{
36
+ code: PaymentMethodCode;
37
+ label: string;
38
+ }>;
39
+ export declare const loadingSectionKeys: readonly ["branding", "support", "legal", "payment"];
40
+ export declare const nextSupportLinkKey: () => string;
41
+ export declare const emptyForm: FormState;
42
+ export declare function toFormState(settings?: StorefrontSettingsRecord): FormState;
43
+ export declare function validateForm(form: FormState): "URLs must be valid http or https links." | "Brand colors must use #RGB or #RRGGBB format." | "Deposit percent must be between 0 and 100." | "Balance due days must be a whole number greater than or equal to 0." | "The default payment method must be enabled." | null;
44
+ export declare function toPayload(form: FormState): StorefrontSettingsPatchInput;
45
+ export declare function hasEmptySettings(settings?: StorefrontSettingsRecord): boolean;
46
+ //# sourceMappingURL=storefront-settings-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storefront-settings-form.d.ts","sourceRoot":"","sources":["../../src/internal/storefront-settings-form.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,4BAA4B,EAC5B,wBAAwB,EACzB,MAAM,4BAA4B,CAAA;AAEnC,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,WAAW,CAAC,wBAAwB,CAAC,SAAS,CAAC,CAAC,eAAe,CAAC,CAAC,CAAA;AAEjG,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,cAAc,EAAE,CAAA;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,eAAe,EAAE,MAAM,CAAA;IACvB,yBAAyB,EAAE,MAAM,CAAA;IACjC,aAAa,EAAE,MAAM,CAAA;IACrB,eAAe,EAAE,wBAAwB,CAAC,cAAc,CAAC,CAAC,iBAAiB,CAAC,CAAA;IAC5E,aAAa,EAAE,iBAAiB,GAAG,MAAM,CAAA;IACzC,cAAc,EAAE,MAAM,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAA;IAClD,cAAc,EAAE,MAAM,CAAA;IACtB,6BAA6B,EAAE,MAAM,CAAA;IACrC,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,gBAAgB,EAAE,MAAM,CAAA;IACxB,gBAAgB,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAM5E,CAAA;AAED,eAAO,MAAM,kBAAkB,sDAAuD,CAAA;AAGtF,eAAO,MAAM,kBAAkB,cAA2C,CAAA;AAE1E,eAAO,MAAM,SAAS,EAAE,SAgCvB,CAAA;AAqBD,wBAAgB,WAAW,CAAC,QAAQ,CAAC,EAAE,wBAAwB,GAAG,SAAS,CAwC1E;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,6QAmC3C;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,GAAG,4BAA4B,CAgEvE;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,CAAC,EAAE,wBAAwB,WAUnE"}
@@ -0,0 +1,202 @@
1
+ export const paymentMethods = [
2
+ { code: "card", label: "Card" },
3
+ { code: "bank_transfer", label: "Bank transfer" },
4
+ { code: "cash", label: "Cash" },
5
+ { code: "voucher", label: "Voucher" },
6
+ { code: "invoice", label: "Invoice" },
7
+ ];
8
+ export const loadingSectionKeys = ["branding", "support", "legal", "payment"];
9
+ let supportLinkSeq = 0;
10
+ export const nextSupportLinkKey = () => `support-link-${++supportLinkSeq}`;
11
+ export const emptyForm = {
12
+ logoUrl: "",
13
+ faviconUrl: "",
14
+ brandMarkUrl: "",
15
+ primaryColor: "",
16
+ accentColor: "",
17
+ supportedLanguages: "",
18
+ supportEmail: "",
19
+ supportPhone: "",
20
+ supportLinks: [],
21
+ termsUrl: "",
22
+ privacyUrl: "",
23
+ cancellationUrl: "",
24
+ defaultContractTemplateId: "",
25
+ defaultLocale: "",
26
+ currencyDisplay: "code",
27
+ defaultMethod: "none",
28
+ enabledMethods: {
29
+ card: false,
30
+ bank_transfer: false,
31
+ cash: false,
32
+ voucher: false,
33
+ invoice: false,
34
+ },
35
+ depositPercent: "",
36
+ balanceDueDaysBeforeDeparture: "",
37
+ accountHolder: "",
38
+ bankName: "",
39
+ iban: "",
40
+ bic: "",
41
+ paymentReference: "",
42
+ bankInstructions: "",
43
+ };
44
+ function optional(value) {
45
+ const trimmed = value.trim();
46
+ return trimmed ? trimmed : null;
47
+ }
48
+ function urlLooksValid(value) {
49
+ if (!value.trim())
50
+ return true;
51
+ try {
52
+ const url = new URL(value);
53
+ return url.protocol === "http:" || url.protocol === "https:";
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ function colorLooksValid(value) {
60
+ return !value.trim() || /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value.trim());
61
+ }
62
+ export function toFormState(settings) {
63
+ if (!settings)
64
+ return emptyForm;
65
+ return {
66
+ logoUrl: settings.branding.logoUrl ?? "",
67
+ faviconUrl: settings.branding.faviconUrl ?? "",
68
+ brandMarkUrl: settings.branding.brandMarkUrl ?? "",
69
+ primaryColor: settings.branding.primaryColor ?? "",
70
+ accentColor: settings.branding.accentColor ?? "",
71
+ supportedLanguages: settings.branding.supportedLanguages.join(", "),
72
+ supportEmail: settings.support.email ?? "",
73
+ supportPhone: settings.support.phone ?? "",
74
+ supportLinks: settings.support.links.map((link) => ({
75
+ rowKey: nextSupportLinkKey(),
76
+ label: link.label,
77
+ url: link.url,
78
+ })),
79
+ termsUrl: settings.legal.termsUrl ?? "",
80
+ privacyUrl: settings.legal.privacyUrl ?? "",
81
+ cancellationUrl: settings.legal.cancellationUrl ?? "",
82
+ defaultContractTemplateId: settings.legal.defaultContractTemplateId ?? "",
83
+ defaultLocale: settings.localization.defaultLocale ?? "",
84
+ currencyDisplay: settings.localization.currencyDisplay,
85
+ defaultMethod: settings.payment.defaultMethod ?? "none",
86
+ enabledMethods: Object.fromEntries(paymentMethods.map((method) => [
87
+ method.code,
88
+ settings.payment.methods.some((stored) => stored.code === method.code && stored.enabled),
89
+ ])),
90
+ depositPercent: settings.payment.defaultSchedule?.depositPercent?.toString() ?? "",
91
+ balanceDueDaysBeforeDeparture: settings.payment.defaultSchedule?.balanceDueDaysBeforeDeparture?.toString() ?? "",
92
+ accountHolder: settings.payment.bankTransfer?.accountHolder ?? "",
93
+ bankName: settings.payment.bankTransfer?.bankName ?? "",
94
+ iban: settings.payment.bankTransfer?.iban ?? "",
95
+ bic: settings.payment.bankTransfer?.bic ?? "",
96
+ paymentReference: settings.payment.bankTransfer?.paymentReference ?? "",
97
+ bankInstructions: settings.payment.bankTransfer?.instructions ?? "",
98
+ };
99
+ }
100
+ export function validateForm(form) {
101
+ const urls = [
102
+ form.logoUrl,
103
+ form.faviconUrl,
104
+ form.brandMarkUrl,
105
+ form.termsUrl,
106
+ form.privacyUrl,
107
+ form.cancellationUrl,
108
+ ...form.supportLinks.map((link) => link.url),
109
+ ];
110
+ if (urls.some((url) => !urlLooksValid(url))) {
111
+ return "URLs must be valid http or https links.";
112
+ }
113
+ if (!colorLooksValid(form.primaryColor) || !colorLooksValid(form.accentColor)) {
114
+ return "Brand colors must use #RGB or #RRGGBB format.";
115
+ }
116
+ const deposit = form.depositPercent ? Number(form.depositPercent) : null;
117
+ if (deposit !== null && (!Number.isFinite(deposit) || deposit < 0 || deposit > 100)) {
118
+ return "Deposit percent must be between 0 and 100.";
119
+ }
120
+ const balanceDue = form.balanceDueDaysBeforeDeparture
121
+ ? Number(form.balanceDueDaysBeforeDeparture)
122
+ : null;
123
+ if (balanceDue !== null && (!Number.isInteger(balanceDue) || balanceDue < 0)) {
124
+ return "Balance due days must be a whole number greater than or equal to 0.";
125
+ }
126
+ if (form.defaultMethod !== "none" && !form.enabledMethods[form.defaultMethod]) {
127
+ return "The default payment method must be enabled.";
128
+ }
129
+ return null;
130
+ }
131
+ export function toPayload(form) {
132
+ const supportLinks = form.supportLinks
133
+ .map((link) => ({ label: link.label.trim(), url: link.url.trim() }))
134
+ .filter((link) => link.label || link.url);
135
+ return {
136
+ branding: {
137
+ logoUrl: optional(form.logoUrl),
138
+ faviconUrl: optional(form.faviconUrl),
139
+ brandMarkUrl: optional(form.brandMarkUrl),
140
+ primaryColor: optional(form.primaryColor),
141
+ accentColor: optional(form.accentColor),
142
+ supportedLanguages: form.supportedLanguages
143
+ .split(",")
144
+ .map((value) => value.trim())
145
+ .filter(Boolean),
146
+ },
147
+ support: {
148
+ email: optional(form.supportEmail),
149
+ phone: optional(form.supportPhone),
150
+ links: supportLinks,
151
+ },
152
+ legal: {
153
+ termsUrl: optional(form.termsUrl),
154
+ privacyUrl: optional(form.privacyUrl),
155
+ cancellationUrl: optional(form.cancellationUrl),
156
+ defaultContractTemplateId: optional(form.defaultContractTemplateId),
157
+ },
158
+ localization: {
159
+ defaultLocale: optional(form.defaultLocale),
160
+ currencyDisplay: form.currencyDisplay,
161
+ },
162
+ payment: {
163
+ defaultMethod: form.defaultMethod === "none" ? null : form.defaultMethod,
164
+ methods: paymentMethods
165
+ .filter((method) => form.enabledMethods[method.code])
166
+ .map((method) => ({ code: method.code })),
167
+ defaultSchedule: form.depositPercent || form.balanceDueDaysBeforeDeparture
168
+ ? {
169
+ depositPercent: form.depositPercent ? Number(form.depositPercent) : null,
170
+ balanceDueDaysBeforeDeparture: form.balanceDueDaysBeforeDeparture
171
+ ? Number(form.balanceDueDaysBeforeDeparture)
172
+ : null,
173
+ }
174
+ : null,
175
+ bankTransfer: form.accountHolder ||
176
+ form.bankName ||
177
+ form.iban ||
178
+ form.bic ||
179
+ form.paymentReference ||
180
+ form.bankInstructions
181
+ ? {
182
+ accountHolder: optional(form.accountHolder),
183
+ bankName: optional(form.bankName),
184
+ iban: optional(form.iban),
185
+ bic: optional(form.bic),
186
+ paymentReference: optional(form.paymentReference),
187
+ instructions: optional(form.bankInstructions),
188
+ }
189
+ : null,
190
+ },
191
+ };
192
+ }
193
+ export function hasEmptySettings(settings) {
194
+ if (!settings)
195
+ return true;
196
+ return (!settings.branding.logoUrl &&
197
+ !settings.support.email &&
198
+ !settings.support.phone &&
199
+ !settings.legal.termsUrl &&
200
+ !settings.payment.defaultMethod &&
201
+ settings.payment.methods.length === 0);
202
+ }
@@ -0,0 +1,12 @@
1
+ import { type FormState } from "./storefront-settings-form.js";
2
+ type SetField = <K extends keyof FormState>(key: K, value: FormState[K]) => void;
3
+ export declare function PaymentSection({ form, setField }: {
4
+ form: FormState;
5
+ setField: SetField;
6
+ }): import("react/jsx-runtime").JSX.Element;
7
+ export declare function StorefrontSettingsSaveButton({ isSaving, save, }: {
8
+ isSaving: boolean;
9
+ save: () => void;
10
+ }): import("react/jsx-runtime").JSX.Element;
11
+ export {};
12
+ //# sourceMappingURL=storefront-settings-payment-section.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storefront-settings-payment-section.d.ts","sourceRoot":"","sources":["../../src/internal/storefront-settings-payment-section.tsx"],"names":[],"mappings":"AAcA,OAAO,EAAE,KAAK,SAAS,EAAkB,MAAM,+BAA+B,CAAA;AAE9E,KAAK,QAAQ,GAAG,CAAC,CAAC,SAAS,MAAM,SAAS,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;AAEhF,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,2CAmIzF;AAED,wBAAgB,4BAA4B,CAAC,EAC3C,QAAQ,EACR,IAAI,GACL,EAAE;IACD,QAAQ,EAAE,OAAO,CAAA;IACjB,IAAI,EAAE,MAAM,IAAI,CAAA;CACjB,2CASA"}
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Checkbox, Input, Textarea, } from "@voyantjs/ui/components";
3
+ import { Field, FieldGroup, FieldLabel, FieldLegend, FieldSet } from "@voyantjs/ui/components/field";
4
+ import { Loader2, Save } from "lucide-react";
5
+ import { paymentMethods } from "./storefront-settings-form.js";
6
+ export function PaymentSection({ form, setField }) {
7
+ return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "Payment defaults" }), _jsx(CardDescription, { children: "Default payment methods, schedules, and bank details." })] }), _jsx(CardContent, { children: _jsxs(FieldGroup, { children: [_jsxs(FieldSet, { children: [_jsx(FieldLegend, { children: "Payment methods" }), _jsx("div", { className: "grid grid-cols-1 gap-2 md:grid-cols-2", children: paymentMethods.map((method) => (_jsxs(Field, { orientation: "horizontal", children: [_jsx(Checkbox, { id: `storefront-payment-${method.code}`, checked: form.enabledMethods[method.code], onCheckedChange: (checked) => setField("enabledMethods", {
8
+ ...form.enabledMethods,
9
+ [method.code]: checked === true,
10
+ }) }), _jsx(FieldLabel, { htmlFor: `storefront-payment-${method.code}`, children: method.label })] }, method.code))) })] }), _jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-3", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-default-method", children: "Default method" }), _jsxs("select", { id: "storefront-default-method", className: "h-9 rounded-md border bg-background px-3 text-sm", value: form.defaultMethod, onChange: (event) => setField("defaultMethod", event.target.value), children: [_jsx("option", { value: "none", children: "None" }), paymentMethods.map((method) => (_jsx("option", { value: method.code, children: method.label }, method.code)))] })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-deposit-percent", children: "Deposit percent" }), _jsx(Input, { id: "storefront-deposit-percent", type: "number", min: 0, max: 100, value: form.depositPercent, onChange: (event) => setField("depositPercent", event.target.value) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-balance-due", children: "Balance due days" }), _jsx(Input, { id: "storefront-balance-due", type: "number", min: 0, value: form.balanceDueDaysBeforeDeparture, onChange: (event) => setField("balanceDueDaysBeforeDeparture", event.target.value) })] })] }), _jsxs(FieldSet, { children: [_jsx(FieldLegend, { children: "Bank transfer details" }), _jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-account-holder", children: "Account holder" }), _jsx(Input, { id: "storefront-account-holder", value: form.accountHolder, onChange: (event) => setField("accountHolder", event.target.value) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-bank-name", children: "Bank name" }), _jsx(Input, { id: "storefront-bank-name", value: form.bankName, onChange: (event) => setField("bankName", event.target.value) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-iban", children: "IBAN or account number" }), _jsx(Input, { id: "storefront-iban", value: form.iban, onChange: (event) => setField("iban", event.target.value) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-bic", children: "BIC or routing code" }), _jsx(Input, { id: "storefront-bic", value: form.bic, onChange: (event) => setField("bic", event.target.value) })] })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-payment-reference", children: "Payment reference" }), _jsx(Input, { id: "storefront-payment-reference", value: form.paymentReference, onChange: (event) => setField("paymentReference", event.target.value) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-bank-instructions", children: "Instructions" }), _jsx(Textarea, { id: "storefront-bank-instructions", value: form.bankInstructions, onChange: (event) => setField("bankInstructions", event.target.value) })] })] })] }) })] }));
11
+ }
12
+ export function StorefrontSettingsSaveButton({ isSaving, save, }) {
13
+ return (_jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", onClick: save, disabled: isSaving, children: [isSaving ? _jsx(Loader2, { className: "size-4 animate-spin" }) : _jsx(Save, { className: "size-4" }), "Save settings"] }) }));
14
+ }
@@ -0,0 +1,23 @@
1
+ import { type FormState, type SupportLinkRow } from "./storefront-settings-form.js";
2
+ type SetField = <K extends keyof FormState>(key: K, value: FormState[K]) => void;
3
+ interface SettingsSectionProps {
4
+ form: FormState;
5
+ setField: SetField;
6
+ }
7
+ interface SupportSectionProps extends SettingsSectionProps {
8
+ updateSupportLink: (rowKey: string, patch: Partial<SupportLinkRow>) => void;
9
+ }
10
+ export declare function StorefrontSettingsLoadingSections(): import("react/jsx-runtime").JSX.Element;
11
+ export declare function StorefrontSettingsErrorState({ error, refetch, }: {
12
+ error: unknown;
13
+ refetch: () => void;
14
+ }): import("react/jsx-runtime").JSX.Element;
15
+ export declare function BrandingSection({ form, setField }: SettingsSectionProps): import("react/jsx-runtime").JSX.Element;
16
+ export declare function SupportSection({ form, setField, updateSupportLink }: SupportSectionProps): import("react/jsx-runtime").JSX.Element;
17
+ export declare function LegalLocalizationSection({ form, setField }: SettingsSectionProps): import("react/jsx-runtime").JSX.Element;
18
+ export declare function StorefrontSettingsSaveError({ localError, mutationError, }: {
19
+ localError: string | null;
20
+ mutationError: unknown;
21
+ }): import("react/jsx-runtime").JSX.Element | null;
22
+ export {};
23
+ //# sourceMappingURL=storefront-settings-sections.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storefront-settings-sections.d.ts","sourceRoot":"","sources":["../../src/internal/storefront-settings-sections.tsx"],"names":[],"mappings":"AAqBA,OAAO,EACL,KAAK,SAAS,EAGd,KAAK,cAAc,EACpB,MAAM,+BAA+B,CAAA;AAEtC,KAAK,QAAQ,GAAG,CAAC,CAAC,SAAS,MAAM,SAAS,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;AAEhF,UAAU,oBAAoB;IAC5B,IAAI,EAAE,SAAS,CAAA;IACf,QAAQ,EAAE,QAAQ,CAAA;CACnB;AAED,UAAU,mBAAoB,SAAQ,oBAAoB;IACxD,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,cAAc,CAAC,KAAK,IAAI,CAAA;CAC5E;AAED,wBAAgB,iCAAiC,4CAkBhD;AAED,wBAAgB,4BAA4B,CAAC,EAC3C,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,OAAO,CAAA;IACd,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,2CAmBA;AAED,wBAAgB,eAAe,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,oBAAoB,2CAoEvE;AAED,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,iBAAiB,EAAE,EAAE,mBAAmB,2CAwFxF;AAED,wBAAgB,wBAAwB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,oBAAoB,2CAuEhF;AAED,wBAAgB,2BAA2B,CAAC,EAC1C,UAAU,EACV,aAAa,GACd,EAAE;IACD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,aAAa,EAAE,OAAO,CAAA;CACvB,kDAWA"}
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Input, } from "@voyantjs/ui/components";
3
+ import { Field, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet, } from "@voyantjs/ui/components/field";
4
+ import { Skeleton } from "@voyantjs/ui/components/skeleton";
5
+ import { AlertCircle, Plus, Trash2 } from "lucide-react";
6
+ import { loadingSectionKeys, nextSupportLinkKey, } from "./storefront-settings-form.js";
7
+ export function StorefrontSettingsLoadingSections() {
8
+ return (_jsx("div", { className: "grid grid-cols-1 gap-4 xl:grid-cols-2", children: loadingSectionKeys.map((key) => (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(Skeleton, { className: "h-5 w-40" }), _jsx(Skeleton, { className: "h-4 w-64" })] }), _jsxs(CardContent, { className: "space-y-3", children: [_jsx(Skeleton, { className: "h-9 w-full" }), _jsx(Skeleton, { className: "h-9 w-full" }), _jsx(Skeleton, { className: "h-9 w-2/3" })] })] }, key))) }));
9
+ }
10
+ export function StorefrontSettingsErrorState({ error, refetch, }) {
11
+ return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs(CardTitle, { className: "flex items-center gap-2", children: [_jsx(AlertCircle, { className: "size-5 text-destructive" }), "Could not load settings"] }), _jsx(CardDescription, { children: error instanceof Error ? error.message : "The storefront settings request failed." })] }), _jsx(CardFooter, { children: _jsx(Button, { type: "button", variant: "outline", onClick: refetch, children: "Try again" }) })] }));
12
+ }
13
+ export function BrandingSection({ form, setField }) {
14
+ return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "Branding" }), _jsx(CardDescription, { children: "Customer-facing assets and brand color tokens." })] }), _jsx(CardContent, { children: _jsxs(FieldGroup, { children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-logo-url", children: "Logo URL" }), _jsx(Input, { id: "storefront-logo-url", value: form.logoUrl, onChange: (event) => setField("logoUrl", event.target.value), placeholder: "https://cdn.example.com/logo.svg" })] }), _jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-favicon-url", children: "Favicon URL" }), _jsx(Input, { id: "storefront-favicon-url", value: form.faviconUrl, onChange: (event) => setField("faviconUrl", event.target.value) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-brand-mark-url", children: "Brand mark URL" }), _jsx(Input, { id: "storefront-brand-mark-url", value: form.brandMarkUrl, onChange: (event) => setField("brandMarkUrl", event.target.value) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-primary-color", children: "Primary color" }), _jsx(Input, { id: "storefront-primary-color", value: form.primaryColor, onChange: (event) => setField("primaryColor", event.target.value), placeholder: "#0f766e" })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-accent-color", children: "Accent color" }), _jsx(Input, { id: "storefront-accent-color", value: form.accentColor, onChange: (event) => setField("accentColor", event.target.value), placeholder: "#f59e0b" })] })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-supported-languages", children: "Supported languages" }), _jsx(Input, { id: "storefront-supported-languages", value: form.supportedLanguages, onChange: (event) => setField("supportedLanguages", event.target.value), placeholder: "en, ro, fr-FR" }), _jsx(FieldDescription, { children: "Use comma-separated BCP 47 language tags." })] })] }) })] }));
15
+ }
16
+ export function SupportSection({ form, setField, updateSupportLink }) {
17
+ return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "Support" }), _jsx(CardDescription, { children: "Contact channels shown to storefront customers." })] }), _jsx(CardContent, { children: _jsxs(FieldGroup, { children: [_jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-support-email", children: "Email" }), _jsx(Input, { id: "storefront-support-email", type: "email", value: form.supportEmail, onChange: (event) => setField("supportEmail", event.target.value) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-support-phone", children: "Phone" }), _jsx(Input, { id: "storefront-support-phone", value: form.supportPhone, onChange: (event) => setField("supportPhone", event.target.value) })] })] }), _jsxs(FieldSet, { children: [_jsx(FieldLegend, { children: "Contact links" }), _jsxs("div", { className: "space-y-2", children: [form.supportLinks.map((link) => (_jsxs("div", { className: "grid grid-cols-1 gap-2 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]", children: [_jsx(Input, { value: link.label, onChange: (event) => updateSupportLink(link.rowKey, { label: event.target.value }), placeholder: "WhatsApp", "aria-label": "Contact link label" }), _jsx(Input, { value: link.url, onChange: (event) => updateSupportLink(link.rowKey, { url: event.target.value }), placeholder: "https://example.com/contact", "aria-label": "Contact link URL" }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => setField("supportLinks", form.supportLinks.filter((row) => row.rowKey !== link.rowKey)), "aria-label": "Remove contact link", children: _jsx(Trash2, { className: "size-4" }) })] }, link.rowKey))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setField("supportLinks", [
18
+ ...form.supportLinks,
19
+ { rowKey: nextSupportLinkKey(), label: "", url: "" },
20
+ ]), children: [_jsx(Plus, { className: "size-4" }), "Add link"] })] })] })] }) })] }));
21
+ }
22
+ export function LegalLocalizationSection({ form, setField }) {
23
+ return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "Legal and localization" }), _jsx(CardDescription, { children: "Policy links and default display preferences." })] }), _jsx(CardContent, { children: _jsx(FieldGroup, { children: _jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-terms-url", children: "Terms URL" }), _jsx(Input, { id: "storefront-terms-url", value: form.termsUrl, onChange: (event) => setField("termsUrl", event.target.value) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-privacy-url", children: "Privacy URL" }), _jsx(Input, { id: "storefront-privacy-url", value: form.privacyUrl, onChange: (event) => setField("privacyUrl", event.target.value) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-cancellation-url", children: "Cancellation URL" }), _jsx(Input, { id: "storefront-cancellation-url", value: form.cancellationUrl, onChange: (event) => setField("cancellationUrl", event.target.value) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-contract-template", children: "Contract template ID" }), _jsx(Input, { id: "storefront-contract-template", value: form.defaultContractTemplateId, onChange: (event) => setField("defaultContractTemplateId", event.target.value) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-default-locale", children: "Default locale" }), _jsx(Input, { id: "storefront-default-locale", value: form.defaultLocale, onChange: (event) => setField("defaultLocale", event.target.value), placeholder: "en-US" })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "storefront-currency-display", children: "Currency display" }), _jsxs("select", { id: "storefront-currency-display", className: "h-9 rounded-md border bg-background px-3 text-sm", value: form.currencyDisplay, onChange: (event) => setField("currencyDisplay", event.target.value), children: [_jsx("option", { value: "code", children: "Code" }), _jsx("option", { value: "symbol", children: "Symbol" }), _jsx("option", { value: "name", children: "Name" })] })] })] }) }) })] }));
24
+ }
25
+ export function StorefrontSettingsSaveError({ localError, mutationError, }) {
26
+ if (!localError && !mutationError)
27
+ return null;
28
+ return (_jsx("p", { className: "rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive", children: localError ??
29
+ (mutationError instanceof Error
30
+ ? mutationError.message
31
+ : "Failed to save storefront settings.") }));
32
+ }
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@voyantjs/storefront-ui",
3
+ "version": "0.50.0",
4
+ "license": "Apache-2.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/voyantjs/voyant.git",
8
+ "directory": "packages/storefront-ui"
9
+ },
10
+ "type": "module",
11
+ "sideEffects": false,
12
+ "exports": {
13
+ ".": "./src/index.ts",
14
+ "./styles.css": "./src/styles.css",
15
+ "./components/storefront-settings-page": "./src/components/storefront-settings-page.tsx"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.build.json",
19
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
20
+ "prepack": "pnpm run build",
21
+ "typecheck": "tsc --noEmit",
22
+ "lint": "biome check src/",
23
+ "test": "vitest run --passWithNoTests"
24
+ },
25
+ "peerDependencies": {
26
+ "@tanstack/react-query": "^5.0.0",
27
+ "@voyantjs/storefront-react": "workspace:*",
28
+ "@voyantjs/ui": "workspace:*",
29
+ "lucide-react": "^0.475.0",
30
+ "react": "^19.0.0",
31
+ "react-dom": "^19.0.0",
32
+ "zod": "^4.3.6"
33
+ },
34
+ "devDependencies": {
35
+ "@tanstack/react-query": "^5.96.2",
36
+ "@types/react": "^19.2.14",
37
+ "@types/react-dom": "^19.2.3",
38
+ "@voyantjs/storefront-react": "workspace:*",
39
+ "@voyantjs/ui": "workspace:*",
40
+ "@voyantjs/voyant-typescript-config": "workspace:*",
41
+ "lucide-react": "^1.7.0",
42
+ "react": "^19.2.4",
43
+ "react-dom": "^19.2.4",
44
+ "typescript": "^6.0.2",
45
+ "vitest": "^4.1.2",
46
+ "zod": "^4.3.6"
47
+ },
48
+ "files": [
49
+ "dist",
50
+ "src/styles.css"
51
+ ],
52
+ "publishConfig": {
53
+ "access": "public",
54
+ "exports": {
55
+ ".": {
56
+ "types": "./dist/index.d.ts",
57
+ "import": "./dist/index.js",
58
+ "default": "./dist/index.js"
59
+ },
60
+ "./styles.css": {
61
+ "default": "./src/styles.css"
62
+ },
63
+ "./components/storefront-settings-page": {
64
+ "types": "./dist/components/storefront-settings-page.d.ts",
65
+ "import": "./dist/components/storefront-settings-page.js",
66
+ "default": "./dist/components/storefront-settings-page.js"
67
+ }
68
+ },
69
+ "main": "./dist/index.js",
70
+ "types": "./dist/index.d.ts"
71
+ }
72
+ }
package/src/styles.css ADDED
@@ -0,0 +1 @@
1
+ @source "./**/*.{ts,tsx}";