@zappie/foundations 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zappie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,274 @@
1
- # Temporary Holding Version
1
+ # @zappie/foundations
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Unstyled React commerce primitives for Open Storefronts PRD 05. They use the public SDK; they do not import Next, host UI, internal storefront packages, CSS or icons. React 18.2/19 is a peer. The package remains private until the planned public release.
4
+
5
+ ## Composition and state
6
+
7
+ Wrap commerce primitives in `ZappieProvider` from `@zappie/sdk/react`. Load/hydrate the store and branch menu using the SDK. Overlays, Quantity, PaymentMethods, Address and OTP can also run without a provider.
8
+
9
+ Every part accepts DOM attributes, a render-prop child, or `asChild` with exactly one element (including a render prop returning that element). Input parts accept a render-prop child returning one React element; their input props/ref are composed into that element. DOM handlers compose: the consumer runs first and can `preventDefault()` to cancel the default action. `asChild` preserves the child's handlers/ref. Components and custom children must forward their ref and supplied accessibility props. Use native buttons for button parts; replacing a button with a div makes the consumer responsible for native keyboard activation.
10
+
11
+ The default elements have no classes, styles, icons or visible text. `Money` supplies its formatted value. Input values, accessible names from catalogue data and OTP digit labels are semantic defaults. Labels/buttons/help/error copy are supplied by the template. Render-prop state is never spread onto the DOM automatically.
12
+
13
+ Controlled inputs use `value`/`onValueChange`; uncontrolled inputs use `defaultValue`. Search and Address use `query`/`onQueryChange`/`defaultQuery`. Overlay state is always the URL, so there is no competing `open` boolean.
14
+
15
+ Boolean data attributes are present as empty strings only when true. Parts expose `data-state` and relevant `data-disabled`, `data-invalid`, `data-selected`, `data-sold-out`. Custom classes/styles pass through unchanged. The only global styling performed internally is the overlay engine's page scroll lock while a modal is open, removed when the last modal closes.
16
+
17
+ ## Money
18
+
19
+ Anatomy: `<Money value={money} />` renders `<data value="450000">…</data>`. Props: branded SDK `Money`, optional `formatOptions`, DOM props. Render props expose `{ formatted }`. No state attributes or keyboard handling.
20
+
21
+ ```tsx
22
+ <Money value={line.lineSubtotal} formatOptions={{ locale: "en-NG" }} />
23
+ ```
24
+
25
+ ## Quantity
26
+
27
+ Anatomy: `Quantity.Root` → `Decrement`, `Value`, `Increment`. Root accepts value/defaultValue/onValueChange, min (1), max (99), step (1), disabled. Render props expose value/setValue/min/max/step/disabled. `Value` is a text input with spinbutton semantics; the buttons never add their own text. Disabled parts expose `data-disabled`.
28
+
29
+ Arrow Up/Down moves one step; Home/End reaches the first/last allowed value. Typed numbers snap to the configured step and bounds. Provide an accessible name for the input and both buttons. For SDK item quantities, use a minimum divisible by the item's step; `Cart.Quantity` applies that rule automatically.
30
+
31
+ ```tsx
32
+ <Quantity.Root min={1} max={10} onValueChange={setQuantity}>
33
+ <Quantity.Decrement aria-label="Decrease quantity" />
34
+ <Quantity.Value aria-label="Quantity" />
35
+ <Quantity.Increment aria-label="Increase quantity" />
36
+ </Quantity.Root>
37
+ ```
38
+
39
+ ## ItemForm, OptionGroup and Choice
40
+
41
+ Anatomy: `ItemForm.Root` → `ItemForm.Options` → `OptionGroup` → `Choice` / `OptionGroup.Input`; `ItemForm.Add` submits the configuration. Root props: item, optional full CartLineInput value/onValueChange, partial defaultValue, defaultQuantity, onAdded. Key the root by item ID when switching items to start a fresh configuration.
42
+
43
+ Root/Options render props expose item, effective groups, quantity/setQuantity, value/setValue, unitPrice/total, issues, canAdd, add and status (`incomplete|valid|added`). `add()` returns a CartLine or null, invokes SDK cart validation, and focuses the first invalid group. Add stays actionable for incomplete configurations so it can explain errors. Runtime failures other than cart validation errors propagate to the caller.
44
+
45
+ `@zappie/sdk` supplies the item preview: it reuses cart conditional groups, exact variant resolution and estimate math. It remains covered by the SDK core size budget. Preview values are estimates, not validated or authoritative quote totals. The SDK remains the sole owner of commerce validation.
46
+
47
+ Groups expose `data-state="valid|invalid"` and `data-invalid`, with radiogroup semantics for one choice and grouped checkboxes otherwise. Choice exposes checked/unchecked, selected, disabled and sold-out. Radio arrows/Home/End select available choices with roving tabindex; Space activates native buttons. Optional single-choice groups can be cleared by activating the selected choice. Input handles text/number/date/time/toggle and informational free responses, enforces maxLength (default 500), and clears mutually exclusive selected options. Choice clears a group's free response. Choice's render state also exposes `quantity` (0 when unselected), `min`, `max` and `setQuantity(n)`, clamped to the choice's bounds (0 deselects), so templates can build per-choice steppers. Groups show `data-invalid` only after a failed add; from then on they follow live validation, so fixing a group clears it immediately.
48
+
49
+ ```tsx
50
+ <ItemForm.Root item={item} onAdded={onAdded}>
51
+ {({ groups, total }) => (
52
+ <>
53
+ {groups.map((group) => (
54
+ <OptionGroup key={group.id} group={group}>
55
+ {group.kind === "select" ? (
56
+ group.choices.map((choice) => (
57
+ <Choice key={choice.id} choice={choice}>
58
+ {choice.name}
59
+ </Choice>
60
+ ))
61
+ ) : (
62
+ <OptionGroup.Input />
63
+ )}
64
+ </OptionGroup>
65
+ ))}
66
+ <Money value={total} />
67
+ <ItemForm.Add>Add to cart</ItemForm.Add>
68
+ </>
69
+ )}
70
+ </ItemForm.Root>
71
+ ```
72
+
73
+ ## Cart and Totals
74
+
75
+ Anatomy: `Cart.Root` → `Line`, `Quantity`, `Remove`, `Empty`, `Issues`. Root subscribes to the SDK snapshot and hydrates after mounting; SSR starts empty. Root render props include lines, count, subtotal, state, issues, fulfilment and actions. Root state is loading/invalid/valid.
76
+
77
+ Line takes a CartLine and exposes the current line, current catalogue item, edit(patch), remove(). Cart.Quantity takes lineId and supplies a Quantity.Root bounded by that item's rules. Remove takes lineId. Empty renders only after hydration when there are no lines. Issues exposes issues/applyFix and has `aria-live="polite"`; interactive fixes (choose option/address/schedule) must open the appropriate editor rather than call applyFix without a selection.
78
+
79
+ Totals independently subscribes to the cart and exposes quote plus loading/stale/ready/offline status. It observes quote expiry and quote_failed network events, uses a polite live region and aria-busy while loading. It does not request a quote just by rendering. SDK mutation debouncing and explicit `cart.quote()` own requests. No quote means stale, not a fabricated zero total. Quote line prices may be null when the upstream total cannot be allocated to lines.
80
+
81
+ ```tsx
82
+ <Cart.Root>{({ lines }) => <>
83
+ <Cart.Empty>Your cart is empty</Cart.Empty>
84
+ {lines.map(line => <Cart.Line key={line.id} line={line}>
85
+ <Cart.Quantity lineId={line.id}><Quantity.Value aria-label="Quantity" /></Cart.Quantity>
86
+ <Cart.Remove lineId={line.id}>Remove {line.name}</Cart.Remove>
87
+ </Cart.Line>)}
88
+ <Cart.Issues>{({ issues }) => issues.map((issue, i) => <p key={i}>{issue.message}</p>)}</Cart.Issues>
89
+ </>}</Cart.Root>
90
+ <Totals>{({ quote, status }) => quote ? <Money value={quote.total} /> : <span>{status}</span>}</Totals>
91
+ ```
92
+
93
+ ## Fulfilment.Switch
94
+
95
+ A group with options (only modes offered by the current branch), value and setValue. Props: optional value/defaultValue/onValueChange. Selection writes the SDK cart and checks branch/mode; delivery requires a complete address object. Keyboard behavior belongs to the native buttons/radios supplied by the template. No default visible text or state attribute.
96
+
97
+ ```tsx
98
+ <Fulfilment.Switch aria-label="Fulfilment">
99
+ {({ options, setValue }) =>
100
+ options.includes("pickup") && (
101
+ <button onClick={() => setValue({ type: "pickup", branchId })}>
102
+ Pickup
103
+ </button>
104
+ )
105
+ }
106
+ </Fulfilment.Switch>
107
+ ```
108
+
109
+ ## Branch.Picker
110
+
111
+ Props: branches, optional controlled branch-ID value, onSelect. Render props expose branches, selected, isOpenNow(branch), async select(branch), isLoading and error. select calls SDK cart.moveTo before onSelect; failure is exposed without reporting a successful selection. It renders a group with aria-busy and loading/invalid/valid state. Supply buttons or a labelled native select for keyboard operation.
112
+
113
+ ```tsx
114
+ <Branch.Picker branches={branches} aria-label="Branch">
115
+ {({ branches, select }) =>
116
+ branches.map((branch) => (
117
+ <button key={branch.id} onClick={() => void select(branch)}>
118
+ {branch.name}
119
+ </button>
120
+ ))
121
+ }
122
+ </Branch.Picker>
123
+ ```
124
+
125
+ ## Address.Picker
126
+
127
+ Anatomy: Address.Picker → Input, Suggestions → Suggestion. Props: getSuggestions(query, optionalAbortSignal), onSelect, controlled/uncontrolled query, debounceMs (200). Suggestions are `{ id, label, address: DeliveryAddress }`; no geocoding provider or API endpoint is built in. Search and Address announce how many options are listed through a visually hidden polite status region (the only inline style foundations renders); pass `announcement={(count) => ...}` to localise it. The region is omitted when the root uses `asChild`.
128
+
129
+ Render props expose query/setQuery, suggestions, loading/ready/error status, error, activeIndex and combobox props/actions. Old responses are ignored and requests are aborted when query/provider changes or on unmount. Input keeps DOM focus; Up/Down changes active descendant, Enter selects, Escape collapses, blur outside closes. Suggestions exposes aria-busy, Suggestion exposes data-selected. Root data-state is loading/invalid/valid.
130
+
131
+ ```tsx
132
+ <Address.Picker getSuggestions={getSuggestions} onSelect={selectAddress}>
133
+ <Address.Input aria-label="Delivery address" />
134
+ <Address.Suggestions aria-label="Addresses">
135
+ {({ suggestions }) =>
136
+ suggestions.map((item, index) => (
137
+ <Address.Suggestion key={item.id} index={index}>
138
+ {item.label}
139
+ </Address.Suggestion>
140
+ ))
141
+ }
142
+ </Address.Suggestions>
143
+ </Address.Picker>
144
+ ```
145
+
146
+ ## Schedule
147
+
148
+ Props: branch, optional timezone (branch then store timezone), controlled/uncontrolled ISO value (`null` means ASAP), onValueChange, intervalMinutes (30), maxDays (14, at most 90), noticeMinutes (0). Render props: days, slots, value/setValue, isAsap, canAsap, isValid. Slots include ISO value, local date and a label with timezone offset (distinguishing repeated DST times).
149
+
150
+ Scheduling must be offered by the branch. Slots intersect branch hours and the loaded menu's top-level schedule, honour special hours/closedUntil and preorder notice/horizon. Missing DST wall times are skipped and repeated times retain distinct instants. A stale or unavailable selection is invalid. ASAP is disabled for closed branches and preorder menus. This is a presentation of known schedule facts; the server quote remains authoritative for item-specific restrictions. State is valid/invalid. Supply native buttons/selects for keyboard handling.
151
+
152
+ ```tsx
153
+ <Schedule branch={branch} onValueChange={setScheduledFor} aria-label="Schedule">
154
+ {({ slots, setValue }) => (
155
+ <select
156
+ aria-label="Time slot"
157
+ onChange={(event) => setValue(event.target.value)}
158
+ >
159
+ {slots.map((slot) => (
160
+ <option key={slot.value} value={slot.value}>
161
+ {slot.date} {slot.label}
162
+ </option>
163
+ ))}
164
+ </select>
165
+ )}
166
+ </Schedule>
167
+ ```
168
+
169
+ ## PaymentMethods
170
+
171
+ Anatomy: Root → Item. Root takes methods (SDK method kinds or `{ value, disabled, reason }`), value/defaultValue/onValueChange; Item takes value. Display/preference only: it never charges or starts payment. Root is a labelled radiogroup. Items expose checked/unchecked, selected and disabled; reason is aria-description and also available to render props. Arrows/Home/End skip disabled methods; Space selects native buttons.
172
+
173
+ ```tsx
174
+ <PaymentMethods.Root
175
+ methods={["card", "bank_transfer"]}
176
+ aria-label="Preferred payment"
177
+ >
178
+ <PaymentMethods.Item value="card">Card</PaymentMethods.Item>
179
+ <PaymentMethods.Item value="bank_transfer">Bank transfer</PaymentMethods.Item>
180
+ </PaymentMethods.Root>
181
+ ```
182
+
183
+ ## Overlay
184
+
185
+ Anatomy: Root → Trigger, Content → Title, Close. Root requires param/id and accepts informational presentation (dialog/drawer/sheet), fallback query and omitParams. Every rendered Root registers only its exact ID by matching the URL; unknown IDs render no content. `useOverlay(param, id)` exposes isOpen/open(query?)/close(). IDs and query values remain data, never executable content.
186
+
187
+ URL search is the sole state source via useSyncExternalStore. Open pushes a marked history entry; close steps back through its stack, or replaces a deep link without leaving the site. Back closes the inner overlay, Forward reopens it. Root/Trigger expose open/closed; Content exposes open and data-presentation. Content is a labelled `aria-modal` overlay. Its mechanics come from Base UI, driven by the URL: it portals to `document.body` (or `container`), contains focus (an explicit `[data-autofocus]`, else the first focusable, else the overlay itself), restores focus to the trigger, hides the page behind from assistive technology, locks page scroll, and closes the top layer on Escape or an outside press (`onEscape` and `onInteractOutside` can `preventDefault()` to keep it open).
188
+
189
+ `presentation` chooses the behaviour: `"dialog"`, a swipeable `"drawer"` (swipe down to dismiss by default) or `"sheet"` (swipe right by default); `swipeDirection` overrides it. It is responsive: `presentation={{ base: "drawer", md: "dialog" }}` uses Tailwind's breakpoints (`sm` 640, `md` 768, `lg` 1024, `xl` 1280) through media queries, and the server renders `base`. Drawers keep focused fields above the on-screen keyboard. Root and Content expose `data-presentation`.
190
+
191
+ The engine is a separate chunk. Root prefetches it when the browser is idle and on trigger hover or focus, so it is normally ready before the first open. On a cold first open it loads before the overlay appears, and the package's initial entry stays within its size gate (`pnpm --filter @zappie/foundations size`).
192
+
193
+ Content must be positioned, as any modal is (for example `position: fixed`): the engine keeps an inert layer behind it to catch outside presses. Each modal also renders an unstyled backdrop marked `data-zappie-backdrop` (with `data-state` and `data-presentation`) for templates to style. Pass `forceMount` to keep content mounted while closed so exit animations can run on `data-state="closed"`. Give every overlay an `Overlay.Title` (visually hidden if need be) or an `aria-label`. Templates supply placement and a visible close button. Mount nested roots inside the outer content.
194
+
195
+ ```tsx
196
+ <Overlay.Root param="modal" id="cart" presentation="drawer">
197
+ <Overlay.Trigger>Open cart</Overlay.Trigger>
198
+ <Overlay.Content>
199
+ <Overlay.Title>Cart</Overlay.Title>
200
+ <Overlay.Close>Close cart</Overlay.Close>
201
+ </Overlay.Content>
202
+ </Overlay.Root>
203
+ ```
204
+
205
+ In Next, call `useNextOverlayBridge()` from `@zappie/foundations/next` once in the client shell. It captures same-page shallow history moves before Next's listener; no Next dependency enters the package. Plain React and React Router need no bridge. Native writes preserve history metadata and bypass framework wrappers; page-changing entries clear stale Next markers.
206
+
207
+ ## OpenStatus
208
+
209
+ Props: branch, optional timezone override. Exposes isOpen, label and nextChange; renders no visible text unless the template chooses it. `data-state="open|closed"`. No keyboard behavior. Uses Intl in the branch/store timezone, merged weekly sessions, overnight hours and calendar exceptions. A timer updates at the next opening/closing boundary, with focus refresh for suspended tabs. Pure helpers getOpenStatus/formatOpeningHours/parseWallClock/formatWallClock can be used outside React.
210
+
211
+ ```tsx
212
+ <OpenStatus branch={branch}>{({ label }) => label}</OpenStatus>
213
+ ```
214
+
215
+ ## Search
216
+
217
+ Anatomy: Root → Input, Results → Result. Root takes query/defaultQuery/onQueryChange and onSelect(result). Results include item/category discriminated unions and scores. Render props expose query/setQuery, results, activeIndex and combobox props/actions. Normalize case/diacritics; token prefixes plus Damerau–Levenshtein distance ≤1 for query tokens of at least four characters. Name weighs above tags above description. Index rebuilds only when the branch menu changes. The SDK emits `search { query, resultCount }` after 600ms of stable nonempty input; no analytics network transport is added.
218
+
219
+ Keyboard: Up/Down, Enter, Escape with DOM focus retained on the input; blur outside closes. Root data-state is loading/stale/valid; results expose data-selected.
220
+
221
+ ```tsx
222
+ <Search.Root onSelect={openSearchResult}>
223
+ <Search.Input aria-label="Search menu" />
224
+ <Search.Results aria-label="Results">
225
+ {({ results }) =>
226
+ results.map((result, index) => (
227
+ <Search.Result key={index} index={index}>
228
+ {result.type === "item" ? result.item.name : result.category.name}
229
+ </Search.Result>
230
+ ))
231
+ }
232
+ </Search.Results>
233
+ </Search.Root>
234
+ ```
235
+
236
+ ## Account stubs
237
+
238
+ Account.SignIn, SignOut and Customer are deliberate PRD 13 placeholders. They expose status `not_configured`; signIn(options)/signOut(options) reject with AccountNotConfiguredError (`code="not_configured"`). Customer is null. They never read host credentials or perform navigation/network auth. Sign-in options and the future customer shape match PRD 13. DOM state is invalid; supply your own markup, usually omit auth UI until configured.
239
+
240
+ ```tsx
241
+ <Account.Customer>
242
+ {({ customer }) => customer?.givenName ?? "Guest"}
243
+ </Account.Customer>
244
+ ```
245
+
246
+ ## Checkout/accounts-only OTP
247
+
248
+ Otp.Input is exported for the checkout and accounts applications, not an alternative storefront login flow. Props: length (6, range 1–12), value/defaultValue/onValueChange/onComplete/disabled. Default markup is a group of labelled text inputs with numeric input mode, one-time-code autocomplete on the first input, paste/autofill, left/right movement and backspace across slots. No value is logged. A render prop can supply all slots through getSlotProps(index), preserving their data-otp-slot attribute. State attribute: data-disabled.
249
+
250
+ ```tsx
251
+ <Otp.Input length={6} aria-label="Verification code" onComplete={verifyCode} />
252
+ ```
253
+
254
+ ## Build or adopt
255
+
256
+ Base UI already ships as the lazily loaded overlay engine, but the other primitives are built in-house on purpose: they render with the page, and the Base UI equivalents of the generic patterns alone are about 75 KB gzipped against 12 KB for all of foundations. Adopt the Base UI component for a single primitive only when the in-house one keeps producing accessibility or device bugs that aren't cheaply fixable, or when the primitive only ever renders inside an overlay (it then loads with the engine chunk). Commerce behaviour and the `data-*` contract stay in foundations either way. The rule, the measurements and the current status of each primitive are in open-storefronts PRD 05 §4.7.
257
+
258
+ ## Examples and verification
259
+
260
+ [examples/commerce.tsx](examples/commerce.tsx) and [examples/overlay.tsx](examples/overlay.tsx) are executable, synthetic examples. The Next host exposes `/foundations-lab` only outside production; the standalone Vite fixture runs on loopback.
261
+
262
+ ```sh
263
+ pnpm exec vite packages/foundations/examples --config packages/foundations/examples/vite.config.ts
264
+ pnpm exec vitest run packages/foundations
265
+ pnpm exec vitest run --config vitest.foundations-browser.config.ts
266
+ pnpm exec playwright test --config playwright.foundations.config.ts
267
+ pnpm packages:build
268
+ pnpm sdk:size
269
+ pnpm check-types
270
+ ```
271
+
272
+ Component tests use RTL and axe in jsdom (color contrast is disabled there because jsdom has no layout/canvas). The browser example test runs axe in Chromium. The separate Playwright matrix exercises Vite and the real Next host on Chromium, WebKit and Mobile Safari. Tests use synthetic data and loopback servers, not production APIs or real payments.
273
+
274
+ Keyboard behavior follows the WAI-ARIA APG [dialog](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/), [combobox](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/), [radio](https://www.w3.org/WAI/ARIA/apg/patterns/radio/) and spinbutton patterns. Consumers must retain semantics and provide meaningful visible/accessibility labels.
@@ -0,0 +1,97 @@
1
+ // src/overlay/history.ts
2
+ var LOCATION_EVENT = "zappie:location";
3
+ function getCurrentOverlayEntry() {
4
+ if (typeof window === "undefined") {
5
+ return null;
6
+ }
7
+ const entry = window.history.state?.__zappieOverlay;
8
+ if (!entry || typeof entry.param !== "string" || typeof entry.id !== "string") {
9
+ return null;
10
+ }
11
+ return {
12
+ param: entry.param,
13
+ id: entry.id,
14
+ stack: Array.isArray(entry.stack) ? entry.stack.filter((key) => typeof key === "string") : []
15
+ };
16
+ }
17
+ function subscribeLocation(listener) {
18
+ window.addEventListener("popstate", listener);
19
+ window.addEventListener(LOCATION_EVENT, listener);
20
+ return () => {
21
+ window.removeEventListener("popstate", listener);
22
+ window.removeEventListener(LOCATION_EVENT, listener);
23
+ };
24
+ }
25
+ function locationSnapshot() {
26
+ return window.location.search;
27
+ }
28
+ function applyQuery(url, query) {
29
+ for (const [key, value] of Object.entries(query)) {
30
+ if (value == null) {
31
+ url.searchParams.delete(key);
32
+ } else {
33
+ url.searchParams.set(key, value);
34
+ }
35
+ }
36
+ }
37
+ function write(type, url, overlay) {
38
+ const state = { ...window.history.state };
39
+ delete state.__zappieOverlay;
40
+ if (overlay) {
41
+ state.__zappieOverlay = overlay;
42
+ }
43
+ state.__zappieBase ?? (state.__zappieBase = window.location.pathname);
44
+ if (url.pathname !== window.location.pathname) {
45
+ delete state.__NA;
46
+ delete state.__PRIVATE_NEXTJS_INTERNALS_TREE;
47
+ }
48
+ const prototype = Object.getPrototypeOf(window.history);
49
+ prototype[type].call(window.history, state, "", url.href);
50
+ window.dispatchEvent(new Event(LOCATION_EVENT));
51
+ }
52
+ function openOverlay(param, id, query = {}) {
53
+ const url = new URL(window.location.href);
54
+ applyQuery(url, query);
55
+ url.searchParams.set(param, id);
56
+ if (url.href === window.location.href) {
57
+ return;
58
+ }
59
+ const previous = getCurrentOverlayEntry();
60
+ write("pushState", url, {
61
+ param,
62
+ id,
63
+ stack: [...previous?.stack ?? [], `${param}:${id}`]
64
+ });
65
+ }
66
+ function closeOverlay(param, id, options = {}) {
67
+ const url = new URL(window.location.href);
68
+ if (!url.searchParams.has(param)) {
69
+ return;
70
+ }
71
+ const entry = getCurrentOverlayEntry();
72
+ const stack = entry?.stack ?? [];
73
+ const index = id ? stack.lastIndexOf(`${param}:${id}`) : stack.reduce(
74
+ (found, key, index2) => key.startsWith(`${param}:`) ? index2 : found,
75
+ -1
76
+ );
77
+ if (index >= 0) {
78
+ window.history.go(-(stack.length - index));
79
+ return;
80
+ }
81
+ applyQuery(url, options.fallback ?? {});
82
+ if (!options.fallback || !(param in options.fallback)) {
83
+ url.searchParams.delete(param);
84
+ }
85
+ for (const omitted of options.omitParams ?? []) {
86
+ url.searchParams.delete(omitted);
87
+ }
88
+ write(
89
+ "replaceState",
90
+ url,
91
+ entry && url.searchParams.get(entry.param) === entry.id ? entry : null
92
+ );
93
+ }
94
+
95
+ export { LOCATION_EVENT, closeOverlay, locationSnapshot, openOverlay, subscribeLocation };
96
+ //# sourceMappingURL=chunk-BY2WQGLO.js.map
97
+ //# sourceMappingURL=chunk-BY2WQGLO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/overlay/history.ts"],"names":["index"],"mappings":"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","file":"chunk-BY2WQGLO.js","sourcesContent":["export type OverlayEntry = { param: string; id: string; stack: string[] };\nexport type OverlayQuery = Record<string, string | null | undefined>;\nexport type CloseOptions = {\n fallback?: OverlayQuery | undefined;\n omitParams?: string[] | undefined;\n};\nexport const LOCATION_EVENT = \"zappie:location\";\nexport function getCurrentOverlayEntry(): OverlayEntry | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n const entry = window.history.state?.__zappieOverlay;\n if (\n !entry ||\n typeof entry.param !== \"string\" ||\n typeof entry.id !== \"string\"\n ) {\n return null;\n }\n return {\n param: entry.param,\n id: entry.id,\n stack: Array.isArray(entry.stack)\n ? entry.stack.filter((key: unknown) => typeof key === \"string\")\n : [],\n };\n}\nexport function subscribeLocation(listener: () => void) {\n window.addEventListener(\"popstate\", listener);\n window.addEventListener(LOCATION_EVENT, listener);\n return () => {\n window.removeEventListener(\"popstate\", listener);\n window.removeEventListener(LOCATION_EVENT, listener);\n };\n}\nexport function locationSnapshot() {\n return window.location.search;\n}\nfunction applyQuery(url: URL, query: OverlayQuery) {\n for (const [key, value] of Object.entries(query)) {\n if (value == null) {\n url.searchParams.delete(key);\n } else {\n url.searchParams.set(key, value);\n }\n }\n}\nfunction write(\n type: \"pushState\" | \"replaceState\",\n url: URL,\n overlay: OverlayEntry | null,\n) {\n const state = { ...window.history.state };\n delete state.__zappieOverlay;\n if (overlay) {\n state.__zappieOverlay = overlay;\n }\n state.__zappieBase ??= window.location.pathname;\n if (url.pathname !== window.location.pathname) {\n delete state.__NA;\n delete state.__PRIVATE_NEXTJS_INTERNALS_TREE;\n }\n // Bypass Next's wrapper: shallow overlay writes must not remount the page.\n const prototype = Object.getPrototypeOf(window.history) as History;\n prototype[type].call(window.history, state, \"\", url.href);\n window.dispatchEvent(new Event(LOCATION_EVENT));\n}\nexport function openOverlay(\n param: string,\n id: string,\n query: OverlayQuery = {},\n) {\n const url = new URL(window.location.href);\n applyQuery(url, query);\n url.searchParams.set(param, id);\n if (url.href === window.location.href) {\n return;\n }\n const previous = getCurrentOverlayEntry();\n write(\"pushState\", url, {\n param,\n id,\n stack: [...(previous?.stack ?? []), `${param}:${id}`],\n });\n}\nexport function closeOverlay(\n param: string,\n id?: string,\n options: CloseOptions = {},\n) {\n const url = new URL(window.location.href);\n if (!url.searchParams.has(param)) {\n return;\n }\n const entry = getCurrentOverlayEntry();\n const stack = entry?.stack ?? [];\n const index = id\n ? stack.lastIndexOf(`${param}:${id}`)\n : stack.reduce(\n (found, key, index) => (key.startsWith(`${param}:`) ? index : found),\n -1,\n );\n if (index >= 0) {\n window.history.go(-(stack.length - index));\n return;\n }\n applyQuery(url, options.fallback ?? {});\n if (!options.fallback || !(param in options.fallback)) {\n url.searchParams.delete(param);\n }\n for (const omitted of options.omitParams ?? []) {\n url.searchParams.delete(omitted);\n }\n write(\n \"replaceState\",\n url,\n entry && url.searchParams.get(entry.param) === entry.id ? entry : null,\n );\n}\n"]}
@@ -0,0 +1,81 @@
1
+ import { Children, isValidElement, cloneElement, createElement } from 'react';
2
+
3
+ // src/internal/slot.tsx
4
+ function composeRefs(...refs) {
5
+ let cleanups = [];
6
+ return (value) => {
7
+ for (const cleanup of cleanups) {
8
+ cleanup();
9
+ }
10
+ cleanups = [];
11
+ if (value === null) {
12
+ return;
13
+ }
14
+ for (const ref of refs) {
15
+ if (typeof ref === "function") {
16
+ const cleanup = ref(value);
17
+ cleanups.push(
18
+ typeof cleanup === "function" ? cleanup : () => ref(null)
19
+ );
20
+ } else if (ref) {
21
+ ref.current = value;
22
+ cleanups.push(() => {
23
+ ref.current = null;
24
+ });
25
+ }
26
+ }
27
+ };
28
+ }
29
+ function mergeProps(base, consumer) {
30
+ const result = { ...base, ...consumer };
31
+ if (typeof base.className === "string" && typeof consumer.className === "string") {
32
+ result.className = `${base.className} ${consumer.className}`;
33
+ }
34
+ if (base.style && consumer.style && typeof base.style === "object" && typeof consumer.style === "object") {
35
+ result.style = { ...base.style, ...consumer.style };
36
+ }
37
+ for (const key of Object.keys(base)) {
38
+ if (/^on[A-Z]/.test(key) && typeof base[key] === "function" && typeof consumer[key] === "function") {
39
+ result[key] = (event) => {
40
+ consumer[key](event);
41
+ if (!event.defaultPrevented) {
42
+ base[key](event);
43
+ }
44
+ };
45
+ }
46
+ }
47
+ return result;
48
+ }
49
+ function part(tag, props, state, defaults = {}, ref) {
50
+ const { asChild, children, ...dom } = props;
51
+ const content = typeof children === "function" ? children(state) : children;
52
+ const merged = mergeProps(defaults, dom);
53
+ if (asChild || tag === "input" && typeof children === "function") {
54
+ const child = Children.only(content);
55
+ if (!isValidElement(child)) {
56
+ throw new Error("asChild requires one React element");
57
+ }
58
+ const element = child;
59
+ const descriptor = Object.getOwnPropertyDescriptor(element, "ref");
60
+ const childRef = element.props.ref ?? (descriptor && "value" in descriptor ? descriptor.value : void 0);
61
+ return cloneElement(element, {
62
+ ...mergeProps(merged, element.props),
63
+ ref: composeRefs(ref, childRef)
64
+ });
65
+ }
66
+ return createElement(tag, { ...merged, ref }, content);
67
+ }
68
+ function partId(props) {
69
+ if (props.id) {
70
+ return props.id;
71
+ }
72
+ if (props.asChild && isValidElement(props.children)) {
73
+ const id = props.children.props.id;
74
+ return typeof id === "string" ? id : void 0;
75
+ }
76
+ return void 0;
77
+ }
78
+
79
+ export { composeRefs, part, partId };
80
+ //# sourceMappingURL=chunk-D3J7S33L.js.map
81
+ //# sourceMappingURL=chunk-D3J7S33L.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/internal/slot.tsx"],"names":[],"mappings":"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","file":"chunk-D3J7S33L.js","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"]}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A parser-neutral rich-text node containing its type, optional tag name, text and children.
3
+ */
4
+ type ParsedRichTextNode = {
5
+ type: string;
6
+ name?: string;
7
+ data?: string;
8
+ children?: ParsedRichTextNode[];
9
+ };
10
+ /**
11
+ * A parser adapter that converts HTML into the node tree consumed by the rich-text sanitizer.
12
+ */
13
+ type ParseRichText = (html: string) => ParsedRichTextNode[];
14
+ /**
15
+ * Create a sanitizer using your HTML parser adapter.
16
+ * Keeps supported formatting tags without attributes, escapes text and removes unsafe element content.
17
+ *
18
+ * @param parseRichText - A parser that returns the shared rich-text node format.
19
+ * @returns A sanitizer that accepts HTML and returns sanitized HTML, or an empty string for empty input.
20
+ */
21
+ declare const createSanitizeRichText: (parseRichText: ParseRichText) => (html?: string | null) => string;
22
+
23
+ export { type ParsedRichTextNode as P, type ParseRichText as a, createSanitizeRichText as c };
@@ -0,0 +1,60 @@
1
+ import * as react from 'react';
2
+ import { AllHTMLAttributes, ReactNode, Ref } from 'react';
3
+ import * as react_jsx_runtime from 'react/jsx-runtime';
4
+ import { Dialog } from '@base-ui/react/dialog';
5
+
6
+ /**
7
+ * HTML attributes shared by foundation parts, with optional render-function children.
8
+ * Set asChild to merge the part props and ref into a single child element.
9
+ */
10
+ type PartProps<S = Record<string, never>> = Omit<AllHTMLAttributes<HTMLElement>, "children" | "defaultValue" | "value"> & {
11
+ asChild?: boolean;
12
+ children?: ReactNode | ((state: S) => ReactNode);
13
+ };
14
+
15
+ type Presentation = "dialog" | "drawer" | "sheet";
16
+ type SwipeDirection = "up" | "down" | "left" | "right";
17
+ type SurfaceProps = {
18
+ open: boolean;
19
+ presentation: Presentation;
20
+ swipeDirection: SwipeDirection;
21
+ /** Ids the trigger points at; caller-supplied ids win. */
22
+ contentId: string;
23
+ titleId?: string | undefined;
24
+ trigger: {
25
+ current: HTMLElement | null;
26
+ };
27
+ onOpen: () => void;
28
+ onClose: () => void;
29
+ onEscape?: ((event: KeyboardEvent) => void) | undefined;
30
+ onInteractOutside?: ((event: Event) => void) | undefined;
31
+ forceMount?: true | undefined;
32
+ container?: HTMLElement | null | undefined;
33
+ state: unknown;
34
+ contentProps: PartProps<never>;
35
+ contentRef: Ref<HTMLElement>;
36
+ };
37
+ type Parts = {
38
+ Title: typeof Dialog.Title;
39
+ Close: typeof Dialog.Close;
40
+ };
41
+ declare function useEngineParts(): Parts;
42
+ declare function Surface(props: SurfaceProps & {
43
+ children?: ReactNode;
44
+ }): react_jsx_runtime.JSX.Element;
45
+ declare const EngineTitle: react.ForwardRefExoticComponent<Omit<react.AllHTMLAttributes<HTMLElement>, "children" | "defaultValue" | "value"> & {
46
+ asChild?: boolean;
47
+ children?: ReactNode | ((state: unknown) => ReactNode);
48
+ } & {
49
+ id?: string | undefined;
50
+ state: unknown;
51
+ } & react.RefAttributes<HTMLElement>>;
52
+ declare const EngineClose: react.ForwardRefExoticComponent<Omit<react.AllHTMLAttributes<HTMLElement>, "children" | "defaultValue" | "value"> & {
53
+ asChild?: boolean;
54
+ children?: ReactNode | ((state: unknown) => ReactNode);
55
+ } & {
56
+ id?: string | undefined;
57
+ state: unknown;
58
+ } & react.RefAttributes<HTMLElement>>;
59
+
60
+ export { EngineClose as E, type PartProps as P, type SwipeDirection as S, type Presentation as a, EngineTitle as b, Surface as c, type SurfaceProps as d, useEngineParts as u };
@@ -0,0 +1,8 @@
1
+ type OverlayQuery = Record<string, string | null | undefined>;
2
+ type CloseOptions = {
3
+ fallback?: OverlayQuery | undefined;
4
+ omitParams?: string[] | undefined;
5
+ };
6
+ declare const LOCATION_EVENT = "zappie:location";
7
+
8
+ export { type CloseOptions as C, LOCATION_EVENT as L, type OverlayQuery as O };