@asteby/storefront-react 0.24.0 → 0.26.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.
Files changed (48) hide show
  1. package/README.md +35 -2
  2. package/dist/account-server.cjs +128 -19
  3. package/dist/account-server.cjs.map +1 -1
  4. package/dist/account-server.d.cts +67 -4
  5. package/dist/account-server.d.ts +67 -4
  6. package/dist/account-server.js +125 -20
  7. package/dist/account-server.js.map +1 -1
  8. package/dist/account.cjs +261 -8
  9. package/dist/account.cjs.map +1 -1
  10. package/dist/account.css +99 -0
  11. package/dist/account.d.cts +132 -6
  12. package/dist/account.d.ts +132 -6
  13. package/dist/account.js +255 -9
  14. package/dist/account.js.map +1 -1
  15. package/dist/checkout.cjs +162 -8
  16. package/dist/checkout.cjs.map +1 -1
  17. package/dist/checkout.d.cts +104 -4
  18. package/dist/checkout.d.ts +104 -4
  19. package/dist/checkout.js +158 -10
  20. package/dist/checkout.js.map +1 -1
  21. package/dist/chunk-Z4C4LKXF.js +25 -0
  22. package/dist/chunk-Z4C4LKXF.js.map +1 -0
  23. package/dist/fit-key-B9sYQVwG.d.cts +49 -0
  24. package/dist/fit-key-B9sYQVwG.d.ts +49 -0
  25. package/dist/fitment.cjs +54 -9
  26. package/dist/fitment.cjs.map +1 -1
  27. package/dist/fitment.d.cts +20 -2
  28. package/dist/fitment.d.ts +20 -2
  29. package/dist/fitment.js +54 -10
  30. package/dist/fitment.js.map +1 -1
  31. package/dist/settings-types-jxFQFFqP.d.cts +77 -0
  32. package/dist/settings-types-jxFQFFqP.d.ts +77 -0
  33. package/dist/subjects-server.cjs +248 -0
  34. package/dist/subjects-server.cjs.map +1 -0
  35. package/dist/subjects-server.d.cts +130 -0
  36. package/dist/subjects-server.d.ts +130 -0
  37. package/dist/subjects-server.js +217 -0
  38. package/dist/subjects-server.js.map +1 -0
  39. package/dist/subjects.cjs +501 -0
  40. package/dist/subjects.cjs.map +1 -0
  41. package/dist/subjects.css +166 -0
  42. package/dist/subjects.d.cts +178 -0
  43. package/dist/subjects.d.ts +178 -0
  44. package/dist/subjects.js +465 -0
  45. package/dist/subjects.js.map +1 -0
  46. package/package.json +23 -2
  47. package/dist/types-D7pVK0GK.d.cts +0 -39
  48. package/dist/types-D7pVK0GK.d.ts +0 -39
@@ -0,0 +1,178 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { SubjectKind, Subject, SubjectValues, SubjectInput, SubjectPatch, StorefrontClient } from '@asteby/storefront';
4
+ import { a as SubjectErrorCode, S as SubjectActionData } from './fit-key-B9sYQVwG.js';
5
+ export { b as SubjectIntent, s as subjectFromFitKey } from './fit-key-B9sYQVwG.js';
6
+
7
+ /** Texts of the «My items» components. Spanish (es-MX) by default, English with `en*`, any locale with `messages`. */
8
+
9
+ type DeepPartial<T> = {
10
+ [K in keyof T]?: T[K] extends Record<string, unknown> ? DeepPartial<T[K]> : T[K];
11
+ };
12
+ /**
13
+ * `{kind}` is the singular name the store gave the kind, lowercased («vehículo»); `{name}` the label of an item. Nothing
14
+ * here names a kind: the words come from the data the organization publishes.
15
+ */
16
+ type SubjectMessages = {
17
+ garage: {
18
+ empty: string;
19
+ add: string;
20
+ active: string;
21
+ makeActive: string;
22
+ edit: string;
23
+ remove: string;
24
+ removeConfirm: string;
25
+ removeHint: string;
26
+ viewParts: string;
27
+ limit: string;
28
+ loadError: string;
29
+ retry: string;
30
+ saved: string;
31
+ removed: string;
32
+ privateLabel: string;
33
+ };
34
+ picker: {
35
+ choose: string;
36
+ chooseFirst: string;
37
+ nickname: string;
38
+ nicknameHint: string;
39
+ privateNote: string;
40
+ privateKeep: string;
41
+ privateClear: string;
42
+ makeActive: string;
43
+ save: string;
44
+ saving: string;
45
+ cancel: string;
46
+ refine: string;
47
+ errors: Record<'required' | 'unknown' | 'invalid' | 'not_in_catalog' | 'year_range' | 'catalog_unavailable', string> & Record<SubjectErrorCode, string>;
48
+ };
49
+ filter: {
50
+ forItem: string;
51
+ change: string;
52
+ choose: string;
53
+ onlyFits: string;
54
+ showAll: string;
55
+ save: string;
56
+ };
57
+ fit: {
58
+ fits: string;
59
+ universal: string;
60
+ no: string;
61
+ unknown: string;
62
+ };
63
+ };
64
+ /** Messages for a locale (`en*` → English, else Spanish), with the store's overrides on top. */
65
+ declare function subjectMessages(locale?: string, overrides?: DeepPartial<SubjectMessages>): SubjectMessages;
66
+ /** `{name}` placeholders. */
67
+ declare function fill(template: string, values: Record<string, string | number>): string;
68
+
69
+ type SubjectPickerClassNames = Partial<Record<'form' | 'field' | 'label' | 'control' | 'hint' | 'error' | 'actions' | 'submit' | 'cancel' | 'privateNote', string>>;
70
+ type SubjectPickerProps = {
71
+ kind: SubjectKind;
72
+ /** The item being edited (its public values start the form); omit to add one. */
73
+ subject?: Subject | null;
74
+ /** Values to start with (what `SubjectActionData.values` returned after an error, or a refinement without JavaScript). */
75
+ values?: SubjectValues;
76
+ /** What the action answered: errors by field, refilled values. */
77
+ data?: SubjectActionData | null;
78
+ /** `action` of the form (default: the current URL). */
79
+ action?: string;
80
+ /** Field-name prefix (default `subject`: `subject.make`). */
81
+ namePrefix?: string;
82
+ /** With JavaScript: receives the validated input; the form does not post. Without it the form posts normally. */
83
+ onSubmit?: (input: SubjectInput, subject?: Subject | null) => void | Promise<void>;
84
+ onCancel?: () => void;
85
+ pending?: boolean;
86
+ /** Offer «use as active» (default true when the buyer already has items of this kind: the caller decides). */
87
+ offerDefault?: boolean;
88
+ locale?: string;
89
+ messages?: DeepPartial<SubjectMessages>;
90
+ classNames?: SubjectPickerClassNames;
91
+ };
92
+ declare function SubjectPicker(props: SubjectPickerProps): react.JSX.Element;
93
+
94
+ type SubjectGarageClassNames = SubjectPickerClassNames & Partial<Record<'root' | 'header' | 'title' | 'list' | 'card' | 'cardTitle' | 'nickname' | 'badge' | 'meta' | 'cardActions' | 'button' | 'primary' | 'danger' | 'empty' | 'notice' | 'limit' | 'details' | 'summary', string>>;
95
+ type SubjectGarageProps = {
96
+ kind: SubjectKind;
97
+ /** The buyer's items (any kind: only this kind's are shown). */
98
+ subjects: Subject[];
99
+ /** `error`: the read failed; the list is replaced by a retry. `ready` (default) shows the list. */
100
+ status?: 'ready' | 'error';
101
+ onRetry?: () => void;
102
+ /** `action` of the forms (default: the current URL). */
103
+ action?: string;
104
+ /** Where «see compatible parts» goes for an item (e.g. `/productos?para=<fit_key>`); `null`/omitted hides the link. */
105
+ partsHref?: (subject: Subject) => string | null | undefined;
106
+ /** What the action answered: errors and refilled values for the form it concerns, a notice after a save. */
107
+ data?: SubjectActionData | null;
108
+ /** Id of the item whose edit form is open without JavaScript (after a failed save); `'new'` for the add form. */
109
+ openId?: string | null;
110
+ onSave?: (input: SubjectInput, subject?: Subject | null) => void | Promise<void>;
111
+ onDelete?: (subject: Subject) => void | Promise<void>;
112
+ onMakeDefault?: (subject: Subject) => void | Promise<void>;
113
+ pending?: boolean;
114
+ headingLevel?: 2 | 3;
115
+ locale?: string;
116
+ messages?: DeepPartial<SubjectMessages>;
117
+ classNames?: SubjectGarageClassNames;
118
+ };
119
+ declare function SubjectGarage(props: SubjectGarageProps): react.JSX.Element;
120
+
121
+ type SubjectFilterClassNames = Partial<Record<'root' | 'label' | 'link' | 'toggle', string>>;
122
+ type SubjectFilterProps = {
123
+ kind: SubjectKind;
124
+ /** The item that applies, or `null` when the buyer has not chosen one. */
125
+ active: {
126
+ label: string;
127
+ fit_key: string;
128
+ } | null;
129
+ /** Showing the whole catalog (the buyer asked for it) instead of only what fits. */
130
+ showingAll?: boolean;
131
+ /** Where «Change» goes (the garage or the picker). Omit to hide it. */
132
+ changeHref?: string;
133
+ /** Where «Choose your …» goes when there is no active item. */
134
+ chooseHref?: string;
135
+ /** The same page showing only what fits / everything (e.g. with and without `?todo=1`). */
136
+ onlyFitsHref?: string;
137
+ showAllHref?: string;
138
+ locale?: string;
139
+ messages?: DeepPartial<SubjectMessages>;
140
+ classNames?: SubjectFilterClassNames;
141
+ };
142
+ declare function SubjectFilter(props: SubjectFilterProps): react.JSX.Element | null;
143
+ type SubjectFitBadgeProps = {
144
+ kind: SubjectKind;
145
+ /** `fitState(product)` of `@asteby/storefront-react/fitment` (the state `withFit` resolved). */
146
+ state: 'fits' | 'universal' | 'no' | 'unknown';
147
+ /** Show the badge for `unknown` too (default: it is hidden: no data, no claim). */
148
+ showUnknown?: boolean;
149
+ locale?: string;
150
+ messages?: DeepPartial<SubjectMessages>;
151
+ className?: string;
152
+ children?: ReactNode;
153
+ };
154
+ /** «Fits your vehicle» / «Universal» / «Not for your vehicle». With no compatibility data it says nothing. */
155
+ declare function SubjectFitBadge(props: SubjectFitBadgeProps): react.JSX.Element | null;
156
+
157
+ /**
158
+ * Data hook of «My items» for stores that render on the client (the loader/action pair of `subjects/server` is the way on
159
+ * a server-rendered store). It talks to the client of the `StorefrontProvider` (or `client`), which must carry the buyer's
160
+ * session (`withCustomerToken`). It never throws: a failure is `status: 'error'` and `reload()` is the retry; a store that
161
+ * declares no kind is `status: 'unavailable'` and the section should not render.
162
+ */
163
+
164
+ type UseSubjectsStatus = 'loading' | 'ready' | 'error' | 'unavailable';
165
+ type UseSubjects = {
166
+ status: UseSubjectsStatus;
167
+ kinds: SubjectKind[];
168
+ subjects: Subject[];
169
+ error: Error | null;
170
+ reload: () => void;
171
+ create: (input: SubjectInput) => Promise<Subject>;
172
+ update: (id: string, patch: SubjectPatch) => Promise<Subject>;
173
+ remove: (id: string) => Promise<void>;
174
+ makeDefault: (id: string) => Promise<Subject>;
175
+ };
176
+ declare function useSubjects(client?: StorefrontClient | null): UseSubjects;
177
+
178
+ export { type DeepPartial, SubjectActionData, SubjectErrorCode, SubjectFilter, type SubjectFilterClassNames, type SubjectFilterProps, SubjectFitBadge, type SubjectFitBadgeProps, SubjectGarage, type SubjectGarageClassNames, type SubjectGarageProps, type SubjectMessages, SubjectPicker, type SubjectPickerClassNames, type SubjectPickerProps, type UseSubjects, type UseSubjectsStatus, fill as fillSubjectMessage, subjectMessages, useSubjects };
@@ -0,0 +1,465 @@
1
+ export { subjectFromFitKey } from './chunk-Z4C4LKXF.js';
2
+ import { useResolvedLocale } from './chunk-T52H5HL3.js';
3
+ import { useOptionalStorefront } from './chunk-DQBL2DFJ.js';
4
+ import { subjectOptions, reconcileSubjectValues, resolveSubject } from '@asteby/storefront';
5
+ import { useId, useState, useMemo, useRef, useCallback, useEffect } from 'react';
6
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
+
8
+ // src/subjects/messages.ts
9
+ var ES = {
10
+ garage: {
11
+ empty: "A\xFAn no guardas ning\xFAn {kind}.",
12
+ add: "Agregar {kind}",
13
+ active: "Activo",
14
+ makeActive: "Hacer activo",
15
+ edit: "Editar",
16
+ remove: "Eliminar",
17
+ removeConfirm: "S\xED, eliminar {name}",
18
+ removeHint: "Se borra de verdad: no queda nada guardado.",
19
+ viewParts: "Ver piezas compatibles",
20
+ limit: "Llegaste al l\xEDmite ({max}). Elimina uno para agregar otro.",
21
+ loadError: "No pudimos cargar esto.",
22
+ retry: "Reintentar",
23
+ saved: "Guardado.",
24
+ removed: "Eliminado.",
25
+ privateLabel: "{field}: {value}"
26
+ },
27
+ picker: {
28
+ choose: "Elige\u2026",
29
+ chooseFirst: "Primero elige el campo anterior",
30
+ nickname: "Apodo (opcional)",
31
+ nicknameHint: "Para reconocerlo, por ejemplo \xABEl de diario\xBB.",
32
+ privateNote: "Solo t\xFA lo ves. No se usa para buscar piezas ni se comparte.",
33
+ privateKeep: "D\xE9jalo vac\xEDo para conservar el guardado.",
34
+ privateClear: "Quitar el guardado",
35
+ makeActive: "Usar como activo",
36
+ save: "Guardar",
37
+ saving: "Guardando\u2026",
38
+ cancel: "Cancelar",
39
+ refine: "Continuar",
40
+ errors: {
41
+ required: "Completa este campo.",
42
+ unknown: "Ese campo no existe.",
43
+ invalid: "El valor no es v\xE1lido.",
44
+ not_in_catalog: "Elige una opci\xF3n de la lista.",
45
+ year_range: "Ese a\xF1o no corresponde a lo que elegiste.",
46
+ catalog_unavailable: "El cat\xE1logo no est\xE1 disponible por ahora.",
47
+ validation: "Revisa los datos.",
48
+ duplicate: "Ya lo tienes guardado.",
49
+ limit: "Llegaste al l\xEDmite. Elimina uno para agregar otro.",
50
+ rate_limited: "Demasiados intentos. Espera un momento.",
51
+ unauthorized: "Inicia sesi\xF3n para continuar.",
52
+ not_found: "Ya no existe.",
53
+ unsupported: "Esta tienda no ofrece esto.",
54
+ store_misconfigured: "La tienda no est\xE1 configurada para esto. Av\xEDsale al negocio.",
55
+ unavailable: "No pudimos guardar. Intenta de nuevo."
56
+ }
57
+ },
58
+ filter: {
59
+ forItem: "Para: {name}",
60
+ change: "Cambiar",
61
+ choose: "Elige tu {kind} para ver solo lo que le queda",
62
+ onlyFits: "Solo lo compatible",
63
+ showAll: "Ver todo",
64
+ save: "Guardar en mi cuenta"
65
+ },
66
+ fit: { fits: "Compatible con tu {kind}", universal: "Universal", no: "No es para tu {kind}", unknown: "Sin datos de compatibilidad" }
67
+ };
68
+ var EN = {
69
+ garage: {
70
+ empty: "You have not saved any {kind} yet.",
71
+ add: "Add {kind}",
72
+ active: "Active",
73
+ makeActive: "Make active",
74
+ edit: "Edit",
75
+ remove: "Delete",
76
+ removeConfirm: "Yes, delete {name}",
77
+ removeHint: "It is really deleted: nothing is kept.",
78
+ viewParts: "See compatible parts",
79
+ limit: "You reached the limit ({max}). Delete one to add another.",
80
+ loadError: "We could not load this.",
81
+ retry: "Try again",
82
+ saved: "Saved.",
83
+ removed: "Deleted.",
84
+ privateLabel: "{field}: {value}"
85
+ },
86
+ picker: {
87
+ choose: "Choose\u2026",
88
+ chooseFirst: "Choose the previous field first",
89
+ nickname: "Nickname (optional)",
90
+ nicknameHint: "To recognize it, e.g. \u201CThe everyday one\u201D.",
91
+ privateNote: "Only you see it. It is not used to search parts and it is never shared.",
92
+ privateKeep: "Leave it empty to keep the saved one.",
93
+ privateClear: "Remove the saved one",
94
+ makeActive: "Use as active",
95
+ save: "Save",
96
+ saving: "Saving\u2026",
97
+ cancel: "Cancel",
98
+ refine: "Continue",
99
+ errors: {
100
+ required: "Fill in this field.",
101
+ unknown: "That field does not exist.",
102
+ invalid: "The value is not valid.",
103
+ not_in_catalog: "Choose an option from the list.",
104
+ year_range: "That year does not match what you chose.",
105
+ catalog_unavailable: "The catalog is not available right now.",
106
+ validation: "Check the details.",
107
+ duplicate: "You already saved this.",
108
+ limit: "You reached the limit. Delete one to add another.",
109
+ rate_limited: "Too many attempts. Wait a moment.",
110
+ unauthorized: "Sign in to continue.",
111
+ not_found: "It no longer exists.",
112
+ unsupported: "This store does not offer this.",
113
+ store_misconfigured: "The store is not set up for this. Let the business know.",
114
+ unavailable: "We could not save. Try again."
115
+ }
116
+ },
117
+ filter: {
118
+ forItem: "For: {name}",
119
+ change: "Change",
120
+ choose: "Choose your {kind} to see only what fits",
121
+ onlyFits: "Only what fits",
122
+ showAll: "Show all",
123
+ save: "Save to my account"
124
+ },
125
+ fit: { fits: "Fits your {kind}", universal: "Universal", no: "Not for your {kind}", unknown: "No compatibility data" }
126
+ };
127
+ function merge(base, over) {
128
+ if (!over) return base;
129
+ const out = { ...base };
130
+ for (const [k, v] of Object.entries(over)) {
131
+ if (v === void 0) continue;
132
+ const cur = out[k];
133
+ out[k] = cur && typeof cur === "object" && typeof v === "object" && v ? merge(cur, v) : v;
134
+ }
135
+ return out;
136
+ }
137
+ function subjectMessages(locale, overrides) {
138
+ return merge(locale?.toLowerCase().startsWith("en") ? EN : ES, overrides);
139
+ }
140
+ function fill(template, values) {
141
+ return template.replace(/\{(\w+)\}/g, (m, k) => k in values ? String(values[k]) : m);
142
+ }
143
+ var isYearField = (f) => f.type === "year";
144
+ var initialValues = (kind, subject, values) => {
145
+ const out = {};
146
+ for (const f of kind.fields) {
147
+ if (f.private) continue;
148
+ const v = values?.[f.key] ?? subject?.attributes[f.key];
149
+ if (v !== void 0 && v !== null) out[f.key] = v;
150
+ }
151
+ return out;
152
+ };
153
+ function SubjectPicker(props) {
154
+ const { kind, subject, data, action, namePrefix = "subject", onSubmit, onCancel, pending, classNames: cx = {} } = props;
155
+ const locale = useResolvedLocale(props.locale);
156
+ const m = subjectMessages(locale, props.messages).picker;
157
+ const uid = useId().replace(/:/g, "");
158
+ const [values, setValues] = useState(() => initialValues(kind, subject, props.values ?? data?.values));
159
+ const [nickname, setNickname] = useState(subject?.nickname ?? "");
160
+ const [makeDefault, setMakeDefault] = useState(false);
161
+ const [clientProblems, setClientProblems] = useState(null);
162
+ const [clear, setClear] = useState({});
163
+ const [privates, setPrivates] = useState({});
164
+ const serverErrors = data?.fieldErrors ?? {};
165
+ const problemOf = (key) => clientProblems?.[key] ?? serverErrors[key];
166
+ const errorText = (code) => code ? m.errors[code] ?? m.errors.invalid : void 0;
167
+ const topError = !data?.ok && data?.error && !data.error.field ? errorText(data.error.code) : void 0;
168
+ const optionsOf = useMemo(() => (f) => f.source === "catalog" ? subjectOptions(kind, f.key, values) : [], [kind, values]);
169
+ const set = (key, value) => {
170
+ setClientProblems(null);
171
+ setValues((cur) => reconcileSubjectValues(kind, { ...cur, [key]: value }));
172
+ };
173
+ const submit = (e) => {
174
+ if (!onSubmit) return;
175
+ e.preventDefault();
176
+ const sent = { ...values };
177
+ for (const f of kind.fields) {
178
+ if (!f.private) continue;
179
+ if (clear[f.key]) sent[f.key] = null;
180
+ else if (privates[f.key]) sent[f.key] = privates[f.key];
181
+ }
182
+ const r = resolveSubject(kind, sent);
183
+ if (!r.ok) {
184
+ const privateKeys = new Set(kind.fields.filter((f) => f.private).map((f) => f.key));
185
+ const blocking = Object.fromEntries(Object.entries(r.problems).filter(([key, code]) => !(subject && code === "required" && privateKeys.has(key))));
186
+ if (Object.keys(blocking).length > 0) {
187
+ setClientProblems(blocking);
188
+ return;
189
+ }
190
+ }
191
+ const attributes = {};
192
+ for (const f of kind.fields) {
193
+ const v = sent[f.key];
194
+ if (v === null && f.private) attributes[f.key] = null;
195
+ else if (v !== void 0 && v !== null && v !== "") attributes[f.key] = v;
196
+ }
197
+ void onSubmit({ kind: kind.key, attributes, nickname: nickname.trim() || null, ...makeDefault ? { is_default: true } : {} }, subject);
198
+ };
199
+ const name = (key) => `${namePrefix}.${key}`;
200
+ const levelKeys = kind.fields.filter((f) => f.source === "catalog" && !isYearField(f)).map((f) => f.key);
201
+ return /* @__PURE__ */ jsxs("form", { method: "post", action, onSubmit: submit, className: cx.form, "data-sf-subject-picker": true, "data-sf-subject-kind": kind.key, noValidate: !!onSubmit, children: [
202
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: "intent", value: "subject-save" }),
203
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: "kind", value: kind.key }),
204
+ subject ? /* @__PURE__ */ jsx("input", { type: "hidden", name: "id", value: subject.id }) : null,
205
+ topError ? /* @__PURE__ */ jsx("p", { role: "alert", className: cx.error, "data-sf-subject-error": true, children: topError }) : null,
206
+ kind.fields.map((f) => {
207
+ const id = `sf-subject-${uid}-${f.key}`;
208
+ const err = errorText(problemOf(f.key));
209
+ const opts = optionsOf(f);
210
+ const waiting = f.source === "catalog" && !isYearField(f) && levelKeys.indexOf(f.key) > 0 && opts.length === 0;
211
+ const describedBy = [err ? `${id}-err` : "", f.private ? `${id}-priv` : ""].filter(Boolean).join(" ") || void 0;
212
+ let control;
213
+ if (f.private) {
214
+ control = /* @__PURE__ */ jsx(
215
+ "input",
216
+ {
217
+ id,
218
+ name: name(f.key),
219
+ type: "text",
220
+ autoComplete: "off",
221
+ autoCapitalize: "characters",
222
+ spellCheck: false,
223
+ maxLength: f.max,
224
+ className: cx.control,
225
+ value: privates[f.key] ?? "",
226
+ onChange: (e) => setPrivates((cur) => ({ ...cur, [f.key]: e.target.value })),
227
+ "aria-describedby": describedBy,
228
+ "aria-invalid": err ? true : void 0,
229
+ placeholder: subject && typeof subject.attributes[f.key] === "string" ? String(subject.attributes[f.key]) : void 0,
230
+ "data-sf-private": true
231
+ }
232
+ );
233
+ } else if (f.source === "catalog" && opts.length > 0) {
234
+ control = /* @__PURE__ */ jsxs("select", { id, name: name(f.key), className: cx.control, value: String(values[f.key] ?? ""), onChange: (e) => set(f.key, e.target.value), required: f.required, "aria-describedby": describedBy, "aria-invalid": err ? true : void 0, children: [
235
+ /* @__PURE__ */ jsx("option", { value: "", children: m.choose }),
236
+ opts.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label }, o.value))
237
+ ] });
238
+ } else if (f.source === "catalog" && !isYearField(f)) {
239
+ control = /* @__PURE__ */ jsx("select", { id, name: name(f.key), className: cx.control, value: "", disabled: true, "aria-describedby": describedBy, children: /* @__PURE__ */ jsx("option", { value: "", children: waiting ? m.chooseFirst : m.choose }) });
240
+ } else {
241
+ control = /* @__PURE__ */ jsx(
242
+ "input",
243
+ {
244
+ id,
245
+ name: name(f.key),
246
+ type: isYearField(f) ? "number" : "text",
247
+ inputMode: isYearField(f) ? "numeric" : void 0,
248
+ min: isYearField(f) ? 1900 : void 0,
249
+ max: isYearField(f) ? 2100 : void 0,
250
+ maxLength: f.max,
251
+ className: cx.control,
252
+ value: String(values[f.key] ?? ""),
253
+ onChange: (e) => set(f.key, e.target.value),
254
+ required: f.required,
255
+ "aria-describedby": describedBy,
256
+ "aria-invalid": err ? true : void 0
257
+ }
258
+ );
259
+ }
260
+ return /* @__PURE__ */ jsxs("div", { className: cx.field, "data-sf-subject-field": f.key, "data-sf-subject-year": isYearField(f) ? "" : void 0, children: [
261
+ /* @__PURE__ */ jsxs("label", { htmlFor: id, className: cx.label, children: [
262
+ f.label,
263
+ f.required ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: " *" }) : null
264
+ ] }),
265
+ control,
266
+ f.private ? /* @__PURE__ */ jsxs("p", { id: `${id}-priv`, className: cx.privateNote ?? cx.hint, "data-sf-subject-private-note": true, children: [
267
+ m.privateNote,
268
+ subject && subject.attributes[f.key] !== void 0 ? ` ${m.privateKeep}` : ""
269
+ ] }) : null,
270
+ f.private && subject && subject.attributes[f.key] !== void 0 ? /* @__PURE__ */ jsxs("label", { className: cx.hint, "data-sf-subject-clear": true, children: [
271
+ /* @__PURE__ */ jsx("input", { type: "checkbox", name: `${name(f.key)}.clear`, checked: !!clear[f.key], onChange: (e) => setClear((cur) => ({ ...cur, [f.key]: e.target.checked })) }),
272
+ " ",
273
+ m.privateClear
274
+ ] }) : null,
275
+ err ? /* @__PURE__ */ jsx("p", { id: `${id}-err`, role: "alert", className: cx.error, "data-sf-subject-error": true, children: err }) : null
276
+ ] }, f.key);
277
+ }),
278
+ /* @__PURE__ */ jsxs("div", { className: cx.field, "data-sf-subject-field": "nickname", children: [
279
+ /* @__PURE__ */ jsx("label", { htmlFor: `sf-subject-${uid}-nick`, className: cx.label, children: m.nickname }),
280
+ /* @__PURE__ */ jsx("input", { id: `sf-subject-${uid}-nick`, name: "nickname", type: "text", maxLength: 40, className: cx.control, value: nickname, onChange: (e) => setNickname(e.target.value), "aria-describedby": `sf-subject-${uid}-nick-h` }),
281
+ /* @__PURE__ */ jsx("p", { id: `sf-subject-${uid}-nick-h`, className: cx.hint, children: m.nicknameHint })
282
+ ] }),
283
+ props.offerDefault && !subject?.is_default ? /* @__PURE__ */ jsxs("label", { className: cx.hint, "data-sf-subject-default": true, children: [
284
+ /* @__PURE__ */ jsx("input", { type: "checkbox", name: "is_default", value: "1", checked: makeDefault, onChange: (e) => setMakeDefault(e.target.checked) }),
285
+ " ",
286
+ m.makeActive
287
+ ] }) : null,
288
+ /* @__PURE__ */ jsxs("div", { className: cx.actions, children: [
289
+ /* @__PURE__ */ jsx("button", { type: "submit", className: cx.submit, disabled: pending, "data-sf-subject-submit": true, children: pending ? m.saving : m.save }),
290
+ /* @__PURE__ */ jsx("noscript", { children: /* @__PURE__ */ jsx("button", { type: "submit", name: "intent", value: "subject-refine", formNoValidate: true, "data-sf-subject-refine": true, children: m.refine }) }),
291
+ onCancel ? /* @__PURE__ */ jsx("button", { type: "button", className: cx.cancel, onClick: onCancel, "data-sf-subject-cancel": true, children: m.cancel }) : null
292
+ ] })
293
+ ] });
294
+ }
295
+ function SubjectGarage(props) {
296
+ const { kind, status = "ready", action, partsHref, data, onSave, onDelete, onMakeDefault, pending, classNames: cx = {} } = props;
297
+ const locale = useResolvedLocale(props.locale);
298
+ const msgs = subjectMessages(locale, props.messages);
299
+ const m = msgs.garage;
300
+ const Heading = props.headingLevel === 3 ? "h3" : "h2";
301
+ const singular = kind.label.toLowerCase();
302
+ const mine = props.subjects.filter((s) => s.kind === kind.key);
303
+ const atLimit = mine.length >= kind.max_per_account;
304
+ const openId = props.openId ?? (data && !data.ok && (data.intent === "subject-save" || data.intent === "subject-refine") ? data.id ?? "new" : null);
305
+ return /* @__PURE__ */ jsxs("section", { className: cx.root, "data-sf-subject-garage": true, "data-sf-subject-kind": kind.key, "aria-labelledby": `sf-garage-${kind.key}`, children: [
306
+ /* @__PURE__ */ jsx("header", { className: cx.header, children: /* @__PURE__ */ jsx(Heading, { id: `sf-garage-${kind.key}`, className: cx.title, children: kind.label_plural }) }),
307
+ data?.ok && data.intent && data.intent !== "subject-default" ? /* @__PURE__ */ jsx("p", { role: "status", className: cx.notice, "data-sf-subject-notice": true, children: data.intent === "subject-delete" ? m.removed : m.saved }) : null,
308
+ status === "error" ? /* @__PURE__ */ jsxs("div", { role: "alert", className: cx.notice, "data-sf-subject-degraded": true, children: [
309
+ /* @__PURE__ */ jsx("p", { children: m.loadError }),
310
+ props.onRetry ? /* @__PURE__ */ jsx("button", { type: "button", className: cx.button, onClick: props.onRetry, children: m.retry }) : /* @__PURE__ */ jsx("a", { href: "", className: cx.button, children: m.retry })
311
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
312
+ mine.length === 0 ? /* @__PURE__ */ jsx("p", { className: cx.empty, "data-sf-subject-empty": true, children: fill(m.empty, { kind: singular }) }) : /* @__PURE__ */ jsx("ul", { className: cx.list, "data-sf-subject-list": true, children: mine.map((s) => {
313
+ const parts = partsHref?.(s);
314
+ const privates = kind.fields.filter((f) => f.private && s.attributes[f.key] !== void 0);
315
+ return /* @__PURE__ */ jsxs("li", { className: cx.card, "data-sf-subject": s.id, "data-sf-subject-active": s.is_default ? "" : void 0, children: [
316
+ /* @__PURE__ */ jsxs("p", { className: cx.cardTitle, "data-sf-subject-label": true, children: [
317
+ s.label,
318
+ s.is_default ? /* @__PURE__ */ jsx("span", { className: cx.badge, "data-sf-subject-badge": true, children: m.active }) : null
319
+ ] }),
320
+ s.nickname ? /* @__PURE__ */ jsx("p", { className: cx.nickname, children: s.nickname }) : null,
321
+ privates.map((f) => /* @__PURE__ */ jsx("p", { className: cx.meta, "data-sf-subject-private-value": true, children: fill(m.privateLabel, { field: f.label, value: String(s.attributes[f.key]) }) }, f.key)),
322
+ /* @__PURE__ */ jsxs("div", { className: cx.cardActions, children: [
323
+ parts ? /* @__PURE__ */ jsx("a", { href: parts, className: `${cx.button ?? ""} ${cx.primary ?? ""}`.trim() || void 0, "data-sf-subject-parts": true, children: m.viewParts }) : null,
324
+ !s.is_default ? /* @__PURE__ */ jsxs("form", { method: "post", action, onSubmit: onMakeDefault ? (e) => {
325
+ e.preventDefault();
326
+ void onMakeDefault(s);
327
+ } : void 0, "data-sf-subject-default-form": true, children: [
328
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: "intent", value: "subject-default" }),
329
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: "id", value: s.id }),
330
+ /* @__PURE__ */ jsx("button", { type: "submit", className: cx.button, disabled: pending, children: m.makeActive })
331
+ ] }) : null,
332
+ /* @__PURE__ */ jsxs("details", { className: cx.details, open: openId === s.id || void 0, "data-sf-subject-edit": true, children: [
333
+ /* @__PURE__ */ jsx("summary", { className: cx.summary, children: m.edit }),
334
+ /* @__PURE__ */ jsx(
335
+ SubjectPicker,
336
+ {
337
+ kind,
338
+ subject: s,
339
+ ...action ? { action } : {},
340
+ data: openId === s.id ? data : null,
341
+ ...onSave ? { onSubmit: onSave } : {},
342
+ pending,
343
+ locale,
344
+ ...props.messages ? { messages: props.messages } : {},
345
+ classNames: cx
346
+ }
347
+ )
348
+ ] }),
349
+ /* @__PURE__ */ jsxs("details", { className: cx.details, "data-sf-subject-remove": true, children: [
350
+ /* @__PURE__ */ jsx("summary", { className: cx.summary, children: m.remove }),
351
+ /* @__PURE__ */ jsx("p", { className: cx.meta, children: m.removeHint }),
352
+ /* @__PURE__ */ jsxs("form", { method: "post", action, onSubmit: onDelete ? (e) => {
353
+ e.preventDefault();
354
+ void onDelete(s);
355
+ } : void 0, children: [
356
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: "intent", value: "subject-delete" }),
357
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: "id", value: s.id }),
358
+ /* @__PURE__ */ jsx("button", { type: "submit", className: `${cx.button ?? ""} ${cx.danger ?? ""}`.trim() || void 0, disabled: pending, children: fill(m.removeConfirm, { name: s.label }) })
359
+ ] })
360
+ ] })
361
+ ] })
362
+ ] }, s.id);
363
+ }) }),
364
+ atLimit ? /* @__PURE__ */ jsx("p", { className: cx.limit, "data-sf-subject-limit": true, children: fill(m.limit, { max: kind.max_per_account }) }) : /* @__PURE__ */ jsxs("details", { className: cx.details, open: openId === "new" || void 0, "data-sf-subject-add": true, children: [
365
+ /* @__PURE__ */ jsx("summary", { className: cx.summary, children: fill(m.add, { kind: singular }) }),
366
+ /* @__PURE__ */ jsx(
367
+ SubjectPicker,
368
+ {
369
+ kind,
370
+ ...action ? { action } : {},
371
+ data: openId === "new" ? data : null,
372
+ ...onSave ? { onSubmit: onSave } : {},
373
+ offerDefault: mine.length > 0,
374
+ pending,
375
+ locale,
376
+ ...props.messages ? { messages: props.messages } : {},
377
+ classNames: cx
378
+ }
379
+ )
380
+ ] })
381
+ ] })
382
+ ] });
383
+ }
384
+ function SubjectFilter(props) {
385
+ const { kind, active, showingAll, classNames: cx = {} } = props;
386
+ const locale = useResolvedLocale(props.locale);
387
+ const m = subjectMessages(locale, props.messages).filter;
388
+ const singular = kind.label.toLowerCase();
389
+ if (!active) {
390
+ return props.chooseHref ? /* @__PURE__ */ jsx("p", { className: cx.root, "data-sf-subject-filter": true, "data-sf-subject-filter-empty": true, children: /* @__PURE__ */ jsx("a", { href: props.chooseHref, className: cx.link, children: fill(m.choose, { kind: singular }) }) }) : null;
391
+ }
392
+ return /* @__PURE__ */ jsxs("div", { className: cx.root, "data-sf-subject-filter": true, "data-sf-subject-showing": showingAll ? "all" : "fits", children: [
393
+ /* @__PURE__ */ jsx("span", { className: cx.label, children: fill(m.forItem, { name: active.label }) }),
394
+ props.changeHref ? /* @__PURE__ */ jsx("a", { href: props.changeHref, className: cx.link, children: m.change }) : null,
395
+ showingAll && props.onlyFitsHref ? /* @__PURE__ */ jsx("a", { href: props.onlyFitsHref, className: cx.toggle ?? cx.link, children: m.onlyFits }) : null,
396
+ !showingAll && props.showAllHref ? /* @__PURE__ */ jsx("a", { href: props.showAllHref, className: cx.toggle ?? cx.link, children: m.showAll }) : null
397
+ ] });
398
+ }
399
+ function SubjectFitBadge(props) {
400
+ const locale = useResolvedLocale(props.locale);
401
+ const m = subjectMessages(locale, props.messages).fit;
402
+ if (props.state === "unknown" && !props.showUnknown) return null;
403
+ return /* @__PURE__ */ jsxs("span", { className: props.className, "data-sf-subject-fit": props.state, children: [
404
+ fill(m[props.state], { kind: props.kind.label.toLowerCase() }),
405
+ props.children
406
+ ] });
407
+ }
408
+ function useSubjects(client) {
409
+ const ctx = useOptionalStorefront();
410
+ const sf = client ?? ctx?.client ?? null;
411
+ const [state, setState] = useState({ status: "loading", kinds: [], subjects: [], error: null });
412
+ const alive = useRef(null);
413
+ const load = useCallback(() => {
414
+ alive.current?.abort();
415
+ if (!sf) {
416
+ setState({ status: "loading", kinds: [], subjects: [], error: null });
417
+ return;
418
+ }
419
+ const ctl = new AbortController();
420
+ alive.current = ctl;
421
+ const opt = { signal: ctl.signal };
422
+ setState((s) => s.status === "ready" ? s : { status: "loading", kinds: [], subjects: [], error: null });
423
+ sf.content.subjectKinds(opt).then(
424
+ async (kinds) => {
425
+ if (ctl.signal.aborted) return;
426
+ if (kinds.length === 0) return setState({ status: "unavailable", kinds: [], subjects: [], error: null });
427
+ try {
428
+ const subjects = await sf.customer.subjects.list(void 0, opt);
429
+ if (!ctl.signal.aborted) setState({ status: "ready", kinds, subjects, error: null });
430
+ } catch (e) {
431
+ if (!ctl.signal.aborted) setState({ status: "error", kinds, subjects: [], error: e instanceof Error ? e : new Error("Request failed") });
432
+ }
433
+ },
434
+ (e) => !ctl.signal.aborted && setState({ status: "error", kinds: [], subjects: [], error: e instanceof Error ? e : new Error("Request failed") })
435
+ );
436
+ }, [sf]);
437
+ useEffect(() => {
438
+ load();
439
+ return () => alive.current?.abort();
440
+ }, [load]);
441
+ const need = () => {
442
+ if (!sf) throw new Error("useSubjects: no storefront client");
443
+ return sf;
444
+ };
445
+ const create = useCallback(async (input) => {
446
+ const saved = await need().customer.subjects.create(input);
447
+ load();
448
+ return saved;
449
+ }, [sf, load]);
450
+ const update = useCallback(async (id, patch) => {
451
+ const saved = await need().customer.subjects.update(id, patch);
452
+ load();
453
+ return saved;
454
+ }, [sf, load]);
455
+ const remove = useCallback(async (id) => {
456
+ await need().customer.subjects.delete(id);
457
+ load();
458
+ }, [sf, load]);
459
+ const makeDefault = useCallback((id) => update(id, { is_default: true }), [update]);
460
+ return { ...state, reload: load, create, update, remove, makeDefault };
461
+ }
462
+
463
+ export { SubjectFilter, SubjectFitBadge, SubjectGarage, SubjectPicker, fill as fillSubjectMessage, subjectMessages, useSubjects };
464
+ //# sourceMappingURL=subjects.js.map
465
+ //# sourceMappingURL=subjects.js.map