@zappie/foundations 0.1.3 → 0.1.5
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 +24 -0
- package/README.md +154 -7
- package/dist/branch-map/colors.js +27 -0
- package/dist/branch-map/colors.js.map +7 -0
- package/dist/branch-map/delivery-map.js +60 -0
- package/dist/branch-map/delivery-map.js.map +7 -0
- package/dist/branch-map/delivery-runtime.js +118 -0
- package/dist/branch-map/delivery-runtime.js.map +7 -0
- package/dist/branch-map/fit.js +123 -0
- package/dist/branch-map/fit.js.map +7 -0
- package/dist/branch-map/geometry.js +1 -27
- package/dist/branch-map/geometry.js.map +2 -2
- package/dist/branch-map/index.js +42 -36
- package/dist/branch-map/index.js.map +2 -2
- package/dist/branch-map/markers.js +143 -0
- package/dist/branch-map/markers.js.map +7 -0
- package/dist/branch-map/runtime.js +258 -53
- package/dist/branch-map/runtime.js.map +3 -3
- package/dist/branch-map/use-map-runtime.js +53 -0
- package/dist/branch-map/use-map-runtime.js.map +7 -0
- package/dist/deferred/index.js +111 -0
- package/dist/deferred/index.js.map +7 -0
- package/dist/index.d.ts +179 -8
- package/dist/index.js +5 -1
- package/dist/index.js.map +2 -2
- package/dist/item-form/index.js.map +1 -1
- package/dist/item-form/root.js +25 -8
- package/dist/item-form/root.js.map +2 -2
- package/dist/overlay/react.js +245 -24
- package/dist/overlay/react.js.map +3 -3
- package/dist/version.js +1 -1
- package/package.json +3 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { HTMLAttributes, KeyboardEvent as KeyboardEvent$1, ImgHTMLAttributes, ChangeEvent, ClipboardEvent } from 'react';
|
|
2
|
+
import { HTMLAttributes, ReactNode, ComponentType, ReactElement, KeyboardEvent as KeyboardEvent$1, ImgHTMLAttributes, ChangeEvent, ClipboardEvent } from 'react';
|
|
3
3
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
4
|
import * as _zappie_sdk from '@zappie/sdk';
|
|
5
5
|
import { Branch as Branch$1, CartLine, CartLineInput, CartIssue, CartFix, Quote, DeliveryAddress, MenuSchedule, CartFulfilment, OptionGroup as OptionGroup$1, Item, Money as Money$1, Choice as Choice$1, MoneyFormatOptions, PaymentMethodKind, Category, Menu, OpeningTime } from '@zappie/sdk';
|
|
@@ -68,20 +68,92 @@ type Coordinates = {
|
|
|
68
68
|
lng: number;
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
+
/** What a template gets to render when a map fails to load. */
|
|
72
|
+
type MapLoadErrorState = {
|
|
73
|
+
/** Why the map failed to load. */
|
|
74
|
+
error: unknown;
|
|
75
|
+
/** Load the map again. */
|
|
76
|
+
retry: () => void;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** The branch, the shopper's address and an optional route for DeliveryMap. */
|
|
80
|
+
type DeliveryMapProps = HTMLAttributes<HTMLDivElement> & {
|
|
81
|
+
/** The branch the order leaves from. Only its coordinates are read. */
|
|
82
|
+
branch: Pick<Branch$1, "coordinates">;
|
|
83
|
+
/** Coordinates of the delivery address. */
|
|
84
|
+
shopper?: Coordinates | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* The path to draw, such as a decoded driving route, from the branch to the
|
|
87
|
+
* shopper. Without it the map draws a straight line.
|
|
88
|
+
*/
|
|
89
|
+
route?: Coordinates[];
|
|
90
|
+
browserKey?: string;
|
|
91
|
+
/** Image shown inside the branch marker, such as the business logo. */
|
|
92
|
+
pinImageUrl?: string;
|
|
93
|
+
/** Called each time the map fails to load. */
|
|
94
|
+
onLoadError?: (error: unknown) => void;
|
|
95
|
+
/**
|
|
96
|
+
* Render your own message when the map fails to load, such as a retry
|
|
97
|
+
* button that calls `retry`. Without it the map shows "Map location unavailable".
|
|
98
|
+
*/
|
|
99
|
+
renderLoadError?: (state: MapLoadErrorState) => ReactNode;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* A static map of a delivery: the branch marker, the shopper's house marker
|
|
103
|
+
* and the line between them, fitted to both. It has no controls and ignores
|
|
104
|
+
* gestures. Maps loads after mount when you supply a browser key. Set the
|
|
105
|
+
* container height with CSS. Colour the house marker with --map-accent and
|
|
106
|
+
* the line with --delivery-map-route.
|
|
107
|
+
*/
|
|
108
|
+
declare function DeliveryMap({ branch, shopper, route, browserKey, pinImageUrl, onLoadError, renderLoadError, ...props }: DeliveryMapProps): react_jsx_runtime.JSX.Element | null;
|
|
109
|
+
|
|
110
|
+
/** What a template gets to render when the map fails to load. */
|
|
111
|
+
type BranchMapLoadErrorState = MapLoadErrorState;
|
|
71
112
|
/** Branches, selection and optional shopper coordinates for BranchMap. */
|
|
72
113
|
type BranchMapProps = Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> & {
|
|
73
114
|
branches: Branch$1[];
|
|
74
115
|
selectedBranchId?: string;
|
|
75
116
|
onSelect: (branch: Branch$1) => void;
|
|
117
|
+
/**
|
|
118
|
+
* The shopper's coordinates. The map shows them with a house marker and
|
|
119
|
+
* draws a line to the selected branch, or to the nearest one.
|
|
120
|
+
*/
|
|
76
121
|
shopperLocation?: Coordinates;
|
|
77
122
|
browserKey?: string;
|
|
123
|
+
/**
|
|
124
|
+
* Highest zoom applied after fitting. Defaults to 15 when a shopper location
|
|
125
|
+
* is shown and 14 otherwise.
|
|
126
|
+
*/
|
|
127
|
+
maxZoom?: number;
|
|
128
|
+
/** Image shown inside each branch pin, such as the business logo. */
|
|
129
|
+
pinImageUrl?: string;
|
|
130
|
+
/** Show the map and satellite switch. Defaults to false. */
|
|
131
|
+
mapTypeControl?: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Show the branch name above the selected pin, or above every pin. Off by
|
|
134
|
+
* default. With a shopper location and no selection, "selected" labels the
|
|
135
|
+
* nearest branch.
|
|
136
|
+
*/
|
|
137
|
+
pinLabel?: "selected" | "all";
|
|
138
|
+
/** Frame a small map, with tighter padding and a wider single-branch view. */
|
|
139
|
+
compact?: boolean;
|
|
140
|
+
/** Called each time the map fails to load. */
|
|
141
|
+
onLoadError?: (error: unknown) => void;
|
|
142
|
+
/**
|
|
143
|
+
* Render your own message when the map fails to load, such as a retry
|
|
144
|
+
* button that calls `retry`. Without it the map shows "Map location unavailable".
|
|
145
|
+
*/
|
|
146
|
+
renderLoadError?: (state: BranchMapLoadErrorState) => ReactNode;
|
|
78
147
|
};
|
|
79
148
|
/**
|
|
80
149
|
* Show branch locations. Maps loads after mount when you supply a browser key.
|
|
81
|
-
*
|
|
82
|
-
*
|
|
150
|
+
* Nearby branches group into numbered clusters. Set the container height with
|
|
151
|
+
* CSS. Colour clusters, the shopper marker, labels and the route with
|
|
152
|
+
* --map-accent and --map-accent-foreground. Style dot pins with
|
|
153
|
+
* --branch-map-pin, --branch-map-pin-selected, --branch-map-pin-border and
|
|
154
|
+
* --branch-map-pin-size.
|
|
83
155
|
*/
|
|
84
|
-
declare function BranchMap({ branches, selectedBranchId, onSelect, shopperLocation, browserKey, ...props }: BranchMapProps): react_jsx_runtime.JSX.Element | null;
|
|
156
|
+
declare function BranchMap({ branches, selectedBranchId, onSelect, shopperLocation, browserKey, maxZoom, pinImageUrl, mapTypeControl, pinLabel, compact, onLoadError, renderLoadError, ...props }: BranchMapProps): react_jsx_runtime.JSX.Element | null;
|
|
85
157
|
|
|
86
158
|
/**
|
|
87
159
|
* The current quantity, bounds, step, disabled state and value setter exposed by a quantity control.
|
|
@@ -194,6 +266,87 @@ declare const Totals: react.ForwardRefExoticComponent<Omit<react.AllHTMLAttribut
|
|
|
194
266
|
children?: react.ReactNode | ((state: TotalsState) => react.ReactNode);
|
|
195
267
|
} & react.RefAttributes<HTMLElement>>;
|
|
196
268
|
|
|
269
|
+
/** Props for a deferred component: its own props plus `open`, which the helper reads and passes on. */
|
|
270
|
+
type DeferredProps<P> = Omit<P, "open"> & {
|
|
271
|
+
/**
|
|
272
|
+
* Load the code if needed and render the component. Before the first open
|
|
273
|
+
* it renders nothing; after it, the component stays mounted and receives
|
|
274
|
+
* `open`, so it can animate out and return focus when it closes.
|
|
275
|
+
*/
|
|
276
|
+
open: boolean;
|
|
277
|
+
/** Shown while `open` is true and the code is still loading, for inline parts that should hold their place. */
|
|
278
|
+
fallback?: ReactNode;
|
|
279
|
+
};
|
|
280
|
+
/** Options for `preloadWhenIdle`. */
|
|
281
|
+
type DeferredIdleOptions = {
|
|
282
|
+
/** Start the load after this many milliseconds even if the browser never goes idle. */
|
|
283
|
+
timeout?: number;
|
|
284
|
+
};
|
|
285
|
+
/**
|
|
286
|
+
* A component whose code loads on demand and renders without suspending.
|
|
287
|
+
* Create it once at module scope with `deferred()`.
|
|
288
|
+
*/
|
|
289
|
+
type Deferred<P> = {
|
|
290
|
+
/**
|
|
291
|
+
* Start loading the code. Repeated calls share one load. A failed load is
|
|
292
|
+
* forgotten, so the next call tries again.
|
|
293
|
+
* @returns The loaded component, or the load error.
|
|
294
|
+
*/
|
|
295
|
+
preload(): Promise<ComponentType<P>>;
|
|
296
|
+
/**
|
|
297
|
+
* Load the code once the browser is idle. Use it only for small chunks.
|
|
298
|
+
* Does nothing on the server.
|
|
299
|
+
* @returns A function that cancels the scheduled load.
|
|
300
|
+
*/
|
|
301
|
+
preloadWhenIdle(options?: DeferredIdleOptions): () => void;
|
|
302
|
+
/**
|
|
303
|
+
* Renders nothing until `open` is first true and the code is in. When
|
|
304
|
+
* `open` turns true it loads the code and renders as soon as it arrives. It
|
|
305
|
+
* never suspends. After the first open the component stays mounted and
|
|
306
|
+
* every prop passes through, `open` included, so it can run its own exit.
|
|
307
|
+
*/
|
|
308
|
+
Component: (props: DeferredProps<P>) => ReactElement | null;
|
|
309
|
+
};
|
|
310
|
+
/** Event handlers that start loading a deferred component when a shopper reaches for its trigger. */
|
|
311
|
+
type IntentPreloadHandlers = {
|
|
312
|
+
onPointerEnter: () => void;
|
|
313
|
+
onPointerDown: () => void;
|
|
314
|
+
onFocus: () => void;
|
|
315
|
+
};
|
|
316
|
+
/** What a deferred load resolves to: a component, or a module whose default export is one. */
|
|
317
|
+
type DeferredModule = ComponentType<never> | {
|
|
318
|
+
default: ComponentType<never>;
|
|
319
|
+
};
|
|
320
|
+
/** The props of the component a deferred load resolves to. */
|
|
321
|
+
type DeferredModuleProps<T> = T extends {
|
|
322
|
+
default: ComponentType<infer P>;
|
|
323
|
+
} ? P : T extends ComponentType<infer P> ? P : never;
|
|
324
|
+
/**
|
|
325
|
+
* Load a component's code on demand without `React.lazy` or `<Suspense>`.
|
|
326
|
+
* React holds a suspended boundary's first reveal for about 300 ms, so a
|
|
327
|
+
* lazy overlay opens late. A deferred one renders on the frame its code
|
|
328
|
+
* arrives. Pass a module whose default export is the component, or resolve
|
|
329
|
+
* to the component itself.
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* const Bag = deferred(() => import("./bag-sheet"));
|
|
333
|
+
* const Finder = deferred(() => import("./finder").then((m) => m.Finder));
|
|
334
|
+
* <Bag.Component open={open} onClose={close} />
|
|
335
|
+
*/
|
|
336
|
+
declare function deferred<T extends DeferredModule>(load: () => Promise<T>): Deferred<DeferredModuleProps<T>>;
|
|
337
|
+
/**
|
|
338
|
+
* Start loading a deferred component when the shopper points at, presses or
|
|
339
|
+
* focuses its trigger, so the code is usually in by the click. Spread the
|
|
340
|
+
* result on the trigger.
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* const intent = useIntentPreload(Bag);
|
|
344
|
+
* <button {...intent} onClick={openBag}>Bag</button>
|
|
345
|
+
*/
|
|
346
|
+
declare function useIntentPreload(target: {
|
|
347
|
+
preload(): Promise<unknown>;
|
|
348
|
+
}): IntentPreloadHandlers;
|
|
349
|
+
|
|
197
350
|
declare function useCombobox<T>(results: readonly T[], onSelect: (item: T) => void): {
|
|
198
351
|
setListId: react.Dispatch<react.SetStateAction<string>>;
|
|
199
352
|
registerOption: (index: number, id: string) => void;
|
|
@@ -416,11 +569,22 @@ type ItemFormContext = {
|
|
|
416
569
|
value: CartLineInput;
|
|
417
570
|
setValue: (value: CartLineInput) => void;
|
|
418
571
|
};
|
|
572
|
+
/**
|
|
573
|
+
* An option group that failed validation when the shopper tried to add, with its element and issues.
|
|
574
|
+
*/
|
|
575
|
+
type ItemFormInvalidGroup = {
|
|
576
|
+
groupId: string;
|
|
577
|
+
/** The item's option group, or undefined for a group the template rendered from its own model. */
|
|
578
|
+
group: ActiveGroup | undefined;
|
|
579
|
+
/** The group's rendered element, or undefined when the group is not on screen. */
|
|
580
|
+
element: HTMLElement | undefined;
|
|
581
|
+
issues: CartIssue[];
|
|
582
|
+
};
|
|
419
583
|
/**
|
|
420
584
|
* Item form configuration with controlled or default selections and cart commit callbacks.
|
|
421
585
|
* Supply onCommit to update an existing line instead of adding a new one.
|
|
422
586
|
*/
|
|
423
|
-
type ItemFormProps = PartProps<ItemFormContext> & {
|
|
587
|
+
type ItemFormProps = Omit<PartProps<ItemFormContext>, "onInvalid"> & {
|
|
424
588
|
item: Item;
|
|
425
589
|
value?: CartLineInput;
|
|
426
590
|
defaultValue?: Partial<CartLineInput>;
|
|
@@ -429,8 +593,15 @@ type ItemFormProps = PartProps<ItemFormContext> & {
|
|
|
429
593
|
onAdded?: (line: CartLine) => void;
|
|
430
594
|
/** Update an existing line through the same validation and focus workflow. */
|
|
431
595
|
onCommit?: (input: CartLineInput) => CartLine;
|
|
432
|
-
|
|
433
|
-
|
|
596
|
+
/**
|
|
597
|
+
* Called when an add fails validation, after the first invalid group gets
|
|
598
|
+
* focus without scrolling. Groups are in display order. Scroll to
|
|
599
|
+
* `groups[0].element` yourself, for example smoothly. The list is empty
|
|
600
|
+
* when no issue belongs to a group.
|
|
601
|
+
*/
|
|
602
|
+
onInvalid?: (groups: ItemFormInvalidGroup[]) => void;
|
|
603
|
+
};
|
|
604
|
+
declare function Root$3({ item, value, defaultValue, onValueChange, defaultQuantity, onAdded, onCommit, onInvalid, ...props }: ItemFormProps): react_jsx_runtime.JSX.Element;
|
|
434
605
|
|
|
435
606
|
type GroupState = {
|
|
436
607
|
group: OptionGroup$1;
|
|
@@ -825,4 +996,4 @@ declare const OpenStatus: react.ForwardRefExoticComponent<Omit<react.AllHTMLAttr
|
|
|
825
996
|
/** The installed @zappie/foundations version. */
|
|
826
997
|
declare const FOUNDATIONS_VERSION: string;
|
|
827
998
|
|
|
828
|
-
export { Account, AccountNotConfiguredError, type ActiveGroup, Address, type AddressState, type AddressSuggestion, Branch, BranchMap, type BranchMapProps, Cart, Choice, type Customer, FOUNDATIONS_VERSION, Fulfilment, Image, type ImageProps, ItemForm, type ItemFormContext, type ItemFormProps, Money, OpenStatus, OptionGroup, Otp, Overlay, PartProps, type PaymentMethod, PaymentMethods, Quantity, type QuantityProps, type QuantityState, Schedule, type ScheduleSlot, type ScheduleState, Search, type SearchResult, type SearchSource, type SearchState, type SignInOptions, Totals, type TotalsState, createSearchIndex, formatOpeningHours, formatWallClock, getOpenStatus, parseWallClock, scheduleSlots, useControllableState, useOverlay };
|
|
999
|
+
export { Account, AccountNotConfiguredError, type ActiveGroup, Address, type AddressState, type AddressSuggestion, Branch, BranchMap, type BranchMapLoadErrorState, type BranchMapProps, Cart, Choice, type Customer, type Deferred, type DeferredIdleOptions, type DeferredProps, DeliveryMap, type DeliveryMapProps, FOUNDATIONS_VERSION, Fulfilment, Image, type ImageProps, type IntentPreloadHandlers, ItemForm, type ItemFormContext, type ItemFormInvalidGroup, type ItemFormProps, type MapLoadErrorState, Money, OpenStatus, OptionGroup, Otp, Overlay, PartProps, type PaymentMethod, PaymentMethods, Quantity, type QuantityProps, type QuantityState, Schedule, type ScheduleSlot, type ScheduleState, Search, type SearchResult, type SearchSource, type SearchState, type SignInOptions, Totals, type TotalsState, createSearchIndex, deferred, formatOpeningHours, formatWallClock, getOpenStatus, parseWallClock, scheduleSlots, useControllableState, useIntentPreload, useOverlay };
|
package/dist/index.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { PACKAGE_VERSION } from "./version.js";
|
|
5
5
|
import { Account, AccountNotConfiguredError } from "./account/index.js";
|
|
6
|
-
import { BranchMap } from "./branch-map/index.js";
|
|
6
|
+
import { BranchMap, DeliveryMap } from "./branch-map/index.js";
|
|
7
7
|
import { Cart, Totals } from "./cart/index.js";
|
|
8
|
+
import { deferred, useIntentPreload } from "./deferred/index.js";
|
|
8
9
|
import { Address } from "./fulfilment/address-picker.js";
|
|
9
10
|
import { Branch } from "./fulfilment/branch-picker.js";
|
|
10
11
|
import { Schedule, scheduleSlots } from "./fulfilment/schedule.js";
|
|
@@ -32,6 +33,7 @@ export {
|
|
|
32
33
|
Branch,
|
|
33
34
|
BranchMap,
|
|
34
35
|
Cart,
|
|
36
|
+
DeliveryMap,
|
|
35
37
|
FOUNDATIONS_VERSION,
|
|
36
38
|
Fulfilment,
|
|
37
39
|
Image,
|
|
@@ -44,12 +46,14 @@ export {
|
|
|
44
46
|
Search,
|
|
45
47
|
Totals,
|
|
46
48
|
createSearchIndex,
|
|
49
|
+
deferred,
|
|
47
50
|
formatOpeningHours,
|
|
48
51
|
formatWallClock,
|
|
49
52
|
getOpenStatus,
|
|
50
53
|
parseWallClock,
|
|
51
54
|
scheduleSlots,
|
|
52
55
|
useControllableState,
|
|
56
|
+
useIntentPreload,
|
|
53
57
|
useOverlay
|
|
54
58
|
};
|
|
55
59
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\";\nimport { PACKAGE_VERSION } from \"./version\";\n\n/** The installed @zappie/foundations version. */\nexport const FOUNDATIONS_VERSION: string = PACKAGE_VERSION;\nexport { Account, AccountNotConfiguredError } from \"./account\";\nexport type { Customer, SignInOptions } from \"./account\";\nexport { BranchMap } from \"./branch-map\";\nexport type {
|
|
5
|
-
"mappings": ";;;AACA,SAAS,uBAAuB;AAIhC,SAAS,SAAS,iCAAiC;AAEnD,SAAS,
|
|
4
|
+
"sourcesContent": ["\"use client\";\nimport { PACKAGE_VERSION } from \"./version\";\n\n/** The installed @zappie/foundations version. */\nexport const FOUNDATIONS_VERSION: string = PACKAGE_VERSION;\nexport { Account, AccountNotConfiguredError } from \"./account\";\nexport type { Customer, SignInOptions } from \"./account\";\nexport { BranchMap, DeliveryMap } from \"./branch-map\";\nexport type {\n BranchMapLoadErrorState,\n BranchMapProps,\n DeliveryMapProps,\n MapLoadErrorState,\n} from \"./branch-map\";\nexport { Cart, Totals } from \"./cart\";\nexport type { TotalsState } from \"./cart\";\nexport { deferred, useIntentPreload } from \"./deferred\";\nexport type {\n Deferred,\n DeferredIdleOptions,\n DeferredProps,\n IntentPreloadHandlers,\n} from \"./deferred\";\nexport { Address } from \"./fulfilment/address-picker\";\nexport type {\n AddressState,\n AddressSuggestion,\n} from \"./fulfilment/address-picker\";\nexport { Branch } from \"./fulfilment/branch-picker\";\nexport { Schedule, scheduleSlots } from \"./fulfilment/schedule\";\nexport type { ScheduleSlot, ScheduleState } from \"./fulfilment/schedule\";\nexport { Fulfilment } from \"./fulfilment/switch\";\nexport type { PartProps } from \"./internal/slot\";\nexport { useControllableState } from \"./internal/use-controllable-state\";\nexport * from \"./item-form\";\nexport { Image } from \"./media/image\";\nexport type { ImageProps } from \"./media/image\";\nexport { Money } from \"./money\";\nexport { Otp } from \"./otp/input\";\nexport { Overlay, useOverlay } from \"./overlay/react\";\nexport { PaymentMethods } from \"./payment/methods\";\nexport type { PaymentMethod } from \"./payment/methods\";\nexport { createSearchIndex, Search } from \"./search\";\nexport type { SearchResult, SearchSource, SearchState } from \"./search\";\nexport {\n formatOpeningHours,\n formatWallClock,\n getOpenStatus,\n parseWallClock,\n} from \"./status/hours\";\nexport { OpenStatus } from \"./status/open-status\";\n"],
|
|
5
|
+
"mappings": ";;;AACA,SAAS,uBAAuB;AAIhC,SAAS,SAAS,iCAAiC;AAEnD,SAAS,WAAW,mBAAmB;AAOvC,SAAS,MAAM,cAAc;AAE7B,SAAS,UAAU,wBAAwB;AAO3C,SAAS,eAAe;AAKxB,SAAS,cAAc;AACvB,SAAS,UAAU,qBAAqB;AAExC,SAAS,kBAAkB;AAE3B,SAAS,4BAA4B;AACrC,cAAc;AACd,SAAS,aAAa;AAEtB,SAAS,aAAa;AACtB,SAAS,WAAW;AACpB,SAAS,SAAS,kBAAkB;AACpC,SAAS,sBAAsB;AAE/B,SAAS,mBAAmB,cAAc;AAE1C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AA9CpB,IAAM,sBAA8B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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 {
|
|
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 {\n ActiveGroup,\n ItemFormContext,\n ItemFormInvalidGroup,\n ItemFormProps,\n} from \"./root\";\n"],
|
|
5
5
|
"mappings": ";AAAA,SAAS,KAAK,eAAe;AAC7B,SAAS,YAAY;AAMrB,SAAS,QAAQ,mBAAmB;AACpC,SAAS,gBAAgB;AAFlB,IAAM,WAAW,EAAE,MAAM,SAAS,IAAI;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/item-form/root.js
CHANGED
|
@@ -36,6 +36,7 @@ function Root({
|
|
|
36
36
|
defaultQuantity,
|
|
37
37
|
onAdded,
|
|
38
38
|
onCommit,
|
|
39
|
+
onInvalid,
|
|
39
40
|
...props
|
|
40
41
|
}) {
|
|
41
42
|
const client = useZappie();
|
|
@@ -78,14 +79,30 @@ function Root({
|
|
|
78
79
|
(quantity) => setValue({ ...normalized, quantity }),
|
|
79
80
|
[setValue, normalized]
|
|
80
81
|
);
|
|
81
|
-
const focusIssue = useCallback(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
const focusIssue = useCallback(
|
|
83
|
+
(errors) => {
|
|
84
|
+
const issuesOf = (groupId) => errors.filter((issue) => issue.groupId === groupId);
|
|
85
|
+
const group = (groupId) => preview.groups.find((candidate) => candidate.id === groupId);
|
|
86
|
+
const rendered = [...registry.current].filter(([groupId]) => issuesOf(groupId).length > 0).map(([groupId, element]) => ({
|
|
87
|
+
groupId,
|
|
88
|
+
group: group(groupId),
|
|
89
|
+
element,
|
|
90
|
+
issues: issuesOf(groupId)
|
|
91
|
+
}));
|
|
92
|
+
const offscreen = preview.groups.filter(
|
|
93
|
+
(candidate) => !registry.current.has(candidate.id) && issuesOf(candidate.id).length > 0
|
|
94
|
+
).map((candidate) => ({
|
|
95
|
+
groupId: candidate.id,
|
|
96
|
+
group: candidate,
|
|
97
|
+
element: void 0,
|
|
98
|
+
issues: issuesOf(candidate.id)
|
|
99
|
+
}));
|
|
100
|
+
const invalid = [...rendered, ...offscreen];
|
|
101
|
+
invalid[0]?.element?.focus({ preventScroll: true });
|
|
102
|
+
onInvalid?.(invalid);
|
|
103
|
+
},
|
|
104
|
+
[preview.groups, onInvalid]
|
|
105
|
+
);
|
|
89
106
|
const add = useCallback(() => {
|
|
90
107
|
const result = client.cart.validate(normalized);
|
|
91
108
|
if (!result.ok) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/item-form/root.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\";\nimport {\n previewItem,\n ZappieCartError,\n type CartIssue,\n type CartLine,\n type CartLineInput,\n type Item,\n type Money,\n type OptionGroup,\n} from \"@zappie/sdk\";\nimport { useCartCatalogue, useZappie } from \"@zappie/sdk/react\";\nimport {\n createContext,\n useCallback,\n useContext,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { part, type PartProps } from \"../internal/slot\";\nimport { useControllableState } from \"../internal/use-controllable-state\";\n/**\n * An SDK option group with the effective rules exposed by the item preview.\n */\nexport type ActiveGroup = OptionGroup;\n/**\n * The current item selection, preview prices, validation issues and actions supplied to item form parts.\n */\nexport type ItemFormContext = {\n item: Item;\n groups: ActiveGroup[];\n quantity: number;\n setQuantity: (quantity: number) => void;\n unitPrice: Money;\n total: Money;\n issues: CartIssue[];\n canAdd: boolean;\n add: () => CartLine | null;\n status: \"incomplete\" | \"valid\" | \"added\";\n value: CartLineInput;\n setValue: (value: CartLineInput) => void;\n};\nexport const ItemFormStateContext = createContext<ItemFormContext | null>(null);\nexport function useItemForm() {\n const value = useContext(ItemFormStateContext);\n if (!value) {\n throw new Error(\"ItemForm parts require ItemForm.Root\");\n }\n return value;\n}\nexport const GroupRegistry = createContext<Map<string, HTMLElement> | null>(\n null,\n);\n/**\n * Item form configuration with controlled or default selections and cart commit callbacks.\n * Supply onCommit to update an existing line instead of adding a new one.\n */\nexport type ItemFormProps = PartProps<ItemFormContext> & {\n item: Item;\n value?: CartLineInput;\n defaultValue?: Partial<CartLineInput>;\n onValueChange?: (value: CartLineInput) => void;\n defaultQuantity?: number;\n onAdded?: (line: CartLine) => void;\n /** Update an existing line through the same validation and focus workflow. */\n onCommit?: (input: CartLineInput) => CartLine;\n};\nexport function Root({\n item,\n value,\n defaultValue,\n onValueChange,\n defaultQuantity,\n onAdded,\n onCommit,\n ...props\n}: ItemFormProps) {\n const client = useZappie();\n const menu = useCartCatalogue();\n const [input, setInput] = useControllableState({\n value,\n defaultValue: {\n ...defaultValue,\n itemId: item.id,\n quantity: defaultQuantity ?? defaultValue?.quantity ?? item.quantity.min,\n },\n onChange: onValueChange,\n });\n const registry = useRef(new Map<string, HTMLElement>());\n const [added, setAdded] = useState(false);\n // Errors show only after the shopper tries to add (PRD 05 5.3), then track live validation.\n const [attempted, setAttempted] = useState(false);\n const [failure, setFailure] = useState<CartIssue[] | null>(null);\n const normalized = useMemo(\n () => ({ ...input, itemId: item.id }),\n [input, item.id],\n );\n const preview = useMemo(\n () => previewItem(item, normalized),\n [item, normalized],\n );\n const validation = useMemo(\n () => client.cart.validate(normalized),\n [client, normalized, menu],\n );\n const issues =\n failure ?? (attempted && !validation.ok ? validation.issues : []);\n const setValue = useCallback(\n (next: CartLineInput) => {\n setAdded(false);\n setFailure(null);\n setInput({ ...next, itemId: item.id });\n },\n [setInput, item.id],\n );\n const setQuantity = useCallback(\n (quantity: number) => setValue({ ...normalized, quantity }),\n [setValue, normalized],\n );\n const focusIssue = useCallback((errors: CartIssue[]) => {\n
|
|
5
|
-
"mappings": ";;;AACA;AAAA,EACE;AAAA,EACA;AAAA,OAOK;AACP,SAAS,kBAAkB,iBAAiB;AAC5C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,YAA4B;AACrC,SAAS,4BAA4B;
|
|
4
|
+
"sourcesContent": ["\"use client\";\nimport {\n previewItem,\n ZappieCartError,\n type CartIssue,\n type CartLine,\n type CartLineInput,\n type Item,\n type Money,\n type OptionGroup,\n} from \"@zappie/sdk\";\nimport { useCartCatalogue, useZappie } from \"@zappie/sdk/react\";\nimport {\n createContext,\n useCallback,\n useContext,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { part, type PartProps } from \"../internal/slot\";\nimport { useControllableState } from \"../internal/use-controllable-state\";\n/**\n * An SDK option group with the effective rules exposed by the item preview.\n */\nexport type ActiveGroup = OptionGroup;\n/**\n * The current item selection, preview prices, validation issues and actions supplied to item form parts.\n */\nexport type ItemFormContext = {\n item: Item;\n groups: ActiveGroup[];\n quantity: number;\n setQuantity: (quantity: number) => void;\n unitPrice: Money;\n total: Money;\n issues: CartIssue[];\n canAdd: boolean;\n add: () => CartLine | null;\n status: \"incomplete\" | \"valid\" | \"added\";\n value: CartLineInput;\n setValue: (value: CartLineInput) => void;\n};\nexport const ItemFormStateContext = createContext<ItemFormContext | null>(null);\nexport function useItemForm() {\n const value = useContext(ItemFormStateContext);\n if (!value) {\n throw new Error(\"ItemForm parts require ItemForm.Root\");\n }\n return value;\n}\n/**\n * An option group that failed validation when the shopper tried to add, with its element and issues.\n */\nexport type ItemFormInvalidGroup = {\n groupId: string;\n /** The item's option group, or undefined for a group the template rendered from its own model. */\n group: ActiveGroup | undefined;\n /** The group's rendered element, or undefined when the group is not on screen. */\n element: HTMLElement | undefined;\n issues: CartIssue[];\n};\nexport const GroupRegistry = createContext<Map<string, HTMLElement> | null>(\n null,\n);\n/**\n * Item form configuration with controlled or default selections and cart commit callbacks.\n * Supply onCommit to update an existing line instead of adding a new one.\n */\nexport type ItemFormProps = Omit<PartProps<ItemFormContext>, \"onInvalid\"> & {\n item: Item;\n value?: CartLineInput;\n defaultValue?: Partial<CartLineInput>;\n onValueChange?: (value: CartLineInput) => void;\n defaultQuantity?: number;\n onAdded?: (line: CartLine) => void;\n /** Update an existing line through the same validation and focus workflow. */\n onCommit?: (input: CartLineInput) => CartLine;\n /**\n * Called when an add fails validation, after the first invalid group gets\n * focus without scrolling. Groups are in display order. Scroll to\n * `groups[0].element` yourself, for example smoothly. The list is empty\n * when no issue belongs to a group.\n */\n onInvalid?: (groups: ItemFormInvalidGroup[]) => void;\n};\nexport function Root({\n item,\n value,\n defaultValue,\n onValueChange,\n defaultQuantity,\n onAdded,\n onCommit,\n onInvalid,\n ...props\n}: ItemFormProps) {\n const client = useZappie();\n const menu = useCartCatalogue();\n const [input, setInput] = useControllableState({\n value,\n defaultValue: {\n ...defaultValue,\n itemId: item.id,\n quantity: defaultQuantity ?? defaultValue?.quantity ?? item.quantity.min,\n },\n onChange: onValueChange,\n });\n const registry = useRef(new Map<string, HTMLElement>());\n const [added, setAdded] = useState(false);\n // Errors show only after the shopper tries to add (PRD 05 5.3), then track live validation.\n const [attempted, setAttempted] = useState(false);\n const [failure, setFailure] = useState<CartIssue[] | null>(null);\n const normalized = useMemo(\n () => ({ ...input, itemId: item.id }),\n [input, item.id],\n );\n const preview = useMemo(\n () => previewItem(item, normalized),\n [item, normalized],\n );\n const validation = useMemo(\n () => client.cart.validate(normalized),\n [client, normalized, menu],\n );\n const issues =\n failure ?? (attempted && !validation.ok ? validation.issues : []);\n const setValue = useCallback(\n (next: CartLineInput) => {\n setAdded(false);\n setFailure(null);\n setInput({ ...next, itemId: item.id });\n },\n [setInput, item.id],\n );\n const setQuantity = useCallback(\n (quantity: number) => setValue({ ...normalized, quantity }),\n [setValue, normalized],\n );\n const focusIssue = useCallback(\n (errors: CartIssue[]) => {\n const issuesOf = (groupId: string) =>\n errors.filter((issue) => issue.groupId === groupId);\n const group = (groupId: string) =>\n preview.groups.find((candidate) => candidate.id === groupId);\n // Rendered groups first, in the order they mounted, then any invalid group not on screen.\n const rendered = [...registry.current]\n .filter(([groupId]) => issuesOf(groupId).length > 0)\n .map(([groupId, element]) => ({\n groupId,\n group: group(groupId),\n element: element as HTMLElement | undefined,\n issues: issuesOf(groupId),\n }));\n const offscreen = preview.groups\n .filter(\n (candidate) =>\n !registry.current.has(candidate.id) &&\n issuesOf(candidate.id).length > 0,\n )\n .map((candidate) => ({\n groupId: candidate.id,\n group: candidate,\n element: undefined,\n issues: issuesOf(candidate.id),\n }));\n const invalid = [...rendered, ...offscreen];\n // Focus moves for keyboard and screen reader users; the template decides how to scroll.\n invalid[0]?.element?.focus({ preventScroll: true });\n onInvalid?.(invalid);\n },\n [preview.groups, onInvalid],\n );\n const add = useCallback(() => {\n const result = client.cart.validate(normalized);\n if (!result.ok) {\n setAttempted(true);\n setFailure(result.issues);\n focusIssue(result.issues);\n return null;\n }\n try {\n const line = onCommit\n ? onCommit(normalized)\n : client.cart.add(item, normalized);\n setAttempted(false);\n setAdded(true);\n onAdded?.(line);\n return line;\n } catch (error) {\n if (error instanceof ZappieCartError) {\n setAttempted(true);\n setFailure([...error.issues]);\n focusIssue([...error.issues]);\n return null;\n }\n throw error;\n }\n }, [client, normalized, item, onAdded, onCommit, focusIssue]);\n const context: ItemFormContext = useMemo(\n () => ({\n item,\n ...preview,\n quantity: normalized.quantity ?? item.quantity.min,\n setQuantity,\n issues,\n canAdd: validation.ok && !failure,\n add,\n status: added ? \"added\" : validation.ok ? \"valid\" : \"incomplete\",\n value: normalized,\n setValue,\n }),\n [\n item,\n preview,\n normalized,\n setQuantity,\n issues,\n validation.ok,\n failure,\n add,\n added,\n setValue,\n ],\n );\n return (\n <ItemFormStateContext.Provider value={context}>\n <GroupRegistry.Provider value={registry.current}>\n {part(\"div\", props, context, {\n \"data-zappie-item\": item.id,\n \"data-state\": issues.length ? \"invalid\" : \"valid\",\n \"data-sold-out\": item.isAvailable ? undefined : \"\",\n })}\n </GroupRegistry.Provider>\n </ItemFormStateContext.Provider>\n );\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|