@zappie/foundations 0.1.7 → 0.1.9

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @zappie/foundations
2
2
 
3
+ ## 0.1.9
4
+
5
+ ### Patch Changes
6
+
7
+ - c1ffb88: Images resize only from `RESIZE_HOSTS` (or your `resizeHosts`), never as `.avif`, `.jxl` or `.svg`. `Image` falls back once to the original when a resized image fails.
8
+ - Updated dependencies [c1ffb88]
9
+ - @zappie/sdk@0.1.22
10
+
11
+ ## 0.1.8
12
+
13
+ ### Patch Changes
14
+
15
+ - f7397bd: `OptionGroup` marks every required group with `data-zappie-required="true"`, single and multi-select, so `zappie verify` finds it. A required group that takes several choices is also described as required to assistive technology through `aria-describedby`, since ARIA doesn't allow `aria-required` on `role="group"`. Single-select groups keep `aria-required`. Pass `requiredNote` to word the note in your language, or `requiredNote={false}` when the group's heading already says it's required.
16
+
3
17
  ## 0.1.7
4
18
 
5
19
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -649,6 +649,12 @@ declare const OptionGroup: react.ForwardRefExoticComponent<Omit<react.AllHTMLAtt
649
649
  children?: react.ReactNode | ((state: GroupState) => react.ReactNode);
650
650
  } & {
651
651
  group: OptionGroup$1;
652
+ /**
653
+ * The hidden text that describes a required multi-select group as required, since
654
+ * role="group" can't take aria-required. Defaults to "Required". Pass false when the
655
+ * group's own heading already says so to assistive technology.
656
+ */
657
+ requiredNote?: string | false;
652
658
  } & react.RefAttributes<HTMLElement>> & {
653
659
  Input: react.ForwardRefExoticComponent<Omit<react.AllHTMLAttributes<HTMLElement>, "children" | "defaultValue" | "value"> & {
654
660
  asChild?: boolean;
@@ -680,9 +686,11 @@ type ImageProps = Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "srcSet"> &
680
686
  priority?: boolean;
681
687
  quality?: number;
682
688
  mode?: "resize" | "direct";
689
+ /** Lowercase hosts your zone resizes. Defaults to `RESIZE_HOSTS` from `@zappie/sdk/template`. */
690
+ resizeHosts?: readonly string[];
683
691
  };
684
- /** Render an image through same-origin resizing, or directly when unavailable. */
685
- declare function Image({ src, sizes, priority, quality, mode, alt, ...props }: ImageProps): react.JSX.Element;
692
+ /** Render an image through same-origin resizing, falling back once to `src` if that fails. */
693
+ declare function Image({ src, sizes, priority, quality, mode, resizeHosts, alt, onError, ...props }: ImageProps): react.JSX.Element;
686
694
 
687
695
  /**
688
696
  * Render formatted SDK money in a data element, with the minor-unit amount as its value.
@@ -2,8 +2,10 @@
2
2
 
3
3
  // src/item-form/option-group.tsx
4
4
  import {
5
+ cloneElement,
5
6
  createContext,
6
7
  forwardRef,
8
+ isValidElement,
7
9
  memo,
8
10
  useContext,
9
11
  useMemo,
@@ -13,7 +15,7 @@ import { flag } from "../internal/data-attrs.js";
13
15
  import { usePartId } from "../internal/ids.js";
14
16
  import { composeRefs, part } from "../internal/slot.js";
15
17
  import { GroupRegistry, useItemForm } from "./root.js";
16
- import { jsx } from "react/jsx-runtime";
18
+ import { jsx, jsxs } from "react/jsx-runtime";
17
19
  var Context = createContext(null);
18
20
  function useGroup() {
19
21
  const value = useContext(Context);
@@ -22,7 +24,12 @@ function useGroup() {
22
24
  }
23
25
  return value;
24
26
  }
25
- var Group = forwardRef(function OptionGroup({ group: supplied, ...props }, ref) {
27
+ var Group = forwardRef(function OptionGroup({
28
+ group: supplied,
29
+ requiredNote = "Required",
30
+ "aria-describedby": describedBy,
31
+ ...props
32
+ }, ref) {
26
33
  const ctx = useItemForm();
27
34
  const registry = useContext(GroupRegistry);
28
35
  const group = ctx.groups.find((candidate) => candidate.id === supplied.id) ?? supplied;
@@ -118,55 +125,70 @@ var Group = forwardRef(function OptionGroup({ group: supplied, ...props }, ref)
118
125
  [group, selectionKey, invalid, input, actions]
119
126
  );
120
127
  const single = group.kind === "select" && group.selection.max === 1;
121
- return /* @__PURE__ */ jsx(Context.Provider, { value: state, children: part(
122
- "div",
123
- props,
124
- state,
125
- {
126
- role: single ? "radiogroup" : "group",
127
- tabIndex: -1,
128
- "aria-label": group.name,
129
- "aria-required": single ? group.required : void 0,
130
- "aria-invalid": invalid || void 0,
131
- "data-invalid": flag(invalid),
132
- "data-state": invalid ? "invalid" : "valid",
133
- "data-zappie-option-group": group.id,
134
- onKeyDown: (event) => {
135
- if (!single || ![
136
- "ArrowDown",
137
- "ArrowRight",
138
- "ArrowUp",
139
- "ArrowLeft",
140
- "Home",
141
- "End"
142
- ].includes(event.key)) {
143
- return;
128
+ const noteId = usePartId();
129
+ const note = group.required && group.kind === "select" && !single && requiredNote;
130
+ const child = props.asChild && isValidElement(props.children) ? props.children : void 0;
131
+ const own = describedBy ?? child?.props["aria-describedby"];
132
+ const description = note ? `${noteId}${own ? ` ${own}` : ""}` : own;
133
+ return /* @__PURE__ */ jsxs(Context.Provider, { value: state, children: [
134
+ note && /* @__PURE__ */ jsx("span", { id: noteId, hidden: true, children: note }),
135
+ part(
136
+ "div",
137
+ child ? {
138
+ ...props,
139
+ children: cloneElement(child, {
140
+ "aria-describedby": description
141
+ })
142
+ } : props,
143
+ state,
144
+ {
145
+ role: single ? "radiogroup" : "group",
146
+ tabIndex: -1,
147
+ "aria-label": group.name,
148
+ "aria-required": single ? group.required : void 0,
149
+ "aria-describedby": description,
150
+ "data-zappie-required": group.required ? "true" : void 0,
151
+ "aria-invalid": invalid || void 0,
152
+ "data-invalid": flag(invalid),
153
+ "data-state": invalid ? "invalid" : "valid",
154
+ "data-zappie-option-group": group.id,
155
+ onKeyDown: (event) => {
156
+ if (!single || ![
157
+ "ArrowDown",
158
+ "ArrowRight",
159
+ "ArrowUp",
160
+ "ArrowLeft",
161
+ "Home",
162
+ "End"
163
+ ].includes(event.key)) {
164
+ return;
165
+ }
166
+ const choices = Array.from(
167
+ event.currentTarget.querySelectorAll(
168
+ '[role="radio"]:not([aria-disabled="true"])'
169
+ )
170
+ );
171
+ if (!choices.length) {
172
+ return;
173
+ }
174
+ const current = choices.indexOf(
175
+ document.activeElement
176
+ );
177
+ const index = event.key === "Home" ? 0 : event.key === "End" ? choices.length - 1 : (current + (event.key === "ArrowUp" || event.key === "ArrowLeft" ? -1 : 1) + choices.length) % choices.length;
178
+ event.preventDefault();
179
+ choices[index]?.focus();
180
+ choices[index]?.click();
144
181
  }
145
- const choices = Array.from(
146
- event.currentTarget.querySelectorAll(
147
- '[role="radio"]:not([aria-disabled="true"])'
148
- )
149
- );
150
- if (!choices.length) {
151
- return;
182
+ },
183
+ composeRefs(ref, (node) => {
184
+ if (node) {
185
+ registry.set(group.id, node);
186
+ } else {
187
+ registry.delete(group.id);
152
188
  }
153
- const current = choices.indexOf(
154
- document.activeElement
155
- );
156
- const index = event.key === "Home" ? 0 : event.key === "End" ? choices.length - 1 : (current + (event.key === "ArrowUp" || event.key === "ArrowLeft" ? -1 : 1) + choices.length) % choices.length;
157
- event.preventDefault();
158
- choices[index]?.focus();
159
- choices[index]?.click();
160
- }
161
- },
162
- composeRefs(ref, (node) => {
163
- if (node) {
164
- registry.set(group.id, node);
165
- } else {
166
- registry.delete(group.id);
167
- }
168
- })
169
- ) });
189
+ })
190
+ )
191
+ ] });
170
192
  });
171
193
  var ChoiceView = memo(
172
194
  forwardRef(function ChoiceView2({
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/item-form/option-group.tsx"],
4
- "sourcesContent": ["\"use client\";\nimport type {\n Choice as ChoiceModel,\n OptionGroup as GroupModel,\n} from \"@zappie/sdk\";\nimport {\n createContext,\n forwardRef,\n memo,\n useContext,\n useMemo,\n useRef,\n type ChangeEvent,\n type KeyboardEvent,\n} from \"react\";\nimport { flag } from \"../internal/data-attrs\";\nimport { usePartId } from \"../internal/ids\";\nimport { composeRefs, part, type PartProps } from \"../internal/slot\";\nimport { GroupRegistry, useItemForm } from \"./root\";\ntype GroupState = {\n group: GroupModel;\n selected: string[];\n invalid: boolean;\n input: string;\n setInput: (value: string) => void;\n toggle: (id: string) => void;\n quantities: Readonly<Record<string, number>>;\n /** Set a choice's quantity within its bounds. Zero deselects it. A positive value selects it. */\n setQuantity: (id: string, quantity: number) => void;\n};\nconst Context = createContext<GroupState | null>(null);\nfunction useGroup() {\n const value = useContext(Context);\n if (!value) {\n throw new Error(\"Choice and OptionGroup.Input require OptionGroup\");\n }\n return value;\n}\nconst Group = forwardRef<\n HTMLElement,\n PartProps<GroupState> & { group: GroupModel }\n>(function OptionGroup({ group: supplied, ...props }, ref) {\n const ctx = useItemForm();\n const registry = useContext(GroupRegistry)!;\n const group =\n ctx.groups.find((candidate) => candidate.id === supplied.id) ?? supplied;\n const mine =\n ctx.value.choices?.filter((choice) => choice.groupId === group.id) ?? [];\n const selected = mine.map((choice) => choice.choiceId);\n const quantities = Object.fromEntries(\n mine.map((choice) => [choice.choiceId, choice.quantity ?? 1]),\n );\n const invalid = ctx.issues.some((issue) => issue.groupId === group.id);\n const input = ctx.value.inputs?.[group.id] ?? \"\";\n const latest = useRef({ ctx, group });\n latest.current = { ctx, group };\n const actions = useMemo(\n () => ({\n toggle: (id: string) => {\n const { ctx, group } = latest.current;\n const choice = group.choices.find((candidate) => candidate.id === id);\n if (!choice?.isAvailable) {\n return;\n }\n const previous = ctx.value.choices ?? [];\n const chosen = previous.some(\n (candidate) =>\n candidate.groupId === group.id && candidate.choiceId === id,\n );\n let choices = previous.filter(\n (candidate) =>\n candidate.groupId !== group.id ||\n (group.selection.max !== 1 && candidate.choiceId !== id),\n );\n if (!chosen || (group.selection.max === 1 && group.required)) {\n choices = [\n ...choices,\n {\n groupId: group.id,\n choiceId: id,\n quantity: choice.quantity?.min ?? 1,\n },\n ];\n }\n const inputs = { ...ctx.value.inputs };\n delete inputs[group.id];\n ctx.setValue({ ...ctx.value, choices, inputs });\n },\n setQuantity: (id: string, quantity: number) => {\n const { ctx, group } = latest.current;\n const choice = group.choices.find((candidate) => candidate.id === id);\n if (!choice?.isAvailable || !Number.isInteger(quantity)) {\n return;\n }\n const others = (ctx.value.choices ?? []).filter(\n (candidate) =>\n candidate.groupId !== group.id ||\n (group.selection.max !== 1 && candidate.choiceId !== id),\n );\n if (quantity <= 0) {\n ctx.setValue({\n ...ctx.value,\n choices: (ctx.value.choices ?? []).filter(\n (candidate) =>\n candidate.groupId !== group.id || candidate.choiceId !== id,\n ),\n });\n return;\n }\n const min = choice.quantity?.min ?? 1;\n const max = choice.quantity?.max ?? 99;\n ctx.setValue({\n ...ctx.value,\n choices: [\n ...others,\n {\n groupId: group.id,\n choiceId: id,\n quantity: Math.min(max, Math.max(min, quantity)),\n },\n ],\n });\n },\n setInput: (value: string) => {\n const { ctx, group } = latest.current;\n ctx.setValue({\n ...ctx.value,\n choices: (ctx.value.choices ?? []).filter(\n (choice) => choice.groupId !== group.id,\n ),\n inputs: {\n ...ctx.value.inputs,\n [group.id]: value.slice(0, group.input?.maxLength ?? 500),\n },\n });\n },\n }),\n [],\n );\n const selectionKey = JSON.stringify(quantities);\n const state = useMemo(\n () => ({ group, selected, quantities, invalid, input, ...actions }),\n [group, selectionKey, invalid, input, actions],\n );\n const single = group.kind === \"select\" && group.selection.max === 1;\n return (\n <Context.Provider value={state}>\n {part(\n \"div\",\n props,\n state,\n {\n role: single ? \"radiogroup\" : \"group\",\n tabIndex: -1,\n \"aria-label\": group.name,\n \"aria-required\": single ? group.required : undefined,\n \"aria-invalid\": invalid || undefined,\n \"data-invalid\": flag(invalid),\n \"data-state\": invalid ? \"invalid\" : \"valid\",\n \"data-zappie-option-group\": group.id,\n onKeyDown: (event: KeyboardEvent<HTMLElement>) => {\n if (\n !single ||\n ![\n \"ArrowDown\",\n \"ArrowRight\",\n \"ArrowUp\",\n \"ArrowLeft\",\n \"Home\",\n \"End\",\n ].includes(event.key)\n ) {\n return;\n }\n const choices = Array.from(\n event.currentTarget.querySelectorAll<HTMLElement>(\n '[role=\"radio\"]:not([aria-disabled=\"true\"])',\n ),\n );\n if (!choices.length) {\n return;\n }\n const current = choices.indexOf(\n document.activeElement as HTMLElement,\n );\n const index =\n event.key === \"Home\"\n ? 0\n : event.key === \"End\"\n ? choices.length - 1\n : (current +\n (event.key === \"ArrowUp\" || event.key === \"ArrowLeft\"\n ? -1\n : 1) +\n choices.length) %\n choices.length;\n event.preventDefault();\n choices[index]?.focus();\n choices[index]?.click();\n },\n },\n composeRefs(ref, (node) => {\n if (node) {\n registry.set(group.id, node);\n } else {\n registry.delete(group.id);\n }\n }),\n )}\n </Context.Provider>\n );\n});\ntype ChoiceState = {\n choice: ChoiceModel;\n selected: boolean;\n disabled: boolean;\n toggle: () => void;\n /** Selected quantity (0 when unselected) and the choice's bounds. */\n quantity: number;\n min: number;\n max: number;\n setQuantity: (quantity: number) => void;\n};\nconst ChoiceView = memo(\n forwardRef<\n HTMLElement,\n PartProps<ChoiceState> & {\n choice: ChoiceModel;\n selected: boolean;\n single: boolean;\n tabIndex: number;\n quantity: number;\n toggleChoice: (id: string) => void;\n setChoiceQuantity: (id: string, quantity: number) => void;\n }\n >(function ChoiceView(\n {\n choice,\n selected,\n single,\n tabIndex,\n quantity,\n toggleChoice,\n setChoiceQuantity,\n ...props\n },\n ref,\n ) {\n const toggle = () => toggleChoice(choice.id);\n const state = {\n choice,\n selected,\n disabled: !choice.isAvailable,\n toggle,\n quantity,\n min: choice.quantity?.min ?? 1,\n max: choice.quantity?.max ?? 99,\n setQuantity: (next: number) => setChoiceQuantity(choice.id, next),\n };\n return part(\n \"button\",\n props,\n state,\n {\n type: \"button\",\n role: single ? \"radio\" : \"checkbox\",\n tabIndex,\n \"aria-checked\": selected,\n \"aria-disabled\": !choice.isAvailable,\n disabled: !choice.isAvailable,\n \"data-selected\": flag(selected),\n \"data-sold-out\": flag(!choice.isAvailable),\n \"data-disabled\": flag(!choice.isAvailable),\n \"data-state\": selected ? \"checked\" : \"unchecked\",\n \"data-zappie-option\": choice.id,\n onClick: toggle,\n },\n ref,\n );\n }),\n);\n/**\n * Render a selectable option within an OptionGroup, with radio or checkbox semantics.\n * Exposes selection and quantity controls and disables unavailable choices.\n */\nexport const Choice = forwardRef<\n HTMLElement,\n PartProps<ChoiceState> & { choice: ChoiceModel }\n>(function Choice(props, ref) {\n const ctx = useGroup();\n const single = ctx.group.selection.max === 1;\n const selected = ctx.selected.includes(props.choice.id);\n const first = ctx.group.choices.find((choice) => choice.isAvailable);\n const tabIndex =\n !single ||\n selected ||\n (!ctx.selected.length && first?.id === props.choice.id)\n ? 0\n : -1;\n return (\n <ChoiceView\n {...props}\n ref={ref}\n selected={selected}\n single={single}\n tabIndex={tabIndex}\n quantity={ctx.quantities[props.choice.id] ?? 0}\n toggleChoice={ctx.toggle}\n setChoiceQuantity={ctx.setQuantity}\n />\n );\n});\nconst Input = forwardRef<HTMLElement, PartProps<GroupState>>(\n function Input(props, ref) {\n const ctx = useGroup();\n const id = usePartId(props.id);\n const toggle = ctx.group.kind === \"toggle\";\n return part(\n \"input\",\n props,\n ctx,\n {\n id,\n type: toggle\n ? \"checkbox\"\n : ctx.group.kind === \"select\"\n ? \"text\"\n : ctx.group.kind,\n value: toggle ? undefined : ctx.input,\n checked: toggle ? ctx.input === \"true\" : undefined,\n required: ctx.group.required,\n \"aria-label\": ctx.group.name,\n \"aria-invalid\": ctx.invalid || undefined,\n \"data-invalid\": flag(ctx.invalid),\n maxLength: ctx.group.input?.maxLength ?? 500,\n placeholder: ctx.group.input?.placeholder,\n onChange: (event: ChangeEvent<HTMLInputElement>) =>\n ctx.setInput(\n toggle ? String(event.target.checked) : event.target.value,\n ),\n },\n ref,\n );\n },\n);\n/**\n * Group item options with selection rules, validation state and an Input part for typed responses.\n * Use within ItemForm.Root. Use Choice for options and OptionGroup.Input for typed responses.\n */\nexport const OptionGroup = Object.assign(Group, { Input });\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\";\nimport type {\n Choice as ChoiceModel,\n OptionGroup as GroupModel,\n} from \"@zappie/sdk\";\nimport {\n cloneElement,\n createContext,\n forwardRef,\n isValidElement,\n memo,\n useContext,\n useMemo,\n useRef,\n type ChangeEvent,\n type KeyboardEvent,\n} from \"react\";\nimport { flag } from \"../internal/data-attrs\";\nimport { usePartId } from \"../internal/ids\";\nimport { composeRefs, part, type PartProps } from \"../internal/slot\";\nimport { GroupRegistry, useItemForm } from \"./root\";\ntype GroupState = {\n group: GroupModel;\n selected: string[];\n invalid: boolean;\n input: string;\n setInput: (value: string) => void;\n toggle: (id: string) => void;\n quantities: Readonly<Record<string, number>>;\n /** Set a choice's quantity within its bounds. Zero deselects it. A positive value selects it. */\n setQuantity: (id: string, quantity: number) => void;\n};\nconst Context = createContext<GroupState | null>(null);\nfunction useGroup() {\n const value = useContext(Context);\n if (!value) {\n throw new Error(\"Choice and OptionGroup.Input require OptionGroup\");\n }\n return value;\n}\nconst Group = forwardRef<\n HTMLElement,\n PartProps<GroupState> & {\n group: GroupModel;\n /**\n * The hidden text that describes a required multi-select group as required, since\n * role=\"group\" can't take aria-required. Defaults to \"Required\". Pass false when the\n * group's own heading already says so to assistive technology.\n */\n requiredNote?: string | false;\n }\n>(function OptionGroup(\n {\n group: supplied,\n requiredNote = \"Required\",\n \"aria-describedby\": describedBy,\n ...props\n },\n ref,\n) {\n const ctx = useItemForm();\n const registry = useContext(GroupRegistry)!;\n const group =\n ctx.groups.find((candidate) => candidate.id === supplied.id) ?? supplied;\n const mine =\n ctx.value.choices?.filter((choice) => choice.groupId === group.id) ?? [];\n const selected = mine.map((choice) => choice.choiceId);\n const quantities = Object.fromEntries(\n mine.map((choice) => [choice.choiceId, choice.quantity ?? 1]),\n );\n const invalid = ctx.issues.some((issue) => issue.groupId === group.id);\n const input = ctx.value.inputs?.[group.id] ?? \"\";\n const latest = useRef({ ctx, group });\n latest.current = { ctx, group };\n const actions = useMemo(\n () => ({\n toggle: (id: string) => {\n const { ctx, group } = latest.current;\n const choice = group.choices.find((candidate) => candidate.id === id);\n if (!choice?.isAvailable) {\n return;\n }\n const previous = ctx.value.choices ?? [];\n const chosen = previous.some(\n (candidate) =>\n candidate.groupId === group.id && candidate.choiceId === id,\n );\n let choices = previous.filter(\n (candidate) =>\n candidate.groupId !== group.id ||\n (group.selection.max !== 1 && candidate.choiceId !== id),\n );\n if (!chosen || (group.selection.max === 1 && group.required)) {\n choices = [\n ...choices,\n {\n groupId: group.id,\n choiceId: id,\n quantity: choice.quantity?.min ?? 1,\n },\n ];\n }\n const inputs = { ...ctx.value.inputs };\n delete inputs[group.id];\n ctx.setValue({ ...ctx.value, choices, inputs });\n },\n setQuantity: (id: string, quantity: number) => {\n const { ctx, group } = latest.current;\n const choice = group.choices.find((candidate) => candidate.id === id);\n if (!choice?.isAvailable || !Number.isInteger(quantity)) {\n return;\n }\n const others = (ctx.value.choices ?? []).filter(\n (candidate) =>\n candidate.groupId !== group.id ||\n (group.selection.max !== 1 && candidate.choiceId !== id),\n );\n if (quantity <= 0) {\n ctx.setValue({\n ...ctx.value,\n choices: (ctx.value.choices ?? []).filter(\n (candidate) =>\n candidate.groupId !== group.id || candidate.choiceId !== id,\n ),\n });\n return;\n }\n const min = choice.quantity?.min ?? 1;\n const max = choice.quantity?.max ?? 99;\n ctx.setValue({\n ...ctx.value,\n choices: [\n ...others,\n {\n groupId: group.id,\n choiceId: id,\n quantity: Math.min(max, Math.max(min, quantity)),\n },\n ],\n });\n },\n setInput: (value: string) => {\n const { ctx, group } = latest.current;\n ctx.setValue({\n ...ctx.value,\n choices: (ctx.value.choices ?? []).filter(\n (choice) => choice.groupId !== group.id,\n ),\n inputs: {\n ...ctx.value.inputs,\n [group.id]: value.slice(0, group.input?.maxLength ?? 500),\n },\n });\n },\n }),\n [],\n );\n const selectionKey = JSON.stringify(quantities);\n const state = useMemo(\n () => ({ group, selected, quantities, invalid, input, ...actions }),\n [group, selectionKey, invalid, input, actions],\n );\n const single = group.kind === \"select\" && group.selection.max === 1;\n const noteId = usePartId();\n const note =\n group.required && group.kind === \"select\" && !single && requiredNote;\n // An asChild element's own description joins the note, as partId reads its id.\n const child =\n props.asChild &&\n isValidElement<{ \"aria-describedby\"?: string | undefined }>(props.children)\n ? props.children\n : undefined;\n const own = describedBy ?? child?.props[\"aria-describedby\"];\n const description = note ? `${noteId}${own ? ` ${own}` : \"\"}` : own;\n return (\n <Context.Provider value={state}>\n {note && (\n <span id={noteId} hidden>\n {note}\n </span>\n )}\n {part(\n \"div\",\n child\n ? {\n ...props,\n children: cloneElement(child, {\n \"aria-describedby\": description,\n }),\n }\n : props,\n state,\n {\n role: single ? \"radiogroup\" : \"group\",\n tabIndex: -1,\n \"aria-label\": group.name,\n \"aria-required\": single ? group.required : undefined,\n \"aria-describedby\": description,\n \"data-zappie-required\": group.required ? \"true\" : undefined,\n \"aria-invalid\": invalid || undefined,\n \"data-invalid\": flag(invalid),\n \"data-state\": invalid ? \"invalid\" : \"valid\",\n \"data-zappie-option-group\": group.id,\n onKeyDown: (event: KeyboardEvent<HTMLElement>) => {\n if (\n !single ||\n ![\n \"ArrowDown\",\n \"ArrowRight\",\n \"ArrowUp\",\n \"ArrowLeft\",\n \"Home\",\n \"End\",\n ].includes(event.key)\n ) {\n return;\n }\n const choices = Array.from(\n event.currentTarget.querySelectorAll<HTMLElement>(\n '[role=\"radio\"]:not([aria-disabled=\"true\"])',\n ),\n );\n if (!choices.length) {\n return;\n }\n const current = choices.indexOf(\n document.activeElement as HTMLElement,\n );\n const index =\n event.key === \"Home\"\n ? 0\n : event.key === \"End\"\n ? choices.length - 1\n : (current +\n (event.key === \"ArrowUp\" || event.key === \"ArrowLeft\"\n ? -1\n : 1) +\n choices.length) %\n choices.length;\n event.preventDefault();\n choices[index]?.focus();\n choices[index]?.click();\n },\n },\n composeRefs(ref, (node) => {\n if (node) {\n registry.set(group.id, node);\n } else {\n registry.delete(group.id);\n }\n }),\n )}\n </Context.Provider>\n );\n});\ntype ChoiceState = {\n choice: ChoiceModel;\n selected: boolean;\n disabled: boolean;\n toggle: () => void;\n /** Selected quantity (0 when unselected) and the choice's bounds. */\n quantity: number;\n min: number;\n max: number;\n setQuantity: (quantity: number) => void;\n};\nconst ChoiceView = memo(\n forwardRef<\n HTMLElement,\n PartProps<ChoiceState> & {\n choice: ChoiceModel;\n selected: boolean;\n single: boolean;\n tabIndex: number;\n quantity: number;\n toggleChoice: (id: string) => void;\n setChoiceQuantity: (id: string, quantity: number) => void;\n }\n >(function ChoiceView(\n {\n choice,\n selected,\n single,\n tabIndex,\n quantity,\n toggleChoice,\n setChoiceQuantity,\n ...props\n },\n ref,\n ) {\n const toggle = () => toggleChoice(choice.id);\n const state = {\n choice,\n selected,\n disabled: !choice.isAvailable,\n toggle,\n quantity,\n min: choice.quantity?.min ?? 1,\n max: choice.quantity?.max ?? 99,\n setQuantity: (next: number) => setChoiceQuantity(choice.id, next),\n };\n return part(\n \"button\",\n props,\n state,\n {\n type: \"button\",\n role: single ? \"radio\" : \"checkbox\",\n tabIndex,\n \"aria-checked\": selected,\n \"aria-disabled\": !choice.isAvailable,\n disabled: !choice.isAvailable,\n \"data-selected\": flag(selected),\n \"data-sold-out\": flag(!choice.isAvailable),\n \"data-disabled\": flag(!choice.isAvailable),\n \"data-state\": selected ? \"checked\" : \"unchecked\",\n \"data-zappie-option\": choice.id,\n onClick: toggle,\n },\n ref,\n );\n }),\n);\n/**\n * Render a selectable option within an OptionGroup, with radio or checkbox semantics.\n * Exposes selection and quantity controls and disables unavailable choices.\n */\nexport const Choice = forwardRef<\n HTMLElement,\n PartProps<ChoiceState> & { choice: ChoiceModel }\n>(function Choice(props, ref) {\n const ctx = useGroup();\n const single = ctx.group.selection.max === 1;\n const selected = ctx.selected.includes(props.choice.id);\n const first = ctx.group.choices.find((choice) => choice.isAvailable);\n const tabIndex =\n !single ||\n selected ||\n (!ctx.selected.length && first?.id === props.choice.id)\n ? 0\n : -1;\n return (\n <ChoiceView\n {...props}\n ref={ref}\n selected={selected}\n single={single}\n tabIndex={tabIndex}\n quantity={ctx.quantities[props.choice.id] ?? 0}\n toggleChoice={ctx.toggle}\n setChoiceQuantity={ctx.setQuantity}\n />\n );\n});\nconst Input = forwardRef<HTMLElement, PartProps<GroupState>>(\n function Input(props, ref) {\n const ctx = useGroup();\n const id = usePartId(props.id);\n const toggle = ctx.group.kind === \"toggle\";\n return part(\n \"input\",\n props,\n ctx,\n {\n id,\n type: toggle\n ? \"checkbox\"\n : ctx.group.kind === \"select\"\n ? \"text\"\n : ctx.group.kind,\n value: toggle ? undefined : ctx.input,\n checked: toggle ? ctx.input === \"true\" : undefined,\n required: ctx.group.required,\n \"aria-label\": ctx.group.name,\n \"aria-invalid\": ctx.invalid || undefined,\n \"data-invalid\": flag(ctx.invalid),\n maxLength: ctx.group.input?.maxLength ?? 500,\n placeholder: ctx.group.input?.placeholder,\n onChange: (event: ChangeEvent<HTMLInputElement>) =>\n ctx.setInput(\n toggle ? String(event.target.checked) : event.target.value,\n ),\n },\n ref,\n );\n },\n);\n/**\n * Group item options with selection rules, validation state and an Input part for typed responses.\n * Use within ItemForm.Root. Use Choice for options and OptionGroup.Input for typed responses.\n */\nexport const OptionGroup = Object.assign(Group, { Input });\n"],
5
+ "mappings": "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",
6
6
  "names": ["ctx", "group", "ChoiceView", "Choice", "Input", "OptionGroup"]
7
7
  }
@@ -1,5 +1,6 @@
1
1
  // src/media/image.tsx
2
- import { resized, srcSet } from "@zappie/sdk/template";
2
+ import { canResize, resized, srcSet } from "@zappie/sdk/template";
3
+ import { useCallback, useState } from "react";
3
4
  import { jsx } from "react/jsx-runtime";
4
5
  function Image({
5
6
  src,
@@ -7,20 +8,38 @@ function Image({
7
8
  priority = false,
8
9
  quality,
9
10
  mode = "resize",
11
+ resizeHosts,
10
12
  alt,
13
+ onError,
11
14
  ...props
12
15
  }) {
13
- const responsive = mode === "resize" && /^https?:\/\//.test(src);
16
+ const [failed, setFailed] = useState(null);
17
+ const responsive = mode === "resize" && failed !== src && canResize(src, resizeHosts);
18
+ const checkLoaded = useCallback(
19
+ (image) => {
20
+ if (responsive && image?.complete && image.naturalWidth === 0) {
21
+ setFailed(src);
22
+ }
23
+ },
24
+ [responsive, src]
25
+ );
14
26
  return /* @__PURE__ */ jsx(
15
27
  "img",
16
28
  {
17
29
  ...props,
30
+ ref: checkLoaded,
18
31
  alt,
19
- src: responsive ? resized(src, 960, quality) : src,
20
- ...responsive ? { srcSet: srcSet(src, quality), sizes } : {},
32
+ src: responsive ? resized(src, 960, quality, resizeHosts) : src,
33
+ ...responsive ? { srcSet: srcSet(src, quality, resizeHosts), sizes } : {},
21
34
  loading: priority ? "eager" : "lazy",
22
35
  decoding: priority ? "sync" : "async",
23
- ...priority ? { fetchPriority: "high" } : {}
36
+ ...priority ? { fetchPriority: "high" } : {},
37
+ onError: (event) => {
38
+ if (responsive) {
39
+ setFailed(src);
40
+ }
41
+ onError?.(event);
42
+ }
24
43
  }
25
44
  );
26
45
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/media/image.tsx"],
4
- "sourcesContent": ["import { resized, srcSet } from \"@zappie/sdk/template\";\nimport type { ImgHTMLAttributes } from \"react\";\n\n/** Props for a responsive storefront image. */\nexport type ImageProps = Omit<\n ImgHTMLAttributes<HTMLImageElement>,\n \"src\" | \"srcSet\"\n> & {\n src: string;\n alt: string;\n sizes?: string;\n priority?: boolean;\n quality?: number;\n mode?: \"resize\" | \"direct\";\n};\n\n/** Render an image through same-origin resizing, or directly when unavailable. */\nexport function Image({\n src,\n sizes = \"100vw\",\n priority = false,\n quality,\n mode = \"resize\",\n alt,\n ...props\n}: ImageProps) {\n const responsive = mode === \"resize\" && /^https?:\\/\\//.test(src);\n return (\n <img\n {...props}\n alt={alt}\n src={responsive ? resized(src, 960, quality) : src}\n {...(responsive ? { srcSet: srcSet(src, quality), sizes } : {})}\n loading={priority ? \"eager\" : \"lazy\"}\n decoding={priority ? \"sync\" : \"async\"}\n {...(priority ? { fetchPriority: \"high\" as const } : {})}\n />\n );\n}\n"],
5
- "mappings": ";AAAA,SAAS,SAAS,cAAc;AA4B5B;AAXG,SAAS,MAAM;AAAA,EACpB;AAAA,EACA,QAAQ;AAAA,EACR,WAAW;AAAA,EACX;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,GAAG;AACL,GAAe;AACb,QAAM,aAAa,SAAS,YAAY,eAAe,KAAK,GAAG;AAC/D,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA,KAAK,aAAa,QAAQ,KAAK,KAAK,OAAO,IAAI;AAAA,MAC9C,GAAI,aAAa,EAAE,QAAQ,OAAO,KAAK,OAAO,GAAG,MAAM,IAAI,CAAC;AAAA,MAC7D,SAAS,WAAW,UAAU;AAAA,MAC9B,UAAU,WAAW,SAAS;AAAA,MAC7B,GAAI,WAAW,EAAE,eAAe,OAAgB,IAAI,CAAC;AAAA;AAAA,EACxD;AAEJ;",
4
+ "sourcesContent": ["import { canResize, resized, srcSet } from \"@zappie/sdk/template\";\nimport { useCallback, useState, type ImgHTMLAttributes } from \"react\";\n\n/** Props for a responsive storefront image. */\nexport type ImageProps = Omit<\n ImgHTMLAttributes<HTMLImageElement>,\n \"src\" | \"srcSet\"\n> & {\n src: string;\n alt: string;\n sizes?: string;\n priority?: boolean;\n quality?: number;\n mode?: \"resize\" | \"direct\";\n /** Lowercase hosts your zone resizes. Defaults to `RESIZE_HOSTS` from `@zappie/sdk/template`. */\n resizeHosts?: readonly string[];\n};\n\n/** Render an image through same-origin resizing, falling back once to `src` if that fails. */\nexport function Image({\n src,\n sizes = \"100vw\",\n priority = false,\n quality,\n mode = \"resize\",\n resizeHosts,\n alt,\n onError,\n ...props\n}: ImageProps) {\n const [failed, setFailed] = useState<string | null>(null);\n const responsive =\n mode === \"resize\" && failed !== src && canResize(src, resizeHosts);\n // Catches an error that fired before hydration.\n const checkLoaded = useCallback(\n (image: HTMLImageElement | null) => {\n if (responsive && image?.complete && image.naturalWidth === 0) {\n setFailed(src);\n }\n },\n [responsive, src],\n );\n return (\n <img\n {...props}\n ref={checkLoaded}\n alt={alt}\n src={responsive ? resized(src, 960, quality, resizeHosts) : src}\n {...(responsive\n ? { srcSet: srcSet(src, quality, resizeHosts), sizes }\n : {})}\n loading={priority ? \"eager\" : \"lazy\"}\n decoding={priority ? \"sync\" : \"async\"}\n {...(priority ? { fetchPriority: \"high\" as const } : {})}\n onError={(event) => {\n if (responsive) {\n setFailed(src);\n }\n onError?.(event);\n }}\n />\n );\n}\n"],
5
+ "mappings": ";AAAA,SAAS,WAAW,SAAS,cAAc;AAC3C,SAAS,aAAa,gBAAwC;AA0C1D;AAxBG,SAAS,MAAM;AAAA,EACpB;AAAA,EACA,QAAQ;AAAA,EACR,WAAW;AAAA,EACX;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAe;AACb,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAwB,IAAI;AACxD,QAAM,aACJ,SAAS,YAAY,WAAW,OAAO,UAAU,KAAK,WAAW;AAEnE,QAAM,cAAc;AAAA,IAClB,CAAC,UAAmC;AAClC,UAAI,cAAc,OAAO,YAAY,MAAM,iBAAiB,GAAG;AAC7D,kBAAU,GAAG;AAAA,MACf;AAAA,IACF;AAAA,IACA,CAAC,YAAY,GAAG;AAAA,EAClB;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,KAAK;AAAA,MACL;AAAA,MACA,KAAK,aAAa,QAAQ,KAAK,KAAK,SAAS,WAAW,IAAI;AAAA,MAC3D,GAAI,aACD,EAAE,QAAQ,OAAO,KAAK,SAAS,WAAW,GAAG,MAAM,IACnD,CAAC;AAAA,MACL,SAAS,WAAW,UAAU;AAAA,MAC9B,UAAU,WAAW,SAAS;AAAA,MAC7B,GAAI,WAAW,EAAE,eAAe,OAAgB,IAAI,CAAC;AAAA,MACtD,SAAS,CAAC,UAAU;AAClB,YAAI,YAAY;AACd,oBAAU,GAAG;AAAA,QACf;AACA,kBAAU,KAAK;AAAA,MACjB;AAAA;AAAA,EACF;AAEJ;",
6
6
  "names": []
7
7
  }
package/dist/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var PACKAGE_VERSION = true ? "0.1.7" : "0.0.0-development";
2
+ var PACKAGE_VERSION = true ? "0.1.9" : "0.0.0-development";
3
3
  export {
4
4
  PACKAGE_VERSION
5
5
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zappie/foundations",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "UI-less React primitives for Zappie storefronts: behaviour, state and accessibility without styles.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -33,7 +33,7 @@
33
33
  "dependencies": {
34
34
  "@base-ui/react": "^1.9.0",
35
35
  "@googlemaps/markerclusterer": "^2.6.2",
36
- "@zappie/sdk": "^0.1.13"
36
+ "@zappie/sdk": "^0.1.22"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "react": "^18.2.0 || ^19.0.0",