@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,261 @@
1
+ "use client";
2
+
3
+ // src/overlay/react.tsx
4
+ import {
5
+ createContext,
6
+ forwardRef,
7
+ useCallback,
8
+ useContext,
9
+ useEffect,
10
+ useId,
11
+ useMemo,
12
+ useRef,
13
+ useState,
14
+ useSyncExternalStore
15
+ } from "react";
16
+ import { composeRefs, part, partId } from "../internal/slot.js";
17
+ import {
18
+ closeOverlay,
19
+ locationSnapshot,
20
+ openOverlay,
21
+ subscribeLocation
22
+ } from "./history.js";
23
+ import { jsx } from "react/jsx-runtime";
24
+ var engineModule;
25
+ var enginePromise;
26
+ function loadOverlayEngine() {
27
+ enginePromise ?? (enginePromise = import("./engine.js").then(
28
+ (module) => engineModule = module,
29
+ (error) => {
30
+ enginePromise = void 0;
31
+ throw error;
32
+ }
33
+ ));
34
+ return enginePromise;
35
+ }
36
+ function useOverlay(param, id, options) {
37
+ const search = useSyncExternalStore(
38
+ subscribeLocation,
39
+ locationSnapshot,
40
+ () => ""
41
+ );
42
+ const optionsRef = useRef(options);
43
+ optionsRef.current = options;
44
+ const open = useCallback(
45
+ (query) => openOverlay(param, id, query),
46
+ [param, id]
47
+ );
48
+ const close = useCallback(
49
+ () => closeOverlay(param, id, optionsRef.current),
50
+ [param, id]
51
+ );
52
+ return useMemo(
53
+ () => ({
54
+ isOpen: new URLSearchParams(search).get(param) === id,
55
+ open,
56
+ close
57
+ }),
58
+ [search, param, id, open, close]
59
+ );
60
+ }
61
+ var BREAKPOINTS = { sm: 640, md: 768, lg: 1024, xl: 1280 };
62
+ function usePresentation(value) {
63
+ const key = typeof value === "string" ? value : JSON.stringify(value);
64
+ const resolve = useCallback(() => {
65
+ const parsed = key.startsWith("{") ? JSON.parse(key) : key;
66
+ if (typeof parsed === "string") {
67
+ return parsed;
68
+ }
69
+ let current = parsed.base;
70
+ for (const [name, width] of Object.entries(BREAKPOINTS)) {
71
+ const at = parsed[name];
72
+ if (at && window.matchMedia(`(min-width: ${width}px)`).matches) {
73
+ current = at;
74
+ }
75
+ }
76
+ return current;
77
+ }, [key]);
78
+ const subscribe = useCallback(
79
+ (onChange) => {
80
+ if (!key.startsWith("{")) {
81
+ return () => {
82
+ };
83
+ }
84
+ const queries = Object.values(BREAKPOINTS).map(
85
+ (width) => window.matchMedia(`(min-width: ${width}px)`)
86
+ );
87
+ queries.forEach((query) => query.addEventListener("change", onChange));
88
+ return () => queries.forEach(
89
+ (query) => query.removeEventListener("change", onChange)
90
+ );
91
+ },
92
+ [key]
93
+ );
94
+ return useSyncExternalStore(
95
+ subscribe,
96
+ resolve,
97
+ () => typeof value === "string" ? value : value.base
98
+ );
99
+ }
100
+ var Context = createContext(null);
101
+ var EngineContext = createContext(null);
102
+ function useContextValue() {
103
+ const value = useContext(Context);
104
+ if (!value) {
105
+ throw new Error("Overlay parts require Overlay.Root");
106
+ }
107
+ return value;
108
+ }
109
+ var prefetch = () => void loadOverlayEngine().catch(() => {
110
+ });
111
+ function Root({
112
+ param,
113
+ id,
114
+ presentation = "dialog",
115
+ swipeDirection,
116
+ fallback,
117
+ omitParams,
118
+ navigation,
119
+ ...props
120
+ }) {
121
+ const queryOverlay = useOverlay(param, id, { fallback, omitParams });
122
+ const overlay = navigation ?? queryOverlay;
123
+ const resolved = usePresentation(presentation);
124
+ const generated = useId();
125
+ const [contentId, setContentId] = useState(`zappie-overlay-${generated}`);
126
+ const trigger = useRef(null);
127
+ useEffect(() => {
128
+ if (engineModule) {
129
+ return void 0;
130
+ }
131
+ if (typeof window.requestIdleCallback === "function") {
132
+ const handle2 = window.requestIdleCallback(prefetch);
133
+ return () => window.cancelIdleCallback(handle2);
134
+ }
135
+ const handle = window.setTimeout(prefetch, 1500);
136
+ return () => window.clearTimeout(handle);
137
+ }, []);
138
+ const context = useMemo(
139
+ () => ({
140
+ ...overlay,
141
+ presentation: resolved,
142
+ swipeDirection: swipeDirection ?? (resolved === "sheet" ? "right" : "down"),
143
+ contentId,
144
+ setContentId,
145
+ trigger
146
+ }),
147
+ [overlay, resolved, swipeDirection, contentId]
148
+ );
149
+ return /* @__PURE__ */ jsx(Context.Provider, { value: context, children: part("div", props, overlay, {
150
+ "data-state": overlay.isOpen ? "open" : "closed",
151
+ "data-presentation": resolved
152
+ }) });
153
+ }
154
+ var Trigger = forwardRef(
155
+ function Trigger2(props, ref) {
156
+ const ctx = useContextValue();
157
+ return part(
158
+ "button",
159
+ props,
160
+ ctx,
161
+ {
162
+ type: "button",
163
+ "aria-haspopup": "dialog",
164
+ "aria-expanded": ctx.isOpen,
165
+ "aria-controls": ctx.contentId,
166
+ "data-state": ctx.isOpen ? "open" : "closed",
167
+ onPointerEnter: prefetch,
168
+ onFocus: prefetch,
169
+ onClick: () => ctx.open()
170
+ },
171
+ composeRefs(ref, (node) => {
172
+ ctx.trigger.current = node;
173
+ })
174
+ );
175
+ }
176
+ );
177
+ var Content = forwardRef(function Content2({ onEscape, onInteractOutside, forceMount, container, ...props }, ref) {
178
+ const ctx = useContextValue();
179
+ const [engine, setEngine] = useState(engineModule);
180
+ const [opened, setOpened] = useState(ctx.isOpen);
181
+ if (ctx.isOpen && !opened) {
182
+ setOpened(true);
183
+ }
184
+ const customId = partId(props);
185
+ const { setContentId } = ctx;
186
+ useEffect(() => {
187
+ if (customId) {
188
+ setContentId(customId);
189
+ }
190
+ }, [customId, setContentId]);
191
+ useEffect(() => {
192
+ if (engine || !(ctx.isOpen || forceMount)) {
193
+ return void 0;
194
+ }
195
+ let live = true;
196
+ void loadOverlayEngine().then(
197
+ (module) => {
198
+ if (live) {
199
+ setEngine(module);
200
+ }
201
+ },
202
+ () => {
203
+ }
204
+ );
205
+ return () => {
206
+ live = false;
207
+ };
208
+ }, [engine, ctx.isOpen, forceMount]);
209
+ if (!engine || !opened && !forceMount) {
210
+ return null;
211
+ }
212
+ return /* @__PURE__ */ jsx(EngineContext.Provider, { value: engine, children: /* @__PURE__ */ jsx(
213
+ engine.Surface,
214
+ {
215
+ open: ctx.isOpen,
216
+ presentation: ctx.presentation,
217
+ swipeDirection: ctx.swipeDirection,
218
+ contentId: ctx.contentId,
219
+ trigger: ctx.trigger,
220
+ onOpen: () => ctx.open(),
221
+ onClose: ctx.close,
222
+ onEscape,
223
+ onInteractOutside,
224
+ forceMount,
225
+ container,
226
+ state: ctx,
227
+ contentProps: props,
228
+ contentRef: ref
229
+ }
230
+ ) });
231
+ });
232
+ function useEngine() {
233
+ const engine = useContext(EngineContext);
234
+ if (!engine) {
235
+ throw new Error(
236
+ "Overlay.Title and Overlay.Close must be inside Overlay.Content"
237
+ );
238
+ }
239
+ return engine;
240
+ }
241
+ var Title = forwardRef(
242
+ function Title2(props, ref) {
243
+ const ctx = useContextValue();
244
+ const { EngineTitle } = useEngine();
245
+ return /* @__PURE__ */ jsx(EngineTitle, { ...props, state: ctx, ref });
246
+ }
247
+ );
248
+ var Close = forwardRef(
249
+ function Close2(props, ref) {
250
+ const ctx = useContextValue();
251
+ const { EngineClose } = useEngine();
252
+ return /* @__PURE__ */ jsx(EngineClose, { ...props, state: ctx, ref });
253
+ }
254
+ );
255
+ var Overlay = { Root, Trigger, Content, Title, Close };
256
+ export {
257
+ Overlay,
258
+ loadOverlayEngine,
259
+ useOverlay
260
+ };
261
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/overlay/react.tsx"],
4
+ "sourcesContent": ["\"use client\";\nimport {\n createContext,\n forwardRef,\n useCallback,\n useContext,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n} from \"react\";\nimport { composeRefs, part, partId, type PartProps } from \"../internal/slot\";\nimport type { Presentation, SwipeDirection } from \"./engine\";\nimport {\n closeOverlay,\n locationSnapshot,\n openOverlay,\n subscribeLocation,\n type CloseOptions,\n type OverlayQuery,\n} from \"./history\";\n\ntype EngineModule = typeof import(\"./engine\");\nlet engineModule: EngineModule | undefined;\nlet enginePromise: Promise<EngineModule> | undefined;\n/**\n * Load the overlay engine (Base UI dialog and drawer). It is a separate chunk: Root prefetches\n * it when the browser is idle and on trigger hover/focus, so it is usually ready by first open.\n */\nexport function loadOverlayEngine(): Promise<EngineModule> {\n enginePromise ??= import(\"./engine\").then(\n (module) => (engineModule = module),\n (error: unknown) => {\n // A failed chunk load (e.g. offline) must not poison later opens.\n enginePromise = undefined;\n throw error;\n },\n );\n return enginePromise;\n}\n\n/**\n * Control an overlay whose open state is stored in a URL query parameter.\n *\n * @param param - The query parameter to read and update.\n * @param id - The value that identifies this overlay as open.\n * @param options - Fallback query values and query parameters to omit when closing.\n * @returns Reactive open state and history-backed open and close actions.\n */\nexport function useOverlay(param: string, id: string, options?: CloseOptions) {\n const search = useSyncExternalStore(\n subscribeLocation,\n locationSnapshot,\n () => \"\",\n );\n const optionsRef = useRef(options);\n optionsRef.current = options;\n const open = useCallback(\n (query?: OverlayQuery) => openOverlay(param, id, query),\n [param, id],\n );\n const close = useCallback(\n () => closeOverlay(param, id, optionsRef.current),\n [param, id],\n );\n return useMemo(\n () => ({\n isOpen: new URLSearchParams(search).get(param) === id,\n open,\n close,\n }),\n [search, param, id, open, close],\n );\n}\n\n/** Tailwind's default min-width breakpoints. */\nconst BREAKPOINTS = { sm: 640, md: 768, lg: 1024, xl: 1280 } as const;\nexport type ResponsivePresentation =\n | Presentation\n | ({ base: Presentation } & Partial<\n Record<keyof typeof BREAKPOINTS, Presentation>\n >);\n/** Choose a presentation using media queries. The server renders `base`. */\nfunction usePresentation(value: ResponsivePresentation): Presentation {\n // Serialise so inline objects don't resubscribe on every render.\n const key = typeof value === \"string\" ? value : JSON.stringify(value);\n const resolve = useCallback((): Presentation => {\n const parsed = (\n key.startsWith(\"{\") ? JSON.parse(key) : key\n ) as ResponsivePresentation;\n if (typeof parsed === \"string\") {\n return parsed;\n }\n let current = parsed.base;\n for (const [name, width] of Object.entries(BREAKPOINTS)) {\n const at = parsed[name as keyof typeof BREAKPOINTS];\n if (at && window.matchMedia(`(min-width: ${width}px)`).matches) {\n current = at;\n }\n }\n return current;\n }, [key]);\n const subscribe = useCallback(\n (onChange: () => void) => {\n if (!key.startsWith(\"{\")) {\n return () => {};\n }\n const queries = Object.values(BREAKPOINTS).map((width) =>\n window.matchMedia(`(min-width: ${width}px)`),\n );\n queries.forEach((query) => query.addEventListener(\"change\", onChange));\n return () =>\n queries.forEach((query) =>\n query.removeEventListener(\"change\", onChange),\n );\n },\n [key],\n );\n return useSyncExternalStore(subscribe, resolve, () =>\n typeof value === \"string\" ? value : value.base,\n );\n}\n\ntype OverlayState = ReturnType<typeof useOverlay>;\ntype OverlayContext = OverlayState & {\n presentation: Presentation;\n swipeDirection: SwipeDirection;\n contentId: string;\n setContentId: (id: string) => void;\n trigger: { current: HTMLElement | null };\n};\nconst Context = createContext<OverlayContext | null>(null);\nconst EngineContext = createContext<EngineModule | null>(null);\nfunction useContextValue() {\n const value = useContext(Context);\n if (!value) {\n throw new Error(\"Overlay parts require Overlay.Root\");\n }\n return value;\n}\nconst prefetch = () => void loadOverlayEngine().catch(() => {});\n\n/**\n * The URL is the only open state. The engine (Base UI) supplies the modal mechanics: portal,\n * focus scope, scroll lock, outside and Escape dismissal, exit presence and, for drawers and\n * sheets, swipe-to-dismiss. Every open or close it requests goes through the history core.\n */\nfunction Root({\n param,\n id,\n presentation = \"dialog\",\n swipeDirection,\n fallback,\n omitParams,\n navigation,\n ...props\n}: PartProps<OverlayState> & {\n param: string;\n id: string;\n /** Your URL adapter for overlays whose open state lives in the path. */\n navigation?: OverlayState;\n /** A `dialog`, swipeable `drawer` or `sheet`. Use `{ base, sm, md, lg, xl }` for responsive layouts. */\n presentation?: ResponsivePresentation;\n /** Swipe direction to dismiss. Defaults to `down` for drawers and `right` for sheets. */\n swipeDirection?: SwipeDirection;\n} & CloseOptions) {\n const queryOverlay = useOverlay(param, id, { fallback, omitParams });\n const overlay = navigation ?? queryOverlay;\n const resolved = usePresentation(presentation);\n const generated = useId();\n const [contentId, setContentId] = useState(`zappie-overlay-${generated}`);\n const trigger = useRef<HTMLElement | null>(null);\n useEffect(() => {\n if (engineModule) {\n return undefined;\n }\n if (typeof window.requestIdleCallback === \"function\") {\n const handle = window.requestIdleCallback(prefetch);\n return () => window.cancelIdleCallback(handle);\n }\n const handle = window.setTimeout(prefetch, 1500);\n return () => window.clearTimeout(handle);\n }, []);\n const context = useMemo(\n () => ({\n ...overlay,\n presentation: resolved,\n swipeDirection:\n swipeDirection ?? (resolved === \"sheet\" ? \"right\" : \"down\"),\n contentId,\n setContentId,\n trigger,\n }),\n [overlay, resolved, swipeDirection, contentId],\n );\n return (\n <Context.Provider value={context}>\n {part(\"div\", props, overlay, {\n \"data-state\": overlay.isOpen ? \"open\" : \"closed\",\n \"data-presentation\": resolved,\n })}\n </Context.Provider>\n );\n}\nconst Trigger = forwardRef<HTMLElement, PartProps<OverlayState>>(\n function Trigger(props, ref) {\n const ctx = useContextValue();\n return part(\n \"button\",\n props,\n ctx,\n {\n type: \"button\",\n \"aria-haspopup\": \"dialog\",\n \"aria-expanded\": ctx.isOpen,\n \"aria-controls\": ctx.contentId,\n \"data-state\": ctx.isOpen ? \"open\" : \"closed\",\n onPointerEnter: prefetch,\n onFocus: prefetch,\n onClick: () => ctx.open(),\n },\n composeRefs(ref, (node: HTMLElement | null) => {\n ctx.trigger.current = node;\n }),\n );\n },\n);\nexport type OverlayContentProps = PartProps<OverlayState> & {\n /** Runs on Escape. Call `event.preventDefault()` to keep the overlay open. */\n onEscape?: (event: KeyboardEvent) => void;\n /** Runs on an outside pointer press or focus. Call `event.preventDefault()` to keep the overlay open. */\n onInteractOutside?: (event: Event) => void;\n /** Keep the content mounted while closed, for exit animations driven by `data-state`. */\n forceMount?: true;\n /** Where to mount the content. Defaults to `document.body`. */\n container?: HTMLElement | null;\n};\nconst Content = forwardRef<HTMLElement, OverlayContentProps>(function Content(\n { onEscape, onInteractOutside, forceMount, container, ...props },\n ref,\n) {\n const ctx = useContextValue();\n const [engine, setEngine] = useState(engineModule);\n // Once opened, the surface stays mounted so the engine can play exit transitions.\n const [opened, setOpened] = useState(ctx.isOpen);\n if (ctx.isOpen && !opened) {\n setOpened(true);\n }\n const customId = partId(props as Parameters<typeof partId>[0]);\n const { setContentId } = ctx;\n useEffect(() => {\n if (customId) {\n setContentId(customId);\n }\n }, [customId, setContentId]);\n useEffect(() => {\n if (engine || !(ctx.isOpen || forceMount)) {\n return undefined;\n }\n let live = true;\n void loadOverlayEngine().then(\n (module) => {\n if (live) {\n setEngine(module);\n }\n },\n () => {},\n );\n return () => {\n live = false;\n };\n }, [engine, ctx.isOpen, forceMount]);\n if (!engine || (!opened && !forceMount)) {\n return null;\n }\n return (\n <EngineContext.Provider value={engine}>\n <engine.Surface\n open={ctx.isOpen}\n presentation={ctx.presentation}\n swipeDirection={ctx.swipeDirection}\n contentId={ctx.contentId}\n trigger={ctx.trigger}\n onOpen={() => ctx.open()}\n onClose={ctx.close}\n onEscape={onEscape}\n onInteractOutside={onInteractOutside}\n forceMount={forceMount}\n container={container}\n state={ctx}\n contentProps={props as PartProps<never>}\n contentRef={ref}\n />\n </EngineContext.Provider>\n );\n});\nfunction useEngine() {\n const engine = useContext(EngineContext);\n if (!engine) {\n throw new Error(\n \"Overlay.Title and Overlay.Close must be inside Overlay.Content\",\n );\n }\n return engine;\n}\nconst Title = forwardRef<HTMLElement, PartProps<OverlayState>>(\n function Title(props, ref) {\n const ctx = useContextValue();\n const { EngineTitle } = useEngine();\n return (\n <EngineTitle {...(props as PartProps<unknown>)} state={ctx} ref={ref} />\n );\n },\n);\nconst Close = forwardRef<HTMLElement, PartProps<OverlayState>>(\n function Close(props, ref) {\n const ctx = useContextValue();\n const { EngineClose } = useEngine();\n return (\n <EngineClose {...(props as PartProps<unknown>)} state={ctx} ref={ref} />\n );\n },\n);\n/**\n * Composable dialog, drawer and sheet parts with URL-backed open state and a lazy modal engine.\n * Use Root, Trigger, Content, Title and Close to connect history, focus handling and dismissal.\n */\nexport const Overlay = { Root, Trigger, Content, Title, Close };\n"],
5
+ "mappings": "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",
6
+ "names": ["handle", "Trigger", "Content", "Title", "Close"]
7
+ }
@@ -0,0 +1,103 @@
1
+ "use client";
2
+
3
+ // src/payment/methods.tsx
4
+ import {
5
+ createContext,
6
+ forwardRef,
7
+ useContext
8
+ } from "react";
9
+ import { usePartId } from "../internal/ids.js";
10
+ import { part } from "../internal/slot.js";
11
+ import { useControllableState } from "../internal/use-controllable-state.js";
12
+ import { jsx } from "react/jsx-runtime";
13
+ var Context = createContext(null);
14
+ function Root({
15
+ methods,
16
+ value,
17
+ defaultValue = null,
18
+ onValueChange,
19
+ ...props
20
+ }) {
21
+ const [selected, set] = useControllableState({
22
+ value,
23
+ defaultValue,
24
+ onChange: onValueChange
25
+ });
26
+ const normalized = methods.map(
27
+ (method) => typeof method === "string" ? { value: method } : method
28
+ );
29
+ const setValue = (next) => {
30
+ if (normalized.some((method) => method.value === next && !method.disabled)) {
31
+ set(next);
32
+ }
33
+ };
34
+ const state = { methods: normalized, value: selected, setValue };
35
+ return /* @__PURE__ */ jsx(Context.Provider, { value: state, children: part("div", props, state, {
36
+ role: "radiogroup",
37
+ onKeyDown: (event) => {
38
+ if (![
39
+ "ArrowRight",
40
+ "ArrowDown",
41
+ "ArrowLeft",
42
+ "ArrowUp",
43
+ "Home",
44
+ "End"
45
+ ].includes(event.key)) {
46
+ return;
47
+ }
48
+ const buttons = Array.from(
49
+ event.currentTarget.querySelectorAll(
50
+ '[role="radio"]:not([aria-disabled="true"])'
51
+ )
52
+ );
53
+ if (!buttons.length) {
54
+ return;
55
+ }
56
+ event.preventDefault();
57
+ const current = buttons.indexOf(
58
+ document.activeElement
59
+ );
60
+ const index = event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : (current + (["ArrowLeft", "ArrowUp"].includes(event.key) ? -1 : 1) + buttons.length) % buttons.length;
61
+ buttons[index]?.focus();
62
+ buttons[index]?.click();
63
+ }
64
+ }) });
65
+ }
66
+ var Item = forwardRef(function Item2({ value, ...props }, ref) {
67
+ const ctx = useContext(Context);
68
+ const id = usePartId();
69
+ if (!ctx) {
70
+ throw new Error("PaymentMethods.Item requires PaymentMethods.Root");
71
+ }
72
+ const method = ctx.methods.find((method2) => method2.value === value);
73
+ if (!method) {
74
+ return null;
75
+ }
76
+ const selected = ctx.value === value;
77
+ const first = ctx.methods.find((method2) => !method2.disabled)?.value;
78
+ return part(
79
+ "button",
80
+ props,
81
+ { ...method, selected },
82
+ {
83
+ id,
84
+ type: "button",
85
+ role: "radio",
86
+ "aria-checked": selected,
87
+ "aria-disabled": method.disabled || void 0,
88
+ "aria-description": method.reason,
89
+ disabled: method.disabled,
90
+ tabIndex: selected || !ctx.value && first === value ? 0 : -1,
91
+ "data-disabled": method.disabled ? "" : void 0,
92
+ "data-selected": selected ? "" : void 0,
93
+ "data-state": selected ? "checked" : "unchecked",
94
+ onClick: () => ctx.setValue(value)
95
+ },
96
+ ref
97
+ );
98
+ });
99
+ var PaymentMethods = { Root, Item };
100
+ export {
101
+ PaymentMethods
102
+ };
103
+ //# sourceMappingURL=methods.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/payment/methods.tsx"],
4
+ "sourcesContent": ["\"use client\";\nimport type { PaymentMethodKind } from \"@zappie/sdk\";\nimport {\n createContext,\n forwardRef,\n useContext,\n type KeyboardEvent,\n} from \"react\";\nimport { usePartId } from \"../internal/ids\";\nimport { part, type PartProps } from \"../internal/slot\";\nimport { useControllableState } from \"../internal/use-controllable-state\";\n/**\n * A payment-method choice with optional disabled state and an explanatory reason.\n */\nexport type PaymentMethod = {\n value: PaymentMethodKind;\n disabled?: boolean;\n reason?: string;\n};\ntype PaymentState = {\n methods: PaymentMethod[];\n value: PaymentMethodKind | null;\n setValue: (value: PaymentMethodKind) => void;\n};\nconst Context = createContext<PaymentState | null>(null);\nfunction Root({\n methods,\n value,\n defaultValue = null,\n onValueChange,\n ...props\n}: PartProps<PaymentState> & {\n methods: (PaymentMethod | PaymentMethodKind)[];\n value?: PaymentMethodKind | null;\n defaultValue?: PaymentMethodKind | null;\n onValueChange?: (value: PaymentMethodKind | null) => void;\n}) {\n const [selected, set] = useControllableState({\n value,\n defaultValue,\n onChange: onValueChange,\n });\n const normalized = methods.map((method) =>\n typeof method === \"string\" ? { value: method } : method,\n );\n const setValue = (next: PaymentMethodKind) => {\n if (\n normalized.some((method) => method.value === next && !method.disabled)\n ) {\n set(next);\n }\n };\n const state = { methods: normalized, value: selected, setValue };\n return (\n <Context.Provider value={state}>\n {part(\"div\", props, state, {\n role: \"radiogroup\",\n onKeyDown: (event: KeyboardEvent<HTMLElement>) => {\n if (\n ![\n \"ArrowRight\",\n \"ArrowDown\",\n \"ArrowLeft\",\n \"ArrowUp\",\n \"Home\",\n \"End\",\n ].includes(event.key)\n ) {\n return;\n }\n const buttons = Array.from(\n event.currentTarget.querySelectorAll<HTMLElement>(\n '[role=\"radio\"]:not([aria-disabled=\"true\"])',\n ),\n );\n if (!buttons.length) {\n return;\n }\n event.preventDefault();\n const current = buttons.indexOf(\n document.activeElement as HTMLElement,\n );\n const index =\n event.key === \"Home\"\n ? 0\n : event.key === \"End\"\n ? buttons.length - 1\n : (current +\n ([\"ArrowLeft\", \"ArrowUp\"].includes(event.key) ? -1 : 1) +\n buttons.length) %\n buttons.length;\n buttons[index]?.focus();\n buttons[index]?.click();\n },\n })}\n </Context.Provider>\n );\n}\nconst Item = forwardRef<\n HTMLElement,\n PartProps<PaymentMethod & { selected: boolean }> & {\n value: PaymentMethodKind;\n }\n>(function Item({ value, ...props }, ref) {\n const ctx = useContext(Context);\n const id = usePartId();\n if (!ctx) {\n throw new Error(\"PaymentMethods.Item requires PaymentMethods.Root\");\n }\n const method = ctx.methods.find((method) => method.value === value);\n if (!method) {\n return null;\n }\n const selected = ctx.value === value;\n const first = ctx.methods.find((method) => !method.disabled)?.value;\n return part(\n \"button\",\n props,\n { ...method, selected },\n {\n id,\n type: \"button\",\n role: \"radio\",\n \"aria-checked\": selected,\n \"aria-disabled\": method.disabled || undefined,\n \"aria-description\": method.reason,\n disabled: method.disabled,\n tabIndex: selected || (!ctx.value && first === value) ? 0 : -1,\n \"data-disabled\": method.disabled ? \"\" : undefined,\n \"data-selected\": selected ? \"\" : undefined,\n \"data-state\": selected ? \"checked\" : \"unchecked\",\n onClick: () => ctx.setValue(value),\n },\n ref,\n );\n});\n/**\n * Composable payment-method radio controls with keyboard navigation.\n * Root manages the selection. Item renders each method and prevents selection when disabled.\n */\nexport const PaymentMethods = { Root, Item };\n"],
5
+ "mappings": "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",
6
+ "names": ["Item", "method"]
7
+ }
@@ -0,0 +1,9 @@
1
+ // src/rich-text/banners.ts
2
+ var BANNER_DELIMITER = "<::>";
3
+ var normalizeBannerDelimiters = (html) => html.replace(/<p>\s*(?:&lt;::&gt;|<::>)\s*<\/p>/gi, BANNER_DELIMITER).replaceAll("&lt;::&gt;", BANNER_DELIMITER);
4
+ var splitInfoBanner = (infoBanner) => normalizeBannerDelimiters(infoBanner ?? "").split(BANNER_DELIMITER);
5
+ export {
6
+ BANNER_DELIMITER,
7
+ splitInfoBanner
8
+ };
9
+ //# sourceMappingURL=banners.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/rich-text/banners.ts"],
4
+ "sourcesContent": ["// Multiple menu info banners are stored as one string joined by this delimiter\n// (the dashboard editor joins on it; here we split it back out for display).\n/**\n * The separator used to store multiple menu info banners in a single string.\n */\nexport const BANNER_DELIMITER = \"<::>\";\n\n// The delimiter looks like an HTML tag, so once it lands inside the dashboard's\n// rich-text editor as content it gets serialised as escaped text \u2014 `&lt;::&gt;`,\n// usually on its own `<p>` line. Splitting on the literal `<::>` then finds\n// nothing and every banner collapses into one. Convert the escaped form (and its\n// `<p>` wrapper) back to the real delimiter before splitting.\nconst normalizeBannerDelimiters = (html: string): string =>\n html\n .replace(/<p>\\s*(?:&lt;::&gt;|<::>)\\s*<\\/p>/gi, BANNER_DELIMITER)\n .replaceAll(\"&lt;::&gt;\", BANNER_DELIMITER);\n\n/** Split a stored info-banner value into individual banner HTML fragments. */\nexport const splitInfoBanner = (infoBanner?: string | null): string[] =>\n normalizeBannerDelimiters(infoBanner ?? \"\").split(BANNER_DELIMITER);\n"],
5
+ "mappings": ";AAKO,IAAM,mBAAmB;AAOhC,IAAM,4BAA4B,CAAC,SACjC,KACG,QAAQ,uCAAuC,gBAAgB,EAC/D,WAAW,cAAc,gBAAgB;AAGvC,IAAM,kBAAkB,CAAC,eAC9B,0BAA0B,cAAc,EAAE,EAAE,MAAM,gBAAgB;",
6
+ "names": []
7
+ }
@@ -18,7 +18,7 @@ var parseRichText = (html) => {
18
18
  const document = new DOMParser().parseFromString(html, "text/html");
19
19
  return Array.from(document.body.childNodes).map(convertNode);
20
20
  };
21
-
22
- export { parseRichText };
21
+ export {
22
+ parseRichText
23
+ };
23
24
  //# sourceMappingURL=browser.js.map
24
- //# sourceMappingURL=browser.js.map
@@ -1 +1,7 @@
1
- {"version":3,"sources":["../../src/rich-text/browser.ts"],"names":[],"mappings":";AAEA,IAAM,SAAA,GAAY,CAAA;AAClB,IAAM,YAAA,GAAe,CAAA;AAErB,IAAM,WAAA,GAAc,CAAC,IAAA,KAAmC;AACtD,EAAA,IAAI,IAAA,CAAK,aAAa,SAAA,EAAW;AAC/B,IAAA,OAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,IAAA,CAAK,aAAa,EAAA,EAAG;AAAA,EACpD;AAEA,EAAA,IAAI,IAAA,CAAK,aAAa,YAAA,EAAc;AAClC,IAAA,OAAO,EAAE,MAAM,SAAA,EAAU;AAAA,EAC3B;AAEA,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,KAAA;AAAA,IACN,IAAA,EAAM,IAAA,CAAK,QAAA,CAAS,WAAA,EAAY;AAAA,IAChC,UAAU,KAAA,CAAM,IAAA,CAAK,KAAK,UAAU,CAAA,CAAE,IAAI,WAAW;AAAA,GACvD;AACF,CAAA;AAMO,IAAM,aAAA,GAAgB,CAAC,IAAA,KAAuC;AACnE,EAAA,MAAM,WAAW,IAAI,SAAA,EAAU,CAAE,eAAA,CAAgB,MAAM,WAAW,CAAA;AAClE,EAAA,OAAO,MAAM,IAAA,CAAK,QAAA,CAAS,KAAK,UAAU,CAAA,CAAE,IAAI,WAAW,CAAA;AAC7D","file":"browser.js","sourcesContent":["import type { ParsedRichTextNode } from \"./core\";\n\nconst TEXT_NODE = 3;\nconst COMMENT_NODE = 8;\n\nconst convertNode = (node: Node): ParsedRichTextNode => {\n if (node.nodeType === TEXT_NODE) {\n return { type: \"text\", data: node.nodeValue || \"\" };\n }\n\n if (node.nodeType === COMMENT_NODE) {\n return { type: \"comment\" };\n }\n\n return {\n type: \"tag\",\n name: node.nodeName.toLowerCase(),\n children: Array.from(node.childNodes).map(convertNode),\n };\n};\n\n/**\n * Parse HTML with the browser DOMParser and convert body children to the shared rich-text node format.\n * Requires a DOM environment. Pair with createSanitizeRichText to remove unsupported content.\n */\nexport const parseRichText = (html: string): ParsedRichTextNode[] => {\n const document = new DOMParser().parseFromString(html, \"text/html\");\n return Array.from(document.body.childNodes).map(convertNode);\n};\n"]}
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/rich-text/browser.ts"],
4
+ "sourcesContent": ["import type { ParsedRichTextNode } from \"./core\";\n\nconst TEXT_NODE = 3;\nconst COMMENT_NODE = 8;\n\nconst convertNode = (node: Node): ParsedRichTextNode => {\n if (node.nodeType === TEXT_NODE) {\n return { type: \"text\", data: node.nodeValue || \"\" };\n }\n\n if (node.nodeType === COMMENT_NODE) {\n return { type: \"comment\" };\n }\n\n return {\n type: \"tag\",\n name: node.nodeName.toLowerCase(),\n children: Array.from(node.childNodes).map(convertNode),\n };\n};\n\n/**\n * Parse HTML with the browser DOMParser and convert body children to the shared rich-text node format.\n * Requires a DOM environment. Pair with createSanitizeRichText to remove unsupported content.\n */\nexport const parseRichText = (html: string): ParsedRichTextNode[] => {\n const document = new DOMParser().parseFromString(html, \"text/html\");\n return Array.from(document.body.childNodes).map(convertNode);\n};\n"],
5
+ "mappings": ";AAEA,IAAM,YAAY;AAClB,IAAM,eAAe;AAErB,IAAM,cAAc,CAAC,SAAmC;AACtD,MAAI,KAAK,aAAa,WAAW;AAC/B,WAAO,EAAE,MAAM,QAAQ,MAAM,KAAK,aAAa,GAAG;AAAA,EACpD;AAEA,MAAI,KAAK,aAAa,cAAc;AAClC,WAAO,EAAE,MAAM,UAAU;AAAA,EAC3B;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,KAAK,SAAS,YAAY;AAAA,IAChC,UAAU,MAAM,KAAK,KAAK,UAAU,EAAE,IAAI,WAAW;AAAA,EACvD;AACF;AAMO,IAAM,gBAAgB,CAAC,SAAuC;AACnE,QAAM,WAAW,IAAI,UAAU,EAAE,gBAAgB,MAAM,WAAW;AAClE,SAAO,MAAM,KAAK,SAAS,KAAK,UAAU,EAAE,IAAI,WAAW;AAC7D;",
6
+ "names": []
7
+ }
@@ -0,0 +1,75 @@
1
+ // src/rich-text/core.ts
2
+ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
3
+ "b",
4
+ "blockquote",
5
+ "br",
6
+ "code",
7
+ "del",
8
+ "em",
9
+ "h1",
10
+ "h2",
11
+ "h3",
12
+ "h4",
13
+ "h5",
14
+ "h6",
15
+ "hr",
16
+ "i",
17
+ "li",
18
+ "ol",
19
+ "p",
20
+ "pre",
21
+ "s",
22
+ "span",
23
+ "strike",
24
+ "strong",
25
+ "sub",
26
+ "sup",
27
+ "u",
28
+ "ul"
29
+ ]);
30
+ var DROP_CONTENT_TAGS = /* @__PURE__ */ new Set([
31
+ "audio",
32
+ "canvas",
33
+ "embed",
34
+ "iframe",
35
+ "math",
36
+ "noscript",
37
+ "object",
38
+ "script",
39
+ "style",
40
+ "svg",
41
+ "template",
42
+ "video"
43
+ ]);
44
+ var VOID_TAGS = /* @__PURE__ */ new Set(["br", "hr"]);
45
+ var escapeText = (value) => value.replaceAll("\r\n", "\n").replaceAll("\r", "\n").replaceAll("&", "&amp;").replaceAll("\xA0", "&nbsp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
46
+ var serializeNode = (node) => {
47
+ if (node.type === "text") {
48
+ return escapeText(node.data || "");
49
+ }
50
+ if (node.type === "comment" || node.type === "directive") {
51
+ return "";
52
+ }
53
+ const tagName = node.name?.toLowerCase();
54
+ if (tagName && DROP_CONTENT_TAGS.has(tagName)) {
55
+ return "";
56
+ }
57
+ const children = (node.children || []).map(serializeNode).join("");
58
+ if (!tagName || !ALLOWED_TAGS.has(tagName)) {
59
+ return children;
60
+ }
61
+ if (VOID_TAGS.has(tagName)) {
62
+ return `<${tagName}>`;
63
+ }
64
+ return `<${tagName}>${children}</${tagName}>`;
65
+ };
66
+ var createSanitizeRichText = (parseRichText) => (html) => {
67
+ if (!html) {
68
+ return "";
69
+ }
70
+ return parseRichText(html).map(serializeNode).join("");
71
+ };
72
+ export {
73
+ createSanitizeRichText
74
+ };
75
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/rich-text/core.ts"],
4
+ "sourcesContent": ["/**\n * A parser-neutral rich-text node containing its type, optional tag name, text and children.\n */\nexport type ParsedRichTextNode = {\n type: string;\n name?: string;\n data?: string;\n children?: ParsedRichTextNode[];\n};\n\n/**\n * A parser adapter that converts HTML into the node tree consumed by the rich-text sanitizer.\n */\nexport type ParseRichText = (html: string) => ParsedRichTextNode[];\n\nconst ALLOWED_TAGS = new Set([\n \"b\",\n \"blockquote\",\n \"br\",\n \"code\",\n \"del\",\n \"em\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n \"hr\",\n \"i\",\n \"li\",\n \"ol\",\n \"p\",\n \"pre\",\n \"s\",\n \"span\",\n \"strike\",\n \"strong\",\n \"sub\",\n \"sup\",\n \"u\",\n \"ul\",\n]);\n\nconst DROP_CONTENT_TAGS = new Set([\n \"audio\",\n \"canvas\",\n \"embed\",\n \"iframe\",\n \"math\",\n \"noscript\",\n \"object\",\n \"script\",\n \"style\",\n \"svg\",\n \"template\",\n \"video\",\n]);\n\nconst VOID_TAGS = new Set([\"br\", \"hr\"]);\n\nconst escapeText = (value: string): string =>\n value\n .replaceAll(\"\\r\\n\", \"\\n\")\n .replaceAll(\"\\r\", \"\\n\")\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"\\u00a0\", \"&nbsp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\");\n\nconst serializeNode = (node: ParsedRichTextNode): string => {\n if (node.type === \"text\") {\n return escapeText(node.data || \"\");\n }\n\n if (node.type === \"comment\" || node.type === \"directive\") {\n return \"\";\n }\n\n const tagName = node.name?.toLowerCase();\n if (tagName && DROP_CONTENT_TAGS.has(tagName)) {\n return \"\";\n }\n\n const children = (node.children || []).map(serializeNode).join(\"\");\n if (!tagName || !ALLOWED_TAGS.has(tagName)) {\n return children;\n }\n\n if (VOID_TAGS.has(tagName)) {\n return `<${tagName}>`;\n }\n\n return `<${tagName}>${children}</${tagName}>`;\n};\n\n/**\n * Create a sanitizer using your HTML parser adapter.\n * Keeps supported formatting tags without attributes, escapes text and removes unsafe element content.\n *\n * @param parseRichText - A parser that returns the shared rich-text node format.\n * @returns A sanitizer that accepts HTML and returns sanitized HTML, or an empty string for empty input.\n */\nexport const createSanitizeRichText =\n (parseRichText: ParseRichText) =>\n (html?: string | null): string => {\n if (!html) {\n return \"\";\n }\n\n return parseRichText(html).map(serializeNode).join(\"\");\n };\n"],
5
+ "mappings": ";AAeA,IAAM,eAAe,oBAAI,IAAI;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,IAAM,oBAAoB,oBAAI,IAAI;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,IAAM,YAAY,oBAAI,IAAI,CAAC,MAAM,IAAI,CAAC;AAEtC,IAAM,aAAa,CAAC,UAClB,MACG,WAAW,QAAQ,IAAI,EACvB,WAAW,MAAM,IAAI,EACrB,WAAW,KAAK,OAAO,EACvB,WAAW,QAAU,QAAQ,EAC7B,WAAW,KAAK,MAAM,EACtB,WAAW,KAAK,MAAM;AAE3B,IAAM,gBAAgB,CAAC,SAAqC;AAC1D,MAAI,KAAK,SAAS,QAAQ;AACxB,WAAO,WAAW,KAAK,QAAQ,EAAE;AAAA,EACnC;AAEA,MAAI,KAAK,SAAS,aAAa,KAAK,SAAS,aAAa;AACxD,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,KAAK,MAAM,YAAY;AACvC,MAAI,WAAW,kBAAkB,IAAI,OAAO,GAAG;AAC7C,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,KAAK,YAAY,CAAC,GAAG,IAAI,aAAa,EAAE,KAAK,EAAE;AACjE,MAAI,CAAC,WAAW,CAAC,aAAa,IAAI,OAAO,GAAG;AAC1C,WAAO;AAAA,EACT;AAEA,MAAI,UAAU,IAAI,OAAO,GAAG;AAC1B,WAAO,IAAI,OAAO;AAAA,EACpB;AAEA,SAAO,IAAI,OAAO,IAAI,QAAQ,KAAK,OAAO;AAC5C;AASO,IAAM,yBACX,CAAC,kBACD,CAAC,SAAiC;AAChC,MAAI,CAAC,MAAM;AACT,WAAO;AAAA,EACT;AAEA,SAAO,cAAc,IAAI,EAAE,IAAI,aAAa,EAAE,KAAK,EAAE;AACvD;",
6
+ "names": []
7
+ }
@@ -1,80 +1,4 @@
1
- // src/rich-text/banners.ts
2
- var BANNER_DELIMITER = "<::>";
3
- var normalizeBannerDelimiters = (html) => html.replace(/<p>\s*(?:&lt;::&gt;|<::>)\s*<\/p>/gi, BANNER_DELIMITER).replaceAll("&lt;::&gt;", BANNER_DELIMITER);
4
- var splitInfoBanner = (infoBanner) => normalizeBannerDelimiters(infoBanner ?? "").split(BANNER_DELIMITER);
5
-
6
- // src/rich-text/core.ts
7
- var ALLOWED_TAGS = /* @__PURE__ */ new Set([
8
- "b",
9
- "blockquote",
10
- "br",
11
- "code",
12
- "del",
13
- "em",
14
- "h1",
15
- "h2",
16
- "h3",
17
- "h4",
18
- "h5",
19
- "h6",
20
- "hr",
21
- "i",
22
- "li",
23
- "ol",
24
- "p",
25
- "pre",
26
- "s",
27
- "span",
28
- "strike",
29
- "strong",
30
- "sub",
31
- "sup",
32
- "u",
33
- "ul"
34
- ]);
35
- var DROP_CONTENT_TAGS = /* @__PURE__ */ new Set([
36
- "audio",
37
- "canvas",
38
- "embed",
39
- "iframe",
40
- "math",
41
- "noscript",
42
- "object",
43
- "script",
44
- "style",
45
- "svg",
46
- "template",
47
- "video"
48
- ]);
49
- var VOID_TAGS = /* @__PURE__ */ new Set(["br", "hr"]);
50
- var escapeText = (value) => value.replaceAll("\r\n", "\n").replaceAll("\r", "\n").replaceAll("&", "&amp;").replaceAll("\xA0", "&nbsp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
51
- var serializeNode = (node) => {
52
- if (node.type === "text") {
53
- return escapeText(node.data || "");
54
- }
55
- if (node.type === "comment" || node.type === "directive") {
56
- return "";
57
- }
58
- const tagName = node.name?.toLowerCase();
59
- if (tagName && DROP_CONTENT_TAGS.has(tagName)) {
60
- return "";
61
- }
62
- const children = (node.children || []).map(serializeNode).join("");
63
- if (!tagName || !ALLOWED_TAGS.has(tagName)) {
64
- return children;
65
- }
66
- if (VOID_TAGS.has(tagName)) {
67
- return `<${tagName}>`;
68
- }
69
- return `<${tagName}>${children}</${tagName}>`;
70
- };
71
- var createSanitizeRichText = (parseRichText) => (html) => {
72
- if (!html) {
73
- return "";
74
- }
75
- return parseRichText(html).map(serializeNode).join("");
76
- };
77
-
78
- export { BANNER_DELIMITER, createSanitizeRichText, splitInfoBanner };
1
+ // src/rich-text/index.ts
2
+ export * from "./banners.js";
3
+ export * from "./core.js";
79
4
  //# sourceMappingURL=index.js.map
80
- //# sourceMappingURL=index.js.map