@zappie/foundations 0.1.0 → 0.1.2

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 (74) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +30 -253
  3. package/dist/account/index.js +49 -0
  4. package/dist/account/index.js.map +7 -0
  5. package/dist/cart/index.js +168 -0
  6. package/dist/cart/index.js.map +7 -0
  7. package/dist/fulfilment/address-picker.js +168 -0
  8. package/dist/fulfilment/address-picker.js.map +7 -0
  9. package/dist/fulfilment/branch-picker.js +54 -0
  10. package/dist/fulfilment/branch-picker.js.map +7 -0
  11. package/dist/fulfilment/schedule.js +146 -0
  12. package/dist/fulfilment/schedule.js.map +7 -0
  13. package/dist/fulfilment/switch.js +54 -0
  14. package/dist/fulfilment/switch.js.map +7 -0
  15. package/dist/index.d.ts +2 -4
  16. package/dist/index.js +49 -2113
  17. package/dist/index.js.map +7 -1
  18. package/dist/internal/combobox.js +131 -0
  19. package/dist/internal/combobox.js.map +7 -0
  20. package/dist/internal/data-attrs.js +8 -0
  21. package/dist/internal/data-attrs.js.map +7 -0
  22. package/dist/internal/ids.js +10 -0
  23. package/dist/internal/ids.js.map +7 -0
  24. package/dist/{chunk-D3J7S33L.js → internal/slot.js} +13 -6
  25. package/dist/internal/slot.js.map +7 -0
  26. package/dist/internal/use-controllable-state.js +29 -0
  27. package/dist/internal/use-controllable-state.js.map +7 -0
  28. package/dist/item-form/index.js +13 -0
  29. package/dist/item-form/index.js.map +7 -0
  30. package/dist/item-form/option-group.js +268 -0
  31. package/dist/item-form/option-group.js.map +7 -0
  32. package/dist/item-form/options.js +30 -0
  33. package/dist/item-form/options.js.map +7 -0
  34. package/dist/item-form/quantity.js +131 -0
  35. package/dist/item-form/quantity.js.map +7 -0
  36. package/dist/item-form/root.js +151 -0
  37. package/dist/item-form/root.js.map +7 -0
  38. package/dist/media/image.js +30 -0
  39. package/dist/media/image.js.map +7 -0
  40. package/dist/money.js +20 -0
  41. package/dist/money.js.map +7 -0
  42. package/dist/next.js +8 -6
  43. package/dist/next.js.map +7 -1
  44. package/dist/otp/input.js +110 -0
  45. package/dist/otp/input.js.map +7 -0
  46. package/dist/overlay/engine.js +16 -8
  47. package/dist/overlay/engine.js.map +7 -1
  48. package/dist/{chunk-BY2WQGLO.js → overlay/history.js} +9 -4
  49. package/dist/overlay/history.js.map +7 -0
  50. package/dist/overlay/react.js +261 -0
  51. package/dist/overlay/react.js.map +7 -0
  52. package/dist/payment/methods.js +103 -0
  53. package/dist/payment/methods.js.map +7 -0
  54. package/dist/rich-text/banners.js +9 -0
  55. package/dist/rich-text/banners.js.map +7 -0
  56. package/dist/rich-text/browser.js +3 -3
  57. package/dist/rich-text/browser.js.map +7 -1
  58. package/dist/rich-text/core.js +75 -0
  59. package/dist/rich-text/core.js.map +7 -0
  60. package/dist/rich-text/index.js +3 -79
  61. package/dist/rich-text/index.js.map +7 -1
  62. package/dist/search/engine.js +93 -0
  63. package/dist/search/engine.js.map +7 -0
  64. package/dist/search/index.js +175 -0
  65. package/dist/search/index.js.map +7 -0
  66. package/dist/status/hours.js +177 -0
  67. package/dist/status/hours.js.map +7 -0
  68. package/dist/status/open-status.js +45 -0
  69. package/dist/status/open-status.js.map +7 -0
  70. package/dist/version.js +6 -0
  71. package/dist/version.js.map +7 -0
  72. package/package.json +5 -4
  73. package/dist/chunk-BY2WQGLO.js.map +0 -1
  74. package/dist/chunk-D3J7S33L.js.map +0 -1
@@ -0,0 +1,10 @@
1
+ // src/internal/ids.ts
2
+ import { useId } from "react";
3
+ function usePartId(provided) {
4
+ const id = useId();
5
+ return provided ?? `zappie-${id.replace(/[^a-zA-Z0-9_-]/g, "")}`;
6
+ }
7
+ export {
8
+ usePartId
9
+ };
10
+ //# sourceMappingURL=ids.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/internal/ids.ts"],
4
+ "sourcesContent": ["import { useId } from \"react\";\nexport function usePartId(provided?: string) {\n const id = useId();\n return provided ?? `zappie-${id.replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n}\n"],
5
+ "mappings": ";AAAA,SAAS,aAAa;AACf,SAAS,UAAU,UAAmB;AAC3C,QAAM,KAAK,MAAM;AACjB,SAAO,YAAY,UAAU,GAAG,QAAQ,mBAAmB,EAAE,CAAC;AAChE;",
6
+ "names": []
7
+ }
@@ -1,6 +1,10 @@
1
- import { Children, isValidElement, cloneElement, createElement } from 'react';
2
-
3
1
  // src/internal/slot.tsx
2
+ import {
3
+ Children,
4
+ cloneElement,
5
+ createElement,
6
+ isValidElement
7
+ } from "react";
4
8
  function composeRefs(...refs) {
5
9
  let cleanups = [];
6
10
  return (value) => {
@@ -75,7 +79,10 @@ function partId(props) {
75
79
  }
76
80
  return void 0;
77
81
  }
78
-
79
- export { composeRefs, part, partId };
80
- //# sourceMappingURL=chunk-D3J7S33L.js.map
81
- //# sourceMappingURL=chunk-D3J7S33L.js.map
82
+ export {
83
+ composeRefs,
84
+ mergeProps,
85
+ part,
86
+ partId
87
+ };
88
+ //# sourceMappingURL=slot.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/internal/slot.tsx"],
4
+ "sourcesContent": ["import {\n Children,\n cloneElement,\n createElement,\n isValidElement,\n type AllHTMLAttributes,\n type ElementType,\n type ReactElement,\n type ReactNode,\n type Ref,\n} from \"react\";\n/**\n * HTML attributes shared by foundation parts, with optional render-function children.\n * Set asChild to merge the part props and ref into a single child element.\n */\nexport type PartProps<S = Record<string, never>> = Omit<\n AllHTMLAttributes<HTMLElement>,\n \"children\" | \"defaultValue\" | \"value\"\n> & {\n asChild?: boolean;\n children?: ReactNode | ((state: S) => ReactNode);\n};\nexport function composeRefs<T>(...refs: (Ref<T> | undefined)[]): Ref<T> {\n let cleanups: (() => void)[] = [];\n return (value) => {\n for (const cleanup of cleanups) {\n cleanup();\n }\n cleanups = [];\n if (value === null) {\n return;\n }\n for (const ref of refs) {\n if (typeof ref === \"function\") {\n const cleanup = ref(value);\n cleanups.push(\n typeof cleanup === \"function\" ? cleanup : () => ref(null),\n );\n } else if (ref) {\n ref.current = value;\n cleanups.push(() => {\n ref.current = null;\n });\n }\n }\n };\n}\n/** Consumer handlers run first and may cancel the primitive's action. */\nexport function mergeProps(\n base: Record<string, unknown>,\n consumer: Record<string, unknown>,\n) {\n const result = { ...base, ...consumer };\n if (\n typeof base.className === \"string\" &&\n typeof consumer.className === \"string\"\n ) {\n result.className = `${base.className} ${consumer.className}`;\n }\n if (\n base.style &&\n consumer.style &&\n typeof base.style === \"object\" &&\n typeof consumer.style === \"object\"\n ) {\n result.style = { ...base.style, ...consumer.style };\n }\n for (const key of Object.keys(base)) {\n if (\n /^on[A-Z]/.test(key) &&\n typeof base[key] === \"function\" &&\n typeof consumer[key] === \"function\"\n ) {\n result[key] = (event: { defaultPrevented?: boolean }) => {\n (consumer[key] as (event: unknown) => void)(event);\n if (!event.defaultPrevented) {\n (base[key] as (event: unknown) => void)(event);\n }\n };\n }\n }\n return result;\n}\nexport function part<S>(\n tag: ElementType,\n props: PartProps<S>,\n state: S,\n defaults: Record<string, unknown> = {},\n ref?: Ref<HTMLElement>,\n) {\n const { asChild, children, ...dom } = props;\n const content = typeof children === \"function\" ? children(state) : children;\n const merged = mergeProps(defaults, dom);\n if (asChild || (tag === \"input\" && typeof children === \"function\")) {\n const child = Children.only(content);\n if (!isValidElement(child)) {\n throw new Error(\"asChild requires one React element\");\n }\n const element = child as ReactElement<Record<string, unknown>>;\n // React 18 stores ref on the element; React 19 stores it in props.\n const descriptor = Object.getOwnPropertyDescriptor(element, \"ref\");\n const childRef =\n element.props.ref ??\n (descriptor && \"value\" in descriptor ? descriptor.value : undefined);\n return cloneElement(element, {\n ...mergeProps(merged, element.props),\n ref: composeRefs(ref, childRef as Ref<HTMLElement>),\n });\n }\n return createElement(tag, { ...merged, ref }, content);\n}\n/** A part's caller-supplied id, whether passed as a prop or set on its `asChild` element. */\nexport function partId(props: {\n id?: string | undefined;\n asChild?: boolean | undefined;\n children?: unknown;\n}): string | undefined {\n if (props.id) {\n return props.id;\n }\n if (props.asChild && isValidElement(props.children)) {\n const id = (props.children.props as { id?: unknown }).id;\n return typeof id === \"string\" ? id : undefined;\n }\n return undefined;\n}\n"],
5
+ "mappings": ";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAMK;AAYA,SAAS,eAAkB,MAAsC;AACtE,MAAI,WAA2B,CAAC;AAChC,SAAO,CAAC,UAAU;AAChB,eAAW,WAAW,UAAU;AAC9B,cAAQ;AAAA,IACV;AACA,eAAW,CAAC;AACZ,QAAI,UAAU,MAAM;AAClB;AAAA,IACF;AACA,eAAW,OAAO,MAAM;AACtB,UAAI,OAAO,QAAQ,YAAY;AAC7B,cAAM,UAAU,IAAI,KAAK;AACzB,iBAAS;AAAA,UACP,OAAO,YAAY,aAAa,UAAU,MAAM,IAAI,IAAI;AAAA,QAC1D;AAAA,MACF,WAAW,KAAK;AACd,YAAI,UAAU;AACd,iBAAS,KAAK,MAAM;AAClB,cAAI,UAAU;AAAA,QAChB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,WACd,MACA,UACA;AACA,QAAM,SAAS,EAAE,GAAG,MAAM,GAAG,SAAS;AACtC,MACE,OAAO,KAAK,cAAc,YAC1B,OAAO,SAAS,cAAc,UAC9B;AACA,WAAO,YAAY,GAAG,KAAK,SAAS,IAAI,SAAS,SAAS;AAAA,EAC5D;AACA,MACE,KAAK,SACL,SAAS,SACT,OAAO,KAAK,UAAU,YACtB,OAAO,SAAS,UAAU,UAC1B;AACA,WAAO,QAAQ,EAAE,GAAG,KAAK,OAAO,GAAG,SAAS,MAAM;AAAA,EACpD;AACA,aAAW,OAAO,OAAO,KAAK,IAAI,GAAG;AACnC,QACE,WAAW,KAAK,GAAG,KACnB,OAAO,KAAK,GAAG,MAAM,cACrB,OAAO,SAAS,GAAG,MAAM,YACzB;AACA,aAAO,GAAG,IAAI,CAAC,UAA0C;AACvD,QAAC,SAAS,GAAG,EAA+B,KAAK;AACjD,YAAI,CAAC,MAAM,kBAAkB;AAC3B,UAAC,KAAK,GAAG,EAA+B,KAAK;AAAA,QAC/C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AACO,SAAS,KACd,KACA,OACA,OACA,WAAoC,CAAC,GACrC,KACA;AACA,QAAM,EAAE,SAAS,UAAU,GAAG,IAAI,IAAI;AACtC,QAAM,UAAU,OAAO,aAAa,aAAa,SAAS,KAAK,IAAI;AACnE,QAAM,SAAS,WAAW,UAAU,GAAG;AACvC,MAAI,WAAY,QAAQ,WAAW,OAAO,aAAa,YAAa;AAClE,UAAM,QAAQ,SAAS,KAAK,OAAO;AACnC,QAAI,CAAC,eAAe,KAAK,GAAG;AAC1B,YAAM,IAAI,MAAM,oCAAoC;AAAA,IACtD;AACA,UAAM,UAAU;AAEhB,UAAM,aAAa,OAAO,yBAAyB,SAAS,KAAK;AACjE,UAAM,WACJ,QAAQ,MAAM,QACb,cAAc,WAAW,aAAa,WAAW,QAAQ;AAC5D,WAAO,aAAa,SAAS;AAAA,MAC3B,GAAG,WAAW,QAAQ,QAAQ,KAAK;AAAA,MACnC,KAAK,YAAY,KAAK,QAA4B;AAAA,IACpD,CAAC;AAAA,EACH;AACA,SAAO,cAAc,KAAK,EAAE,GAAG,QAAQ,IAAI,GAAG,OAAO;AACvD;AAEO,SAAS,OAAO,OAIA;AACrB,MAAI,MAAM,IAAI;AACZ,WAAO,MAAM;AAAA,EACf;AACA,MAAI,MAAM,WAAW,eAAe,MAAM,QAAQ,GAAG;AACnD,UAAM,KAAM,MAAM,SAAS,MAA2B;AACtD,WAAO,OAAO,OAAO,WAAW,KAAK;AAAA,EACvC;AACA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,29 @@
1
+ // src/internal/use-controllable-state.ts
2
+ import { useCallback, useRef, useState } from "react";
3
+ function useControllableState({
4
+ value,
5
+ defaultValue,
6
+ onChange
7
+ }) {
8
+ const [internal, setInternal] = useState(defaultValue);
9
+ const current = value === void 0 ? internal : value;
10
+ const latest = useRef({ current, onChange, controlled: value !== void 0 });
11
+ latest.current = { current, onChange, controlled: value !== void 0 };
12
+ const set = useCallback((next) => {
13
+ const snapshot = latest.current;
14
+ const resolved = typeof next === "function" ? next(snapshot.current) : next;
15
+ if (Object.is(resolved, snapshot.current)) {
16
+ return;
17
+ }
18
+ if (!snapshot.controlled) {
19
+ latest.current.current = resolved;
20
+ setInternal(resolved);
21
+ }
22
+ snapshot.onChange?.(resolved);
23
+ }, []);
24
+ return [current, set];
25
+ }
26
+ export {
27
+ useControllableState
28
+ };
29
+ //# sourceMappingURL=use-controllable-state.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/internal/use-controllable-state.ts"],
4
+ "sourcesContent": ["import { useCallback, useRef, useState } from \"react\";\n/**\n * Manage either a supplied value or internal state initialized from defaultValue.\n * The setter accepts a value or updater and calls onChange only when Object.is detects a change.\n *\n * @returns The current value and a stable setter. A defined value keeps state controlled by you.\n */\nexport function useControllableState<T>({\n value,\n defaultValue,\n onChange,\n}: {\n value?: T | undefined;\n defaultValue: T;\n onChange?: ((value: T) => void) | undefined;\n}) {\n const [internal, setInternal] = useState(defaultValue);\n const current = value === undefined ? internal : value;\n const latest = useRef({ current, onChange, controlled: value !== undefined });\n latest.current = { current, onChange, controlled: value !== undefined };\n const set = useCallback((next: T | ((previous: T) => T)) => {\n const snapshot = latest.current;\n const resolved =\n typeof next === \"function\"\n ? (next as (previous: T) => T)(snapshot.current)\n : next;\n if (Object.is(resolved, snapshot.current)) {\n return;\n }\n if (!snapshot.controlled) {\n latest.current.current = resolved;\n setInternal(resolved);\n }\n snapshot.onChange?.(resolved);\n }, []);\n return [current, set] as const;\n}\n"],
5
+ "mappings": ";AAAA,SAAS,aAAa,QAAQ,gBAAgB;AAOvC,SAAS,qBAAwB;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,YAAY;AACrD,QAAM,UAAU,UAAU,SAAY,WAAW;AACjD,QAAM,SAAS,OAAO,EAAE,SAAS,UAAU,YAAY,UAAU,OAAU,CAAC;AAC5E,SAAO,UAAU,EAAE,SAAS,UAAU,YAAY,UAAU,OAAU;AACtE,QAAM,MAAM,YAAY,CAAC,SAAmC;AAC1D,UAAM,WAAW,OAAO;AACxB,UAAM,WACJ,OAAO,SAAS,aACX,KAA4B,SAAS,OAAO,IAC7C;AACN,QAAI,OAAO,GAAG,UAAU,SAAS,OAAO,GAAG;AACzC;AAAA,IACF;AACA,QAAI,CAAC,SAAS,YAAY;AACxB,aAAO,QAAQ,UAAU;AACzB,kBAAY,QAAQ;AAAA,IACtB;AACA,aAAS,WAAW,QAAQ;AAAA,EAC9B,GAAG,CAAC,CAAC;AACL,SAAO,CAAC,SAAS,GAAG;AACtB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,13 @@
1
+ // src/item-form/index.ts
2
+ import { Add, Options } from "./options.js";
3
+ import { Root } from "./root.js";
4
+ import { Choice, OptionGroup } from "./option-group.js";
5
+ import { Quantity } from "./quantity.js";
6
+ var ItemForm = { Root, Options, Add };
7
+ export {
8
+ Choice,
9
+ ItemForm,
10
+ OptionGroup,
11
+ Quantity
12
+ };
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/item-form/index.ts"],
4
+ "sourcesContent": ["import { Add, Options } from \"./options\";\nimport { Root } from \"./root\";\n/**\n * Composable item configuration parts for selecting options, validating a line and adding it to the cart.\n * Use ItemForm.Root within your ZappieProvider and place Options and Add inside it.\n */\nexport const ItemForm = { Root, Options, Add };\nexport { Choice, OptionGroup } from \"./option-group\";\nexport { Quantity } from \"./quantity\";\nexport type { QuantityProps, QuantityState } from \"./quantity\";\nexport type { ActiveGroup, ItemFormContext, ItemFormProps } from \"./root\";\n"],
5
+ "mappings": ";AAAA,SAAS,KAAK,eAAe;AAC7B,SAAS,YAAY;AAMrB,SAAS,QAAQ,mBAAmB;AACpC,SAAS,gBAAgB;AAFlB,IAAM,WAAW,EAAE,MAAM,SAAS,IAAI;",
6
+ "names": []
7
+ }
@@ -0,0 +1,268 @@
1
+ "use client";
2
+
3
+ // src/item-form/option-group.tsx
4
+ import {
5
+ createContext,
6
+ forwardRef,
7
+ memo,
8
+ useContext,
9
+ useMemo,
10
+ useRef
11
+ } from "react";
12
+ import { flag } from "../internal/data-attrs.js";
13
+ import { usePartId } from "../internal/ids.js";
14
+ import { composeRefs, part } from "../internal/slot.js";
15
+ import { GroupRegistry, useItemForm } from "./root.js";
16
+ import { jsx } from "react/jsx-runtime";
17
+ var Context = createContext(null);
18
+ function useGroup() {
19
+ const value = useContext(Context);
20
+ if (!value) {
21
+ throw new Error("Choice and OptionGroup.Input require OptionGroup");
22
+ }
23
+ return value;
24
+ }
25
+ var Group = forwardRef(function OptionGroup({ group: supplied, ...props }, ref) {
26
+ const ctx = useItemForm();
27
+ const registry = useContext(GroupRegistry);
28
+ const group = ctx.groups.find((candidate) => candidate.id === supplied.id) ?? supplied;
29
+ const mine = ctx.value.choices?.filter((choice) => choice.groupId === group.id) ?? [];
30
+ const selected = mine.map((choice) => choice.choiceId);
31
+ const quantities = Object.fromEntries(
32
+ mine.map((choice) => [choice.choiceId, choice.quantity ?? 1])
33
+ );
34
+ const invalid = ctx.issues.some((issue) => issue.groupId === group.id);
35
+ const input = ctx.value.inputs?.[group.id] ?? "";
36
+ const latest = useRef({ ctx, group });
37
+ latest.current = { ctx, group };
38
+ const actions = useMemo(
39
+ () => ({
40
+ toggle: (id) => {
41
+ const { ctx: ctx2, group: group2 } = latest.current;
42
+ const choice = group2.choices.find((candidate) => candidate.id === id);
43
+ if (!choice?.isAvailable) {
44
+ return;
45
+ }
46
+ const previous = ctx2.value.choices ?? [];
47
+ const chosen = previous.some(
48
+ (candidate) => candidate.groupId === group2.id && candidate.choiceId === id
49
+ );
50
+ let choices = previous.filter(
51
+ (candidate) => candidate.groupId !== group2.id || group2.selection.max !== 1 && candidate.choiceId !== id
52
+ );
53
+ if (!chosen || group2.selection.max === 1 && group2.required) {
54
+ choices = [
55
+ ...choices,
56
+ {
57
+ groupId: group2.id,
58
+ choiceId: id,
59
+ quantity: choice.quantity?.min ?? 1
60
+ }
61
+ ];
62
+ }
63
+ const inputs = { ...ctx2.value.inputs };
64
+ delete inputs[group2.id];
65
+ ctx2.setValue({ ...ctx2.value, choices, inputs });
66
+ },
67
+ setQuantity: (id, quantity) => {
68
+ const { ctx: ctx2, group: group2 } = latest.current;
69
+ const choice = group2.choices.find((candidate) => candidate.id === id);
70
+ if (!choice?.isAvailable || !Number.isInteger(quantity)) {
71
+ return;
72
+ }
73
+ const others = (ctx2.value.choices ?? []).filter(
74
+ (candidate) => candidate.groupId !== group2.id || group2.selection.max !== 1 && candidate.choiceId !== id
75
+ );
76
+ if (quantity <= 0) {
77
+ ctx2.setValue({
78
+ ...ctx2.value,
79
+ choices: (ctx2.value.choices ?? []).filter(
80
+ (candidate) => candidate.groupId !== group2.id || candidate.choiceId !== id
81
+ )
82
+ });
83
+ return;
84
+ }
85
+ const min = choice.quantity?.min ?? 1;
86
+ const max = choice.quantity?.max ?? 99;
87
+ ctx2.setValue({
88
+ ...ctx2.value,
89
+ choices: [
90
+ ...others,
91
+ {
92
+ groupId: group2.id,
93
+ choiceId: id,
94
+ quantity: Math.min(max, Math.max(min, quantity))
95
+ }
96
+ ]
97
+ });
98
+ },
99
+ setInput: (value) => {
100
+ const { ctx: ctx2, group: group2 } = latest.current;
101
+ ctx2.setValue({
102
+ ...ctx2.value,
103
+ choices: (ctx2.value.choices ?? []).filter(
104
+ (choice) => choice.groupId !== group2.id
105
+ ),
106
+ inputs: {
107
+ ...ctx2.value.inputs,
108
+ [group2.id]: value.slice(0, group2.input?.maxLength ?? 500)
109
+ }
110
+ });
111
+ }
112
+ }),
113
+ []
114
+ );
115
+ const selectionKey = JSON.stringify(quantities);
116
+ const state = useMemo(
117
+ () => ({ group, selected, quantities, invalid, input, ...actions }),
118
+ [group, selectionKey, invalid, input, actions]
119
+ );
120
+ 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;
144
+ }
145
+ const choices = Array.from(
146
+ event.currentTarget.querySelectorAll(
147
+ '[role="radio"]:not([aria-disabled="true"])'
148
+ )
149
+ );
150
+ if (!choices.length) {
151
+ return;
152
+ }
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
+ ) });
170
+ });
171
+ var ChoiceView = memo(
172
+ forwardRef(function ChoiceView2({
173
+ choice,
174
+ selected,
175
+ single,
176
+ tabIndex,
177
+ quantity,
178
+ toggleChoice,
179
+ setChoiceQuantity,
180
+ ...props
181
+ }, ref) {
182
+ const toggle = () => toggleChoice(choice.id);
183
+ const state = {
184
+ choice,
185
+ selected,
186
+ disabled: !choice.isAvailable,
187
+ toggle,
188
+ quantity,
189
+ min: choice.quantity?.min ?? 1,
190
+ max: choice.quantity?.max ?? 99,
191
+ setQuantity: (next) => setChoiceQuantity(choice.id, next)
192
+ };
193
+ return part(
194
+ "button",
195
+ props,
196
+ state,
197
+ {
198
+ type: "button",
199
+ role: single ? "radio" : "checkbox",
200
+ tabIndex,
201
+ "aria-checked": selected,
202
+ "aria-disabled": !choice.isAvailable,
203
+ disabled: !choice.isAvailable,
204
+ "data-selected": flag(selected),
205
+ "data-sold-out": flag(!choice.isAvailable),
206
+ "data-disabled": flag(!choice.isAvailable),
207
+ "data-state": selected ? "checked" : "unchecked",
208
+ "data-zappie-option": choice.id,
209
+ onClick: toggle
210
+ },
211
+ ref
212
+ );
213
+ })
214
+ );
215
+ var Choice = forwardRef(function Choice2(props, ref) {
216
+ const ctx = useGroup();
217
+ const single = ctx.group.selection.max === 1;
218
+ const selected = ctx.selected.includes(props.choice.id);
219
+ const first = ctx.group.choices.find((choice) => choice.isAvailable);
220
+ const tabIndex = !single || selected || !ctx.selected.length && first?.id === props.choice.id ? 0 : -1;
221
+ return /* @__PURE__ */ jsx(
222
+ ChoiceView,
223
+ {
224
+ ...props,
225
+ ref,
226
+ selected,
227
+ single,
228
+ tabIndex,
229
+ quantity: ctx.quantities[props.choice.id] ?? 0,
230
+ toggleChoice: ctx.toggle,
231
+ setChoiceQuantity: ctx.setQuantity
232
+ }
233
+ );
234
+ });
235
+ var Input = forwardRef(
236
+ function Input2(props, ref) {
237
+ const ctx = useGroup();
238
+ const id = usePartId(props.id);
239
+ const toggle = ctx.group.kind === "toggle";
240
+ return part(
241
+ "input",
242
+ props,
243
+ ctx,
244
+ {
245
+ id,
246
+ type: toggle ? "checkbox" : ctx.group.kind === "select" ? "text" : ctx.group.kind,
247
+ value: toggle ? void 0 : ctx.input,
248
+ checked: toggle ? ctx.input === "true" : void 0,
249
+ required: ctx.group.required,
250
+ "aria-label": ctx.group.name,
251
+ "aria-invalid": ctx.invalid || void 0,
252
+ "data-invalid": flag(ctx.invalid),
253
+ maxLength: ctx.group.input?.maxLength ?? 500,
254
+ placeholder: ctx.group.input?.placeholder,
255
+ onChange: (event) => ctx.setInput(
256
+ toggle ? String(event.target.checked) : event.target.value
257
+ )
258
+ },
259
+ ref
260
+ );
261
+ }
262
+ );
263
+ var OptionGroup2 = Object.assign(Group, { Input });
264
+ export {
265
+ Choice,
266
+ OptionGroup2 as OptionGroup
267
+ };
268
+ //# sourceMappingURL=option-group.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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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",
6
+ "names": ["ctx", "group", "ChoiceView", "Choice", "Input", "OptionGroup"]
7
+ }
@@ -0,0 +1,30 @@
1
+ // src/item-form/options.tsx
2
+ import { forwardRef } from "react";
3
+ import { part } from "../internal/slot.js";
4
+ import { useItemForm } from "./root.js";
5
+ var Options = forwardRef(
6
+ function Options2(props, ref) {
7
+ return part("div", props, useItemForm(), {}, ref);
8
+ }
9
+ );
10
+ var Add = forwardRef(
11
+ function Add2(props, ref) {
12
+ const context = useItemForm();
13
+ return part(
14
+ "button",
15
+ props,
16
+ context,
17
+ {
18
+ type: "button",
19
+ onClick: context.add,
20
+ "data-zappie-action": "add-to-cart"
21
+ },
22
+ ref
23
+ );
24
+ }
25
+ );
26
+ export {
27
+ Add,
28
+ Options
29
+ };
30
+ //# sourceMappingURL=options.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/item-form/options.tsx"],
4
+ "sourcesContent": ["import { forwardRef } from \"react\";\nimport { part, type PartProps } from \"../internal/slot\";\nimport { useItemForm, type ItemFormContext } from \"./root\";\nexport const Options = forwardRef<HTMLElement, PartProps<ItemFormContext>>(\n function Options(props, ref) {\n return part(\"div\", props, useItemForm(), {}, ref);\n },\n);\nexport const Add = forwardRef<HTMLElement, PartProps<ItemFormContext>>(\n function Add(props, ref) {\n const context = useItemForm();\n // Keep invalid configurations actionable so add() can explain and focus errors.\n return part(\n \"button\",\n props,\n context,\n {\n type: \"button\",\n onClick: context.add,\n \"data-zappie-action\": \"add-to-cart\",\n },\n ref,\n );\n },\n);\n"],
5
+ "mappings": ";AAAA,SAAS,kBAAkB;AAC3B,SAAS,YAA4B;AACrC,SAAS,mBAAyC;AAC3C,IAAM,UAAU;AAAA,EACrB,SAASA,SAAQ,OAAO,KAAK;AAC3B,WAAO,KAAK,OAAO,OAAO,YAAY,GAAG,CAAC,GAAG,GAAG;AAAA,EAClD;AACF;AACO,IAAM,MAAM;AAAA,EACjB,SAASC,KAAI,OAAO,KAAK;AACvB,UAAM,UAAU,YAAY;AAE5B,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,SAAS,QAAQ;AAAA,QACjB,sBAAsB;AAAA,MACxB;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;",
6
+ "names": ["Options", "Add"]
7
+ }
@@ -0,0 +1,131 @@
1
+ "use client";
2
+
3
+ // src/item-form/quantity.tsx
4
+ import {
5
+ createContext,
6
+ forwardRef,
7
+ useCallback,
8
+ useContext,
9
+ useMemo
10
+ } from "react";
11
+ import { flag } from "../internal/data-attrs.js";
12
+ import { part } from "../internal/slot.js";
13
+ import { useControllableState } from "../internal/use-controllable-state.js";
14
+ import { jsx } from "react/jsx-runtime";
15
+ var Context = createContext(null);
16
+ function useQuantity() {
17
+ const context = useContext(Context);
18
+ if (!context) {
19
+ throw new Error("Quantity parts require Quantity.Root");
20
+ }
21
+ return context;
22
+ }
23
+ function Root({
24
+ value,
25
+ defaultValue,
26
+ onValueChange,
27
+ min = 1,
28
+ max = 99,
29
+ step = 1,
30
+ disabled = false,
31
+ ...props
32
+ }) {
33
+ if (!Number.isFinite(min) || !Number.isFinite(max) || !Number.isFinite(step) || step <= 0 || min > max) {
34
+ throw new RangeError(
35
+ "Quantity requires finite ordered bounds and a positive step"
36
+ );
37
+ }
38
+ const [current, set] = useControllableState({
39
+ value,
40
+ defaultValue: defaultValue ?? min,
41
+ onChange: onValueChange
42
+ });
43
+ const setValue = useCallback(
44
+ (next) => {
45
+ if (!disabled && Number.isFinite(next)) {
46
+ const last = min + Math.floor((max - min) / step) * step;
47
+ set(
48
+ Math.max(
49
+ min,
50
+ Math.min(last, min + Math.round((next - min) / step) * step)
51
+ )
52
+ );
53
+ }
54
+ },
55
+ [min, max, step, disabled, set]
56
+ );
57
+ const state = useMemo(
58
+ () => ({ value: current, setValue, min, max, step, disabled }),
59
+ [current, setValue, min, max, step, disabled]
60
+ );
61
+ return /* @__PURE__ */ jsx(Context.Provider, { value: state, children: part("div", props, state, { "data-disabled": flag(disabled) }) });
62
+ }
63
+ function button(direction) {
64
+ return forwardRef(
65
+ function QuantityButton(props, ref) {
66
+ const ctx = useQuantity();
67
+ const disabled = ctx.disabled || (direction < 0 ? ctx.value <= ctx.min : ctx.value + ctx.step > ctx.max);
68
+ return part(
69
+ "button",
70
+ props,
71
+ ctx,
72
+ {
73
+ type: "button",
74
+ disabled,
75
+ "aria-disabled": disabled,
76
+ "data-disabled": flag(disabled),
77
+ onClick: () => ctx.setValue(ctx.value + direction * ctx.step)
78
+ },
79
+ ref
80
+ );
81
+ }
82
+ );
83
+ }
84
+ var Value = forwardRef(
85
+ function Value2(props, ref) {
86
+ const ctx = useQuantity();
87
+ return part(
88
+ "input",
89
+ props,
90
+ ctx,
91
+ {
92
+ type: "text",
93
+ inputMode: "numeric",
94
+ role: "spinbutton",
95
+ value: String(ctx.value),
96
+ disabled: ctx.disabled,
97
+ "aria-valuenow": ctx.value,
98
+ "aria-valuemin": ctx.min,
99
+ "aria-valuemax": ctx.max,
100
+ onChange: (event) => {
101
+ if (event.target.value.trim()) {
102
+ ctx.setValue(Number(event.target.value));
103
+ }
104
+ },
105
+ onKeyDown: (event) => {
106
+ const next = {
107
+ ArrowUp: ctx.value + ctx.step,
108
+ ArrowDown: ctx.value - ctx.step,
109
+ Home: ctx.min,
110
+ End: ctx.max
111
+ }[event.key];
112
+ if (next !== void 0) {
113
+ event.preventDefault();
114
+ ctx.setValue(next);
115
+ }
116
+ }
117
+ },
118
+ ref
119
+ );
120
+ }
121
+ );
122
+ var Quantity = {
123
+ Root,
124
+ Value,
125
+ Increment: button(1),
126
+ Decrement: button(-1)
127
+ };
128
+ export {
129
+ Quantity
130
+ };
131
+ //# sourceMappingURL=quantity.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/item-form/quantity.tsx"],
4
+ "sourcesContent": ["\"use client\";\nimport {\n createContext,\n forwardRef,\n useCallback,\n useContext,\n useMemo,\n type ChangeEvent,\n type KeyboardEvent,\n} from \"react\";\nimport { flag } from \"../internal/data-attrs\";\nimport { part, type PartProps } from \"../internal/slot\";\nimport { useControllableState } from \"../internal/use-controllable-state\";\n/**\n * The current quantity, bounds, step, disabled state and value setter exposed by a quantity control.\n */\nexport type QuantityState = {\n value: number;\n setValue: (value: number) => void;\n min: number;\n max: number;\n step: number;\n disabled: boolean;\n};\nconst Context = createContext<QuantityState | null>(null);\nfunction useQuantity() {\n const context = useContext(Context);\n if (!context) {\n throw new Error(\"Quantity parts require Quantity.Root\");\n }\n return context;\n}\n/**\n * Controlled or default quantity, bounds and change callback for Quantity.Root.\n */\nexport type QuantityProps = PartProps<QuantityState> & {\n value?: number;\n defaultValue?: number;\n onValueChange?: (value: number) => void;\n min?: number;\n max?: number;\n step?: number;\n disabled?: boolean;\n};\nfunction Root({\n value,\n defaultValue,\n onValueChange,\n min = 1,\n max = 99,\n step = 1,\n disabled = false,\n ...props\n}: QuantityProps) {\n if (\n !Number.isFinite(min) ||\n !Number.isFinite(max) ||\n !Number.isFinite(step) ||\n step <= 0 ||\n min > max\n ) {\n throw new RangeError(\n \"Quantity requires finite ordered bounds and a positive step\",\n );\n }\n const [current, set] = useControllableState({\n value,\n defaultValue: defaultValue ?? min,\n onChange: onValueChange,\n });\n const setValue = useCallback(\n (next: number) => {\n if (!disabled && Number.isFinite(next)) {\n const last = min + Math.floor((max - min) / step) * step;\n set(\n Math.max(\n min,\n Math.min(last, min + Math.round((next - min) / step) * step),\n ),\n );\n }\n },\n [min, max, step, disabled, set],\n );\n const state = useMemo(\n () => ({ value: current, setValue, min, max, step, disabled }),\n [current, setValue, min, max, step, disabled],\n );\n return (\n <Context.Provider value={state}>\n {part(\"div\", props, state, { \"data-disabled\": flag(disabled) })}\n </Context.Provider>\n );\n}\nfunction button(direction: -1 | 1) {\n return forwardRef<HTMLElement, PartProps<QuantityState>>(\n function QuantityButton(props, ref) {\n const ctx = useQuantity();\n const disabled =\n ctx.disabled ||\n (direction < 0 ? ctx.value <= ctx.min : ctx.value + ctx.step > ctx.max);\n return part(\n \"button\",\n props,\n ctx,\n {\n type: \"button\",\n disabled,\n \"aria-disabled\": disabled,\n \"data-disabled\": flag(disabled),\n onClick: () => ctx.setValue(ctx.value + direction * ctx.step),\n },\n ref,\n );\n },\n );\n}\nconst Value = forwardRef<HTMLElement, PartProps<QuantityState>>(\n function Value(props, ref) {\n const ctx = useQuantity();\n return part(\n \"input\",\n props,\n ctx,\n {\n type: \"text\",\n inputMode: \"numeric\",\n role: \"spinbutton\",\n value: String(ctx.value),\n disabled: ctx.disabled,\n \"aria-valuenow\": ctx.value,\n \"aria-valuemin\": ctx.min,\n \"aria-valuemax\": ctx.max,\n onChange: (event: ChangeEvent<HTMLInputElement>) => {\n if (event.target.value.trim()) {\n ctx.setValue(Number(event.target.value));\n }\n },\n onKeyDown: (event: KeyboardEvent) => {\n const next = {\n ArrowUp: ctx.value + ctx.step,\n ArrowDown: ctx.value - ctx.step,\n Home: ctx.min,\n End: ctx.max,\n }[event.key];\n if (next !== undefined) {\n event.preventDefault();\n ctx.setValue(next);\n }\n },\n },\n ref,\n );\n },\n);\n/**\n * Composable quantity controls with bounded step changes and keyboard support.\n * Wrap Value, Increment and Decrement in Quantity.Root.\n *\n * @example\n * ```tsx\n * <Quantity.Root min={1} max={10} defaultValue={1}>\n * <Quantity.Decrement>Less</Quantity.Decrement>\n * <Quantity.Value aria-label=\"Quantity\" />\n * <Quantity.Increment>More</Quantity.Increment>\n * </Quantity.Root>\n * ```\n */\nexport const Quantity = {\n Root,\n Value,\n Increment: button(1),\n Decrement: button(-1),\n};\n"],
5
+ "mappings": ";;;AACA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP,SAAS,YAAY;AACrB,SAAS,YAA4B;AACrC,SAAS,4BAA4B;AA6EjC;AAjEJ,IAAM,UAAU,cAAoC,IAAI;AACxD,SAAS,cAAc;AACrB,QAAM,UAAU,WAAW,OAAO;AAClC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,sCAAsC;AAAA,EACxD;AACA,SAAO;AACT;AAaA,SAAS,KAAK;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,MAAM;AAAA,EACN,OAAO;AAAA,EACP,WAAW;AAAA,EACX,GAAG;AACL,GAAkB;AAChB,MACE,CAAC,OAAO,SAAS,GAAG,KACpB,CAAC,OAAO,SAAS,GAAG,KACpB,CAAC,OAAO,SAAS,IAAI,KACrB,QAAQ,KACR,MAAM,KACN;AACA,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,QAAM,CAAC,SAAS,GAAG,IAAI,qBAAqB;AAAA,IAC1C;AAAA,IACA,cAAc,gBAAgB;AAAA,IAC9B,UAAU;AAAA,EACZ,CAAC;AACD,QAAM,WAAW;AAAA,IACf,CAAC,SAAiB;AAChB,UAAI,CAAC,YAAY,OAAO,SAAS,IAAI,GAAG;AACtC,cAAM,OAAO,MAAM,KAAK,OAAO,MAAM,OAAO,IAAI,IAAI;AACpD;AAAA,UACE,KAAK;AAAA,YACH;AAAA,YACA,KAAK,IAAI,MAAM,MAAM,KAAK,OAAO,OAAO,OAAO,IAAI,IAAI,IAAI;AAAA,UAC7D;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA,CAAC,KAAK,KAAK,MAAM,UAAU,GAAG;AAAA,EAChC;AACA,QAAM,QAAQ;AAAA,IACZ,OAAO,EAAE,OAAO,SAAS,UAAU,KAAK,KAAK,MAAM,SAAS;AAAA,IAC5D,CAAC,SAAS,UAAU,KAAK,KAAK,MAAM,QAAQ;AAAA,EAC9C;AACA,SACE,oBAAC,QAAQ,UAAR,EAAiB,OAAO,OACtB,eAAK,OAAO,OAAO,OAAO,EAAE,iBAAiB,KAAK,QAAQ,EAAE,CAAC,GAChE;AAEJ;AACA,SAAS,OAAO,WAAmB;AACjC,SAAO;AAAA,IACL,SAAS,eAAe,OAAO,KAAK;AAClC,YAAM,MAAM,YAAY;AACxB,YAAM,WACJ,IAAI,aACH,YAAY,IAAI,IAAI,SAAS,IAAI,MAAM,IAAI,QAAQ,IAAI,OAAO,IAAI;AACrE,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,UACE,MAAM;AAAA,UACN;AAAA,UACA,iBAAiB;AAAA,UACjB,iBAAiB,KAAK,QAAQ;AAAA,UAC9B,SAAS,MAAM,IAAI,SAAS,IAAI,QAAQ,YAAY,IAAI,IAAI;AAAA,QAC9D;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AACA,IAAM,QAAQ;AAAA,EACZ,SAASA,OAAM,OAAO,KAAK;AACzB,UAAM,MAAM,YAAY;AACxB,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,WAAW;AAAA,QACX,MAAM;AAAA,QACN,OAAO,OAAO,IAAI,KAAK;AAAA,QACvB,UAAU,IAAI;AAAA,QACd,iBAAiB,IAAI;AAAA,QACrB,iBAAiB,IAAI;AAAA,QACrB,iBAAiB,IAAI;AAAA,QACrB,UAAU,CAAC,UAAyC;AAClD,cAAI,MAAM,OAAO,MAAM,KAAK,GAAG;AAC7B,gBAAI,SAAS,OAAO,MAAM,OAAO,KAAK,CAAC;AAAA,UACzC;AAAA,QACF;AAAA,QACA,WAAW,CAAC,UAAyB;AACnC,gBAAM,OAAO;AAAA,YACX,SAAS,IAAI,QAAQ,IAAI;AAAA,YACzB,WAAW,IAAI,QAAQ,IAAI;AAAA,YAC3B,MAAM,IAAI;AAAA,YACV,KAAK,IAAI;AAAA,UACX,EAAE,MAAM,GAAG;AACX,cAAI,SAAS,QAAW;AACtB,kBAAM,eAAe;AACrB,gBAAI,SAAS,IAAI;AAAA,UACnB;AAAA,QACF;AAAA,MACF;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAcO,IAAM,WAAW;AAAA,EACtB;AAAA,EACA;AAAA,EACA,WAAW,OAAO,CAAC;AAAA,EACnB,WAAW,OAAO,EAAE;AACtB;",
6
+ "names": ["Value"]
7
+ }