@8ux-co/eelzap 0.11.0 → 0.11.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.
- package/CHANGELOG.md +25 -1
- package/README.md +18 -2
- package/dist/{chunk-72YCJCBI.js → chunk-G2JAAZKS.js} +47 -11
- package/dist/chunk-G2JAAZKS.js.map +1 -0
- package/dist/fields.cjs +3 -0
- package/dist/fields.cjs.map +1 -1
- package/dist/fields.d.cts +22 -1
- package/dist/fields.d.ts +22 -1
- package/dist/fields.js +3 -0
- package/dist/fields.js.map +1 -1
- package/dist/index.d.cts +42 -5
- package/dist/index.d.ts +42 -5
- package/dist/preview.d.ts +54 -20
- package/dist/preview.js +65 -37
- package/dist/preview.js.map +1 -1
- package/dist/react.d.ts +43 -7
- package/dist/react.js +33 -4
- package/dist/react.js.map +1 -1
- package/dist/{suggest-ERMR7PCE.js → suggest-FDD4MBRP.js} +73 -39
- package/dist/suggest-FDD4MBRP.js.map +1 -0
- package/package.json +15 -12
- package/dist/chunk-72YCJCBI.js.map +0 -1
- package/dist/suggest-ERMR7PCE.js.map +0 -1
package/dist/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/react.ts","../src/refs.ts","../src/preview/site/shortcut.ts","../src/boot/local.ts","../src/boot/release.ts","../src/boot/boot.ts","../src/boot/component.ts"],"sourcesContent":["import * as React from 'react'\n\nimport { recordRefsOf, type EntryLike, type RecordRef } from './refs'\nimport type { PreviewValue, ValuesPayload } from './preview/protocol'\n\nexport { bootZapPreview, type ZapBootOptions } from './boot/boot'\nexport { ZapPreview, type ZapPreviewProps } from './boot/component'\nexport { recordRefsOf, zapAttrs, type EntryLike } from './refs'\n\n/**\n * `@8ux-co/eelzap/react` (zap-cms-v2 §2.5, §2.7): the preview boot for React\n * sites (`ZapPreview`, `bootZapPreview`) and the hook below. Nothing here\n * bundles the overlay: it loads from the CDN in a preview session, and\n * announces each `zap:values` patch with a window event (`eelzap:values`),\n * which `onValues` and `useZapLiveUpdates` listen to.\n *\n * `useZapLiveUpdates(entry)` returns `entry` with the editor's unsaved values\n * merged in while the page is framed by Zap, and `entry` itself (the same\n * object) otherwise, the shape of Contentful's `useContentfulLiveUpdates`:\n *\n * ```tsx\n * const post = useZapLiveUpdates(props.post)\n * return <h1 {...zapAttrs(post, 'title')}>{post.content.title}</h1>\n * ```\n *\n * The entry is a delivery API item or document (its `content` gets the\n * values) or any object keyed by field (the object itself gets them). Values\n * arrive formatted for display, as the overlay writes them: text, a URL and an\n * email as a string, rich text as its sanitised HTML, an image as\n * `{ url, alt }` merged into what the field held. The boot must run somewhere on the page (`ZapPreview`);\n * without it, or outside Zap, the hook never changes anything.\n */\n\ntype Patch = Record<string, PreviewValue>\n\n/** One display value as a plain field value, merged into what the field held. */\nexport function toFieldValue(value: PreviewValue, previous: unknown): unknown {\n if (value === null || typeof value !== 'object') return value\n if ('text' in value) return value.text\n if ('html' in value) return value.html\n if ('url' in value) return value.url\n if ('email' in value) return value.email\n const image = value.image\n const base =\n previous && typeof previous === 'object' && !Array.isArray(previous)\n ? (previous as Record<string, unknown>)\n : {}\n const next: Record<string, unknown> = { ...base }\n if (image.src !== undefined) next.url = image.src\n if (image.alt !== undefined) next.alt = image.alt\n return next\n}\n\n/** `entry` with `patch` merged into its `content` (or into itself). Pure. */\nexport function mergeLiveValues<T>(entry: T, patch: Patch): T {\n const keys = Object.keys(patch)\n if (keys.length === 0 || !entry || typeof entry !== 'object') return entry\n const record = entry as Record<string, unknown>\n const content = record.content\n const target =\n content && typeof content === 'object' && !Array.isArray(content)\n ? (content as Record<string, unknown>)\n : record\n const merged: Record<string, unknown> = { ...target }\n for (const key of keys) merged[key] = toFieldValue(patch[key] as PreviewValue, target[key])\n return (target === record ? merged : { ...record, content: merged }) as T\n}\n\n/** The window event the overlay announces every `zap:values` patch with. */\nexport const VALUES_EVENT = 'eelzap:values'\n\n/** A values event's detail, checked enough to merge it safely. */\nfunction readValues(event: Event): ValuesPayload | null {\n const detail = (event as CustomEvent<unknown>).detail as Partial<ValuesPayload> | null\n if (!detail || typeof detail.recordRef !== 'string') return null\n const patch = detail.patch\n if (!patch || typeof patch !== 'object' || Array.isArray(patch)) return null\n return detail as ValuesPayload\n}\n\n/** `onValues` for references already resolved. */\nexport function subscribeRefs(\n refs: readonly RecordRef[],\n handler: (patch: Patch, payload: ValuesPayload) => void,\n win: Window | undefined = typeof window === 'undefined' ? undefined : window,\n): () => void {\n if (!win) return () => {}\n const listener = (event: Event) => {\n const payload = readValues(event)\n if (!payload || !refs.includes(payload.recordRef)) return\n try {\n handler(payload.patch, payload)\n } catch (error) {\n // A site handler that throws must not take the others down with it.\n console.error('eelzap: onValues handler failed', error)\n }\n }\n win.addEventListener(VALUES_EVENT, listener)\n return () => win.removeEventListener(VALUES_EVENT, listener)\n}\n\n/**\n * Call `handler` with each unsaved-values patch for `entry` while the page is\n * in Zap's preview, for sites that compute things from values (excerpts,\n * lists, counts). Framework-free despite living here. Returns an unsubscribe\n * function; outside a preview it never fires.\n */\nexport function onValues(\n entry: EntryLike,\n handler: (patch: Patch, payload: ValuesPayload) => void,\n): () => void {\n return subscribeRefs(recordRefsOf(entry), handler)\n}\n\nexport function useZapLiveUpdates<T extends EntryLike>(entry: T): T {\n // Every reference counts: the editor names the record by whichever one the\n // page tagged (the default slug, or a localized one).\n const refsKey = recordRefsOf(entry).join('|')\n const [state, setState] = React.useState<{ refsKey: string; patch: Patch }>({\n refsKey,\n patch: {},\n })\n\n React.useEffect(() => {\n if (!refsKey) return\n return subscribeRefs(refsKey.split('|'), (patch) => {\n setState((previous) => ({\n refsKey,\n patch: previous.refsKey === refsKey ? { ...previous.patch, ...patch } : { ...patch },\n }))\n })\n // `refsKey` stands for the entry's identity: new data for the same entry\n // keeps the unsaved values on top of it.\n }, [refsKey])\n\n const patch = state.refsKey === refsKey ? state.patch : null\n return React.useMemo(() => (patch ? mergeLiveValues(entry, patch) : entry), [entry, patch])\n}\n","/**\n * The record and field grammar of tags (`.docs/proposals/zap-cms-v2.md` §2.4),\n * in a module of its own so the light entries (`./fields`, `./react`, the\n * boot) can name records without pulling in the preview protocol.\n * No imports, no side effects.\n */\n\n/**\n * A record reference in its canonical string form: `collectionKey/slug` for an\n * entry, `doc:documentKey` for a document (the tag grammar, §2.4).\n */\nexport type RecordRef = string\n\n/** Zap's field key pattern. */\nexport const FIELD_KEY_RE = /^[a-z][a-z0-9_]{0,127}$/\n/** Entry and document references; keys and slugs are `[a-z0-9]+(?:-[a-z0-9]+)*`. */\nconst ENTRY_REF_RE = /^([a-z0-9]+(?:-[a-z0-9]+)*)\\/([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst DOC_REF_RE = /^doc:([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst MAX_REF_LENGTH = 300\n\nexport type ParsedRecordRef =\n | { kind: 'entry'; collection: string; slug: string }\n | { kind: 'document'; key: string }\n\nexport function parseRecordRef(value: unknown): ParsedRecordRef | null {\n if (typeof value !== 'string' || value.length > MAX_REF_LENGTH) return null\n const doc = DOC_REF_RE.exec(value)\n if (doc) return { kind: 'document', key: doc[1]! }\n const entry = ENTRY_REF_RE.exec(value)\n if (entry) return { kind: 'entry', collection: entry[1]!, slug: entry[2]! }\n return null\n}\n\nexport function isRecordRef(value: unknown): value is RecordRef {\n return parseRecordRef(value) !== null\n}\n\n/** `zpt_` + 32 bytes in base64url, as Zap mints it (zap-cms-v2 §3.5). */\nexport const PREVIEW_TOKEN_RE = /^zpt_[A-Za-z0-9_-]{43}$/\n\n/** A preview token in the shape Zap mints; says nothing about whether it is live. */\nexport function isPreviewTokenShape(value: unknown): value is string {\n return typeof value === 'string' && PREVIEW_TOKEN_RE.test(value)\n}\n\nexport function isFieldKey(value: unknown): value is string {\n return typeof value === 'string' && FIELD_KEY_RE.test(value)\n}\n\n/**\n * What `zapAttrs` and `onValues` accept: a record reference string\n * (`blog/hola`, `doc:home`) or a delivery API entry (an item carries\n * `collection.key` and `slug`, a document `key`).\n */\nexport type EntryLike =\n | RecordRef\n | {\n slug?: string | null\n key?: string | null\n collection?: { key?: string | null } | null\n localizedSlugs?: Record<string, string | null | undefined> | null\n }\n\n/** Every reference an entry answers to (default slug first, then localized ones). */\nexport function recordRefsOf(entry: EntryLike | null | undefined): RecordRef[] {\n if (!entry) return []\n if (typeof entry === 'string') return isRecordRef(entry) ? [entry] : []\n const refs: RecordRef[] = []\n const add = (ref: string) => {\n if (isRecordRef(ref) && !refs.includes(ref)) refs.push(ref)\n }\n const collection = entry.collection?.key\n if (collection) {\n if (entry.slug) add(`${collection}/${entry.slug}`)\n for (const slug of Object.values(entry.localizedSlugs ?? {})) {\n if (slug) add(`${collection}/${slug}`)\n }\n } else if (entry.key) {\n add(`doc:${entry.key}`)\n }\n return refs\n}\n\n/**\n * The tag for one field, as attributes to spread on the element that renders\n * it: `{ 'data-zap': 'blog/hola#title' }`. Empty when the entry or the field\n * key is not one Zap can name, so a bad call never breaks the page.\n */\nexport function zapAttrs(entry: EntryLike, fieldKey: string): { 'data-zap'?: string } {\n const [ref] = recordRefsOf(entry)\n if (!ref || !isFieldKey(fieldKey)) return {}\n return { 'data-zap': `${ref}#${fieldKey}` }\n}\n","/**\n * The keyboard shortcut that opens «Editar» and «Comentar» on the live site\n * (zap-cms-v2 §3.4, owner 2026-10-05): Shift plus one letter, `Z` by default.\n * It replaced «Shift twice», which fired while people typed capitals.\n *\n * Shared by the boot every visitor runs (`src/boot/boot.ts`, under 1 KB) and\n * the overlay core (`./boot.ts`), so the two can never disagree about what\n * counts. No directive, no imports.\n *\n * It never fires while the person is typing: focus in an input, a textarea, a\n * select or anything editable, or a key that is part of an IME composition\n * (`isComposing`, or the legacy `keyCode` 229 some browsers send instead).\n */\n\n/** The letter pressed with Shift when a site sets none. */\nexport const DEFAULT_SHORTCUT = 'Z'\n\n/**\n * Where typing happens: the target itself or anything it sits in. A\n * `contenteditable=\"false\"` region is not typing, so the shortcut still works\n * there; a field being edited on the live site is, so Shift+Z types a capital.\n */\nconst TYPING = 'input,textarea,select,[contenteditable]:not([contenteditable=\"false\"])'\n\nexport function isShortcut(event: KeyboardEvent, key: string): boolean {\n // The real target, inside an open shadow root too (a site's own components).\n const target = (event.composedPath?.()[0] ?? event.target) as Element | null\n return (\n event.shiftKey &&\n !(event.ctrlKey || event.metaKey || event.altKey || event.repeat || event.isComposing) &&\n // Some browsers report a key inside an IME composition only as keyCode 229.\n event.keyCode !== 229 &&\n event.key?.toLowerCase() === key.toLowerCase() &&\n !target?.closest?.(TYPING)\n )\n}\n","/**\n * Development origins: `localhost`, `127.0.0.1`, `[::1]` and any `*.localhost`\n * name (which browsers resolve to the machine itself and treat as secure\n * contexts), over http OR https, any port: a site in development runs on\n * either.\n *\n * Shared by the boot (`./boot.ts`, which every visitor runs, so one regex and\n * no more) and the overlay (`../preview/live.ts`), so the two never disagree\n * about what counts as local.\n */\nexport const LOCAL_ORIGIN =\n /^https?:\\/\\/(?:(?:[a-z\\d-]+\\.)*localhost|127\\.0\\.0\\.1|\\[::1\\])(?::\\d+)?$/i\n\n/** True for a development hostname (no scheme, no port). */\nexport const isLocalHost = (hostname: string): boolean => LOCAL_ORIGIN.test(`http://${hostname}`)\n","// GENERATED by packages/zap-sdk/scripts/release-cdn.mjs. Committed: the CDN\n// release of `./preview` the boot loads, compiled into the npm build and the\n// script-tag boot. The boot joins the path to the Zap origin it trusts (the\n// framing editor, the configured Zap, or production); the SRI hash pins the\n// bytes. apps/zap live-client.spec.ts checks it names a released file.\n\nexport const PREVIEW_RELEASE_PATH = '/js/preview/preview.v1.65276174c667c5b2.js'\n\n// Lazy chunks pinned by the overlay; checked before npm publication.\nexport const PREVIEW_RELEASE_DEPENDENCIES = [\n '/js/preview/signin.v1.477e8b5df57ba15e.js',\n '/js/preview/suggest.v1.cbb2a568bdeba1d5.js',\n] as const\n\nexport const PREVIEW_RELEASE_INTEGRITY =\n 'sha384-bWEm1wybKLG8kadCDDKNtPJ09317f/GidS4+2uXW/waeV//cFIk1kQ6BHxiYaIoZ'\n","import { DEFAULT_SHORTCUT, isShortcut } from '../preview/site/shortcut'\nimport { LOCAL_ORIGIN } from './local'\nimport { PREVIEW_RELEASE_INTEGRITY, PREVIEW_RELEASE_PATH } from './release'\n\n/**\n * The boot (`.docs/proposals/zap-cms-v2.md` §2.7 \"What a visitor pays\",\n * §7.5): the only Zap code an ordinary visitor runs. Under 1 KB gzip, and it\n * makes NO request unless a preview session is present:\n *\n * 1. the server says a draft-mode session is active (`preview: true`);\n * 2. the page is framed by a Zap origin: framed, and, where the browser tells\n * (`location.ancestorOrigins`, Chrome and Safari), the parent is the\n * production editor or a local Zap seen from a local page (Zap's own\n * preview deployments are not editors) (`./local.ts`: localhost, 127.0.0.1,\n * [::1] or *.localhost, over http or https); where it cannot tell\n * (Firefox), any frame counts and the overlay itself refuses every editor\n * that is not Zap;\n * and with a site id (suggestion mode, §3.4):\n * 3. the URL carries `?zap`;\n * 4. this page is the OAuth callback of a sign-in this client started\n * (`/?code&state=zap1.…`);\n * 5. this tab already holds a suggestion session or a pending sign-in\n * (`sessionStorage`);\n * 6. the shortcut is pressed: Shift Z by default, never while the person is\n * typing in a field or composing with an IME (`preview/site/shortcut.ts`).\n *\n * Only then does it add one `<script>` for `./preview`: the release path\n * compiled in (`./release.ts`) with its SRI hash, on a Zap origin. The origin\n * is the editor that framed the page when the browser names it and it is a\n * Zap origin (so a local Zap serves its own overlay), else `zapOrigin` when it\n * is a Zap origin by the same rule (a local Zap from a local page), else\n * production. Never a URL from the page or the query string, and the SRI\n * hash means only the released bytes run, whichever Zap serves them.\n *\n * One script per document: a second boot on the same page (React StrictMode\n * runs every effect twice, a remount, a script tag beside the component) finds\n * the first one by its SRI hash and adds nothing. It reads the parent's\n * origin, the URL, sessionStorage keys and key presses; it writes nothing and\n * draws nothing.\n */\n\nexport interface ZapBootOptions {\n /** The Zap site's key. */\n siteKey: string\n /** The Zap site's id: turns on suggestion mode on the live site (§3.4). */\n siteId?: string\n /** The site's draft-mode route path, announced to the editor (§2.5). */\n draftRoute?: string | null\n /** The server saw an active draft-mode session (Next: `draftMode().isEnabled`). */\n preview?: boolean\n /** The letter pressed with Shift to open «Editar» and «Comentar»; default `Z`, false for none. */\n shortcut?: string | false\n /**\n * The Zap the site works against (`EELZAP_ORIGIN`): the overlay loads from\n * it and trusts it. Honoured for a local Zap from a local page\n * (development) and for production; anything else is ignored.\n */\n zapOrigin?: string\n /** A local Nest, under the same rule. */\n authOrigin?: string\n}\n\n/**\n * The string options and the `data-*` attribute each travels in to the\n * overlay's script (and, in the script-tag boot, comes from).\n */\nexport const BOOT_ATTRIBUTES = {\n siteKey: 'site',\n siteId: 'site-id',\n draftRoute: 'zap-draft-route',\n shortcut: 'zap-shortcut',\n zapOrigin: 'zap-origin',\n authOrigin: 'auth-origin',\n} as const\n\nconst ZAP = 'https://zap.eel.software'\n\n/** A Zap origin, as seen from `page`: see the list above. */\nfunction isZap(origin: string, page: string): boolean {\n return origin === ZAP || (LOCAL_ORIGIN.test(origin) && LOCAL_ORIGIN.test(page))\n}\n\n/**\n * The parent's origin: undefined when not framed, '' when framed but the\n * browser does not say by whom (Firefox).\n */\nfunction parentOrigin(win: Window): string | undefined {\n if (win.parent === win) return undefined\n const ancestors = (win.location as Location & { ancestorOrigins?: DOMStringList }).ancestorOrigins\n return ancestors?.[0] ?? ''\n}\n\nfunction suggesting(win: Window): boolean {\n const query = new URLSearchParams(win.location.search)\n if (query.has('zap')) return true\n if (\n win.location.pathname === '/' &&\n query.has('code') &&\n /^zap1\\./.test(query.get('state') ?? '')\n )\n return true\n try {\n const store = win.sessionStorage\n return !!(store.getItem('eelzap:site-token') || store.getItem('eelzap:signin'))\n } catch {\n return false\n }\n}\n\n/** Start the boot. Returns a function that removes its one listener. */\nexport function bootZapPreview(options: ZapBootOptions, win: Window = window): () => void {\n const page = win.location.origin\n const parent = parentOrigin(win)\n // The framing editor when it is Zap, else '' (not framed, unnamed, or not Zap).\n const editor = parent && isZap(parent, page) ? parent : ''\n // Where the overlay comes from (see above): that editor, the configured\n // Zap (an exact origin, as `EELZAP_ORIGIN` holds it), or production.\n const zap = editor || (isZap(options.zapOrigin ?? '', page) ? options.zapOrigin! : ZAP)\n let loaded = false\n const load = (open?: boolean) => {\n if (loaded || !PREVIEW_RELEASE_PATH) return\n loaded = true\n stop()\n const doc = win.document\n if (doc.querySelector(`script[integrity=\"${PREVIEW_RELEASE_INTEGRITY}\"]`)) return\n const script = doc.createElement('script')\n script.src = zap + PREVIEW_RELEASE_PATH\n script.setAttribute('integrity', PREVIEW_RELEASE_INTEGRITY)\n script.setAttribute('crossorigin', 'anonymous')\n let key: keyof typeof BOOT_ATTRIBUTES\n for (key in BOOT_ATTRIBUTES) {\n const value = key === 'shortcut' && options.shortcut === false ? 'off' : options[key]\n if (typeof value === 'string') script.setAttribute(`data-${BOOT_ATTRIBUTES[key]}`, value)\n }\n // The overlay trusts the Zap it came from (a local one only from a local page).\n script.setAttribute('data-zap-origin', zap)\n if (open) script.setAttribute('data-zap-open', '')\n doc.head.appendChild(script)\n }\n\n const key = options.shortcut === undefined ? DEFAULT_SHORTCUT : options.shortcut\n const onKey = (event: KeyboardEvent) => {\n if (key && isShortcut(event, key)) load(true)\n }\n const stop = () => win.removeEventListener('keydown', onKey, true)\n\n if (!options.siteKey) return stop\n if (options.preview || parent === '' || editor) load()\n else if (options.siteId) {\n if (suggesting(win)) load()\n else if (key) win.addEventListener('keydown', onKey, true)\n }\n return stop\n}\n","import { createElement, Fragment, useEffect } from 'react'\n\nimport { bootZapPreview, type ZapBootOptions } from './boot'\n\nexport type ZapPreviewProps = ZapBootOptions\n\n/**\n * The preview boot as a React component (`@8ux-co/eelzap/react` and, with a\n * default draft route, `@8ux-co/eelzap/next`). Renders nothing; after mount it\n * runs `bootZapPreview`, which loads the overlay only in a preview session.\n *\n * ```tsx\n * <ZapPreview siteKey=\"verdeorigen\" siteId=\"…uuid…\" />\n * ```\n */\nexport function ZapPreview(props: ZapPreviewProps) {\n const { siteKey, siteId, draftRoute, preview, shortcut, zapOrigin, authOrigin } = props\n useEffect(\n () => bootZapPreview({ siteKey, siteId, draftRoute, preview, shortcut, zapOrigin, authOrigin }),\n [siteKey, siteId, draftRoute, preview, shortcut, zapOrigin, authOrigin],\n )\n return createElement(Fragment)\n}\n"],"mappings":"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","names":["key","patch"]}
|
|
1
|
+
{"version":3,"sources":["../src/react.ts","../src/currency-digits.ts","../src/refs.ts","../src/preview/site/shortcut.ts","../src/boot/local.ts","../src/boot/release.ts","../src/boot/boot.ts","../src/boot/component.ts","../src/version.ts"],"sourcesContent":["import * as React from 'react'\n\nimport { currencyDigits } from './currency-digits'\nimport { recordRefsOf, type EntryLike, type RecordRef } from './refs'\nimport type { PreviewValue, ValuesPayload } from './preview/protocol'\n\nexport { bootZapPreview, type ZapBootOptions } from './boot/boot'\nexport { ZapPreview, type ZapPreviewProps } from './boot/component'\nexport { recordRefsOf, zapAttrs, type EntryLike } from './refs'\n\n/**\n * `@8ux-co/eelzap/react` (zap-cms-v2 §2.5, §2.7): the preview boot for React\n * sites (`ZapPreview`, `bootZapPreview`) and the hook below. Nothing here\n * bundles the overlay: it loads from the CDN in a preview session, and\n * announces each `zap:values` patch with a window event (`eelzap:values`),\n * which `onValues` and `useZapLiveUpdates` listen to.\n *\n * `useZapLiveUpdates(entry)` returns `entry` with the editor's unsaved values\n * merged in while the page is framed by Zap, and `entry` itself (the same\n * object) otherwise, the shape of Contentful's `useContentfulLiveUpdates`:\n *\n * ```tsx\n * const post = useZapLiveUpdates(props.post)\n * return <h1 {...zapAttrs(post, 'title')}>{post.content.title}</h1>\n * ```\n *\n * The entry is a delivery API item or document (its `content` gets the\n * values) or any object keyed by field (the object itself gets them). Values\n * arrive formatted for display, as the overlay writes them: text, a URL and an\n * email as a string, rich text as its sanitised HTML, an image as\n * `{ url, alt }` merged into what the field held, a CURRENCY as\n * `{ amountMinor, currency, amount, text }` and an ENUM as `{ value, label }`.\n * The boot must run somewhere on the page (`ZapPreview`); without it, or\n * outside Zap, the hook never changes anything.\n */\n\ntype Patch = Record<string, PreviewValue>\n\n/**\n * One display value as a field value, merged into what the field held.\n *\n * A structured CURRENCY (`{ amountMinor, currency, text }`) and ENUM\n * (`{ value, label }`) come through in the delivery API's shapes, so code\n * reading `price.amountMinor`, `price.amount` or `category.value` keeps\n * working while the editor types. CURRENCY also keeps `text`, the figure\n * formatted the way Zap formats it, and recomputes `amount` (major units)\n * from the ISO 4217 exponent.\n */\nexport function toFieldValue(value: PreviewValue, previous: unknown): unknown {\n if (value === null || typeof value !== 'object') return value\n if ('amountMinor' in value) {\n return {\n amountMinor: value.amountMinor,\n currency: value.currency,\n amount: Number(`${value.amountMinor}e-${currencyDigits(value.currency)}`),\n text: value.text,\n }\n }\n if ('label' in value) return { value: value.value, label: value.label }\n if ('text' in value) return value.text\n if ('html' in value) return value.html\n if ('url' in value) return value.url\n if ('email' in value) return value.email\n const image = value.image\n const base =\n previous && typeof previous === 'object' && !Array.isArray(previous)\n ? (previous as Record<string, unknown>)\n : {}\n const next: Record<string, unknown> = { ...base }\n if (image.src !== undefined) next.url = image.src\n if (image.alt !== undefined) next.alt = image.alt\n return next\n}\n\n/** `entry` with `patch` merged into its `content` (or into itself). Pure. */\nexport function mergeLiveValues<T>(entry: T, patch: Patch): T {\n const keys = Object.keys(patch)\n if (keys.length === 0 || !entry || typeof entry !== 'object') return entry\n const record = entry as Record<string, unknown>\n const content = record.content\n const target =\n content && typeof content === 'object' && !Array.isArray(content)\n ? (content as Record<string, unknown>)\n : record\n const merged: Record<string, unknown> = { ...target }\n for (const key of keys) merged[key] = toFieldValue(patch[key] as PreviewValue, target[key])\n return (target === record ? merged : { ...record, content: merged }) as T\n}\n\n/** The window event the overlay announces every `zap:values` patch with. */\nexport const VALUES_EVENT = 'eelzap:values'\n\n/** A values event's detail, checked enough to merge it safely. */\nfunction readValues(event: Event): ValuesPayload | null {\n const detail = (event as CustomEvent<unknown>).detail as Partial<ValuesPayload> | null\n if (!detail || typeof detail.recordRef !== 'string') return null\n const patch = detail.patch\n if (!patch || typeof patch !== 'object' || Array.isArray(patch)) return null\n return detail as ValuesPayload\n}\n\n/** `onValues` for references already resolved. */\nexport function subscribeRefs(\n refs: readonly RecordRef[],\n handler: (patch: Patch, payload: ValuesPayload) => void,\n win: Window | undefined = typeof window === 'undefined' ? undefined : window,\n): () => void {\n if (!win) return () => {}\n const listener = (event: Event) => {\n const payload = readValues(event)\n if (!payload || !refs.includes(payload.recordRef)) return\n try {\n handler(payload.patch, payload)\n } catch (error) {\n // A site handler that throws must not take the others down with it.\n console.error('eelzap: onValues handler failed', error)\n }\n }\n win.addEventListener(VALUES_EVENT, listener)\n return () => win.removeEventListener(VALUES_EVENT, listener)\n}\n\n/**\n * Call `handler` with each unsaved-values patch for `entry` while the page is\n * in Zap's preview, for sites that compute things from values (excerpts,\n * lists, counts). Framework-free despite living here. Returns an unsubscribe\n * function; outside a preview it never fires.\n */\nexport function onValues(\n entry: EntryLike,\n handler: (patch: Patch, payload: ValuesPayload) => void,\n): () => void {\n return subscribeRefs(recordRefsOf(entry), handler)\n}\n\nexport function useZapLiveUpdates<T extends EntryLike>(entry: T): T {\n // Every reference counts: the editor names the record by whichever one the\n // page tagged (the default slug, or a localized one).\n const refsKey = recordRefsOf(entry).join('|')\n const [state, setState] = React.useState<{ refsKey: string; patch: Patch }>({\n refsKey,\n patch: {},\n })\n\n React.useEffect(() => {\n if (!refsKey) return\n return subscribeRefs(refsKey.split('|'), (patch) => {\n setState((previous) => ({\n refsKey,\n patch: previous.refsKey === refsKey ? { ...previous.patch, ...patch } : { ...patch },\n }))\n })\n // `refsKey` stands for the entry's identity: new data for the same entry\n // keeps the unsaved values on top of it.\n }, [refsKey])\n\n const patch = state.refsKey === refsKey ? state.patch : null\n return React.useMemo(() => (patch ? mergeLiveValues(entry, patch) : entry), [entry, patch])\n}\n","/**\n * ISO 4217 minor units: how many digits `amountMinor` carries for a currency\n * (COP and USD 2, JPY 0, KWD 3, CLF 4); 2 when the code is unknown.\n *\n * A fixed table, never `Intl`: CLDR's display digits differ (it shows COP with\n * no decimals), while Zap stores COP, like USD, in hundredths. This package\n * ships on its own, so it carries a copy of the suite's one table\n * (`@eel/money`'s `CURRENCY_EXPONENTS`); Zap's\n * `currency-digits.contract.spec.ts` fails the moment the two disagree.\n *\n * No imports: the preview overlay, the React hook and Zap's editor bridge all\n * read it without pulling each other in.\n */\n// Space-padded code lists rather than Sets: this ships in `./react`, under a\n// byte budget (`scripts/bundle-config.mjs`).\nconst ZERO = ' BIF CLP DJF GNF ISK JPY KMF KRW PYG RWF UGX UYI VND VUV XAF XOF XPF '\nconst THREE = ' BHD IQD JOD KWD LYD OMR TND '\nconst FOUR = ' CLF UYW '\n\nexport function currencyDigits(code: string | undefined): number {\n const padded = ` ${(code ?? '').trim().toUpperCase()} `\n if (padded.length !== 5) return 2\n return ZERO.includes(padded) ? 0 : THREE.includes(padded) ? 3 : FOUR.includes(padded) ? 4 : 2\n}\n","/**\n * The record and field grammar of tags (`.docs/proposals/zap-cms-v2.md` §2.4),\n * in a module of its own so the light entries (`./fields`, `./react`, the\n * boot) can name records without pulling in the preview protocol.\n * No imports, no side effects.\n */\n\n/**\n * A record reference in its canonical string form: `collectionKey/slug` for an\n * entry, `doc:documentKey` for a document (the tag grammar, §2.4).\n */\nexport type RecordRef = string\n\n/** Zap's field key pattern. */\nexport const FIELD_KEY_RE = /^[a-z][a-z0-9_]{0,127}$/\n/** Entry and document references; keys and slugs are `[a-z0-9]+(?:-[a-z0-9]+)*`. */\nconst ENTRY_REF_RE = /^([a-z0-9]+(?:-[a-z0-9]+)*)\\/([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst DOC_REF_RE = /^doc:([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst MAX_REF_LENGTH = 300\n\nexport type ParsedRecordRef =\n | { kind: 'entry'; collection: string; slug: string }\n | { kind: 'document'; key: string }\n\nexport function parseRecordRef(value: unknown): ParsedRecordRef | null {\n if (typeof value !== 'string' || value.length > MAX_REF_LENGTH) return null\n const doc = DOC_REF_RE.exec(value)\n if (doc) return { kind: 'document', key: doc[1]! }\n const entry = ENTRY_REF_RE.exec(value)\n if (entry) return { kind: 'entry', collection: entry[1]!, slug: entry[2]! }\n return null\n}\n\nexport function isRecordRef(value: unknown): value is RecordRef {\n return parseRecordRef(value) !== null\n}\n\n/** `zpt_` + 32 bytes in base64url, as Zap mints it (zap-cms-v2 §3.5). */\nexport const PREVIEW_TOKEN_RE = /^zpt_[A-Za-z0-9_-]{43}$/\n\n/** A preview token in the shape Zap mints; says nothing about whether it is live. */\nexport function isPreviewTokenShape(value: unknown): value is string {\n return typeof value === 'string' && PREVIEW_TOKEN_RE.test(value)\n}\n\nexport function isFieldKey(value: unknown): value is string {\n return typeof value === 'string' && FIELD_KEY_RE.test(value)\n}\n\n/**\n * What `zapAttrs` and `onValues` accept: a record reference string\n * (`blog/hola`, `doc:home`) or a delivery API entry (an item carries\n * `collection.key` and `slug`, a document `key`).\n */\nexport type EntryLike =\n | RecordRef\n | {\n slug?: string | null\n key?: string | null\n collection?: { key?: string | null } | null\n localizedSlugs?: Record<string, string | null | undefined> | null\n }\n\n/** Every reference an entry answers to (default slug first, then localized ones). */\nexport function recordRefsOf(entry: EntryLike | null | undefined): RecordRef[] {\n if (!entry) return []\n if (typeof entry === 'string') return isRecordRef(entry) ? [entry] : []\n const refs: RecordRef[] = []\n const add = (ref: string) => {\n if (isRecordRef(ref) && !refs.includes(ref)) refs.push(ref)\n }\n const collection = entry.collection?.key\n if (collection) {\n if (entry.slug) add(`${collection}/${entry.slug}`)\n for (const slug of Object.values(entry.localizedSlugs ?? {})) {\n if (slug) add(`${collection}/${slug}`)\n }\n } else if (entry.key) {\n add(`doc:${entry.key}`)\n }\n return refs\n}\n\n/**\n * The tag for one field, as attributes to spread on the element that renders\n * it: `{ 'data-zap': 'blog/hola#title' }`. Empty when the entry or the field\n * key is not one Zap can name, so a bad call never breaks the page.\n */\nexport function zapAttrs(entry: EntryLike, fieldKey: string): { 'data-zap'?: string } {\n const [ref] = recordRefsOf(entry)\n if (!ref || !isFieldKey(fieldKey)) return {}\n return { 'data-zap': `${ref}#${fieldKey}` }\n}\n","/**\n * The keyboard shortcut that opens «Editar» and «Comentar» on the live site\n * (zap-cms-v2 §3.4, owner 2026-10-05): Shift plus one letter, `Z` by default.\n * It replaced «Shift twice», which fired while people typed capitals.\n *\n * Shared by the boot every visitor runs (`src/boot/boot.ts`, under 1 KB) and\n * the overlay core (`./boot.ts`), so the two can never disagree about what\n * counts. No directive, no imports.\n *\n * It never fires while the person is typing: focus in an input, a textarea, a\n * select or anything editable, or a key that is part of an IME composition\n * (`isComposing`, or the legacy `keyCode` 229 some browsers send instead).\n */\n\n/** The letter pressed with Shift when a site sets none. */\nexport const DEFAULT_SHORTCUT = 'Z'\n\n/**\n * Where typing happens: the target itself or anything it sits in. A\n * `contenteditable=\"false\"` region is not typing, so the shortcut still works\n * there; a field being edited on the live site is, so Shift+Z types a capital.\n */\nconst TYPING = 'input,textarea,select,[contenteditable]:not([contenteditable=\"false\"])'\n\nexport function isShortcut(event: KeyboardEvent, key: string): boolean {\n // The real target, inside an open shadow root too (a site's own components).\n const target = (event.composedPath?.()[0] ?? event.target) as Element | null\n return (\n event.shiftKey &&\n !(event.ctrlKey || event.metaKey || event.altKey || event.repeat || event.isComposing) &&\n // Some browsers report a key inside an IME composition only as keyCode 229.\n event.keyCode !== 229 &&\n event.key?.toLowerCase() === key.toLowerCase() &&\n !target?.closest?.(TYPING)\n )\n}\n","/**\n * Development origins: `localhost`, `127.0.0.1`, `[::1]` and any `*.localhost`\n * name (which browsers resolve to the machine itself and treat as secure\n * contexts), over http OR https, any port: a site in development runs on\n * either.\n *\n * Shared by the boot (`./boot.ts`, which every visitor runs, so one regex and\n * no more) and the overlay (`../preview/live.ts`), so the two never disagree\n * about what counts as local.\n */\nexport const LOCAL_ORIGIN =\n /^https?:\\/\\/(?:(?:[a-z\\d-]+\\.)*localhost|127\\.0\\.0\\.1|\\[::1\\])(?::\\d+)?$/i\n\n/** True for a development hostname (no scheme, no port). */\nexport const isLocalHost = (hostname: string): boolean => LOCAL_ORIGIN.test(`http://${hostname}`)\n","// GENERATED by packages/zap-sdk/scripts/release-cdn.mjs. Committed: the CDN\n// release of `./preview` the boot loads, compiled into the npm build and the\n// script-tag boot. The boot joins the path to the Zap origin it trusts (the\n// framing editor, the configured Zap, or production); the SRI hash pins the\n// bytes. apps/zap live-client.spec.ts checks it names a released file.\n\nexport const PREVIEW_RELEASE_PATH = '/js/preview/preview.v1.ae804b72342eda4a.js'\n\n// Lazy chunks pinned by the overlay; checked before npm publication.\nexport const PREVIEW_RELEASE_DEPENDENCIES = [\n '/js/preview/signin.v1.477e8b5df57ba15e.js',\n '/js/preview/suggest.v1.f9dacd1d2008749b.js',\n] as const\n\nexport const PREVIEW_RELEASE_INTEGRITY =\n 'sha384-glp6U0Mz3+ALlGM3ow3OFm1neCdGalPxjzbsUqiBEcpchMOWuX0en/P0+nChQUBK'\n","import { DEFAULT_SHORTCUT, isShortcut } from '../preview/site/shortcut'\nimport { LOCAL_ORIGIN } from './local'\nimport { PREVIEW_RELEASE_INTEGRITY, PREVIEW_RELEASE_PATH } from './release'\n\n/**\n * The boot (`.docs/proposals/zap-cms-v2.md` §2.7 \"What a visitor pays\",\n * §7.5): the only Zap code an ordinary visitor runs. Under 1 KB gzip, and it\n * makes NO request unless a preview session is present:\n *\n * 1. the server says a draft-mode session is active (`preview: true`);\n * 2. the page is framed by a Zap origin: framed, and, where the browser tells\n * (`location.ancestorOrigins`, Chrome and Safari), the parent is the\n * production editor or a local Zap seen from a local page (Zap's own\n * preview deployments are not editors) (`./local.ts`: localhost, 127.0.0.1,\n * [::1] or *.localhost, over http or https); where it cannot tell\n * (Firefox), any frame counts and the overlay itself refuses every editor\n * that is not Zap;\n * and with a site id (suggestion mode, §3.4):\n * 3. the URL carries `?zap`;\n * 4. this page is the OAuth callback of a sign-in this client started\n * (`/?code&state=zap1.…`);\n * 5. this tab already holds a suggestion session or a pending sign-in\n * (`sessionStorage`);\n * 6. the shortcut is pressed: Shift Z by default, never while the person is\n * typing in a field or composing with an IME (`preview/site/shortcut.ts`).\n *\n * Only then does it add one `<script>` for `./preview`: the release path\n * compiled in (`./release.ts`) with its SRI hash, on a Zap origin. The origin\n * is the editor that framed the page when the browser names it and it is a\n * Zap origin (so a local Zap serves its own overlay), else `zapOrigin` when it\n * is a Zap origin by the same rule (a local Zap from a local page), else\n * production. Never a URL from the page or the query string, and the SRI\n * hash means only the released bytes run, whichever Zap serves them.\n *\n * One script per document: a second boot on the same page (React StrictMode\n * runs every effect twice, a remount, a script tag beside the component) finds\n * the first one by its SRI hash and adds nothing. It reads the parent's\n * origin, the URL, sessionStorage keys and key presses; it writes nothing and\n * draws nothing.\n */\n\nexport interface ZapBootOptions {\n /** The Zap site's key. */\n siteKey: string\n /** The Zap site's id: turns on suggestion mode on the live site (§3.4). */\n siteId?: string\n /** The site's draft-mode route path, announced to the editor (§2.5). */\n draftRoute?: string | null\n /** The server saw an active draft-mode session (Next: `draftMode().isEnabled`). */\n preview?: boolean\n /** The letter pressed with Shift to open «Editar» and «Comentar»; default `Z`, false for none. */\n shortcut?: string | false\n /**\n * The Zap the site works against (`EELZAP_ORIGIN`): the overlay loads from\n * it and trusts it. Honoured for a local Zap from a local page\n * (development) and for production; anything else is ignored.\n */\n zapOrigin?: string\n /** A local Nest, under the same rule. */\n authOrigin?: string\n}\n\n/**\n * The string options and the `data-*` attribute each travels in to the\n * overlay's script (and, in the script-tag boot, comes from).\n */\nexport const BOOT_ATTRIBUTES = {\n siteKey: 'site',\n siteId: 'site-id',\n draftRoute: 'zap-draft-route',\n shortcut: 'zap-shortcut',\n zapOrigin: 'zap-origin',\n authOrigin: 'auth-origin',\n} as const\n\nconst ZAP = 'https://zap.eel.software'\n\n/** A Zap origin, as seen from `page`: see the list above. */\nfunction isZap(origin: string, page: string): boolean {\n return origin === ZAP || (LOCAL_ORIGIN.test(origin) && LOCAL_ORIGIN.test(page))\n}\n\n/**\n * The parent's origin: undefined when not framed, '' when framed but the\n * browser does not say by whom (Firefox).\n */\nfunction parentOrigin(win: Window): string | undefined {\n if (win.parent === win) return undefined\n const ancestors = (win.location as Location & { ancestorOrigins?: DOMStringList }).ancestorOrigins\n return ancestors?.[0] ?? ''\n}\n\nfunction suggesting(win: Window): boolean {\n const query = new URLSearchParams(win.location.search)\n if (query.has('zap')) return true\n if (\n win.location.pathname === '/' &&\n query.has('code') &&\n /^zap1\\./.test(query.get('state') ?? '')\n )\n return true\n try {\n const store = win.sessionStorage\n return !!(store.getItem('eelzap:site-token') || store.getItem('eelzap:signin'))\n } catch {\n return false\n }\n}\n\n/** Start the boot. Returns a function that removes its one listener. */\nexport function bootZapPreview(options: ZapBootOptions, win: Window = window): () => void {\n const page = win.location.origin\n const parent = parentOrigin(win)\n // The framing editor when it is Zap, else '' (not framed, unnamed, or not Zap).\n const editor = parent && isZap(parent, page) ? parent : ''\n // Where the overlay comes from (see above): that editor, the configured\n // Zap (an exact origin, as `EELZAP_ORIGIN` holds it), or production.\n const zap = editor || (isZap(options.zapOrigin ?? '', page) ? options.zapOrigin! : ZAP)\n let loaded = false\n const load = (open?: boolean) => {\n if (loaded || !PREVIEW_RELEASE_PATH) return\n loaded = true\n stop()\n const doc = win.document\n if (doc.querySelector(`script[integrity=\"${PREVIEW_RELEASE_INTEGRITY}\"]`)) return\n const script = doc.createElement('script')\n script.src = zap + PREVIEW_RELEASE_PATH\n script.setAttribute('integrity', PREVIEW_RELEASE_INTEGRITY)\n script.setAttribute('crossorigin', 'anonymous')\n let key: keyof typeof BOOT_ATTRIBUTES\n for (key in BOOT_ATTRIBUTES) {\n const value = key === 'shortcut' && options.shortcut === false ? 'off' : options[key]\n if (typeof value === 'string') script.setAttribute(`data-${BOOT_ATTRIBUTES[key]}`, value)\n }\n // The overlay trusts the Zap it came from (a local one only from a local page).\n script.setAttribute('data-zap-origin', zap)\n if (open) script.setAttribute('data-zap-open', '')\n doc.head.appendChild(script)\n }\n\n const key = options.shortcut === undefined ? DEFAULT_SHORTCUT : options.shortcut\n const onKey = (event: KeyboardEvent) => {\n if (key && isShortcut(event, key)) load(true)\n }\n const stop = () => win.removeEventListener('keydown', onKey, true)\n\n if (!options.siteKey) return stop\n if (options.preview || parent === '' || editor) load()\n else if (options.siteId) {\n if (suggesting(win)) load()\n else if (key) win.addEventListener('keydown', onKey, true)\n }\n return stop\n}\n","import { createElement, useEffect } from 'react'\n\nimport { CLIENT_VERSION } from '../version'\nimport { bootZapPreview, type ZapBootOptions } from './boot'\n\nexport type ZapPreviewProps = ZapBootOptions\n\n/**\n * The install marker's `name` (#1113). Zap's server fetches the site's page,\n * with no cookies and no script, and reads\n * `<meta name=\"zap-client\" content=\"<version>\" data-site=\"<siteKey>\">` to tell\n * site settings the client is installed, which version, and for which site.\n */\nexport const CLIENT_MARKER_NAME = 'zap-client'\n\n/**\n * The preview boot as a React component (`@8ux-co/eelzap/react` and, with a\n * default draft route, `@8ux-co/eelzap/next`). After mount it runs\n * `bootZapPreview`, which loads the overlay only in a preview session.\n *\n * Its only markup is the install marker: one `<meta>`, rendered on the server\n * too, which React 19 hoists into `<head>`. It carries no record tag and no\n * stega, so a published page stays untagged (ADR 041 amendment 2026-10-06).\n *\n * ```tsx\n * <ZapPreview siteKey=\"verdeorigen\" siteId=\"…uuid…\" />\n * ```\n */\nexport function ZapPreview(props: ZapPreviewProps) {\n const { siteKey, siteId, draftRoute, preview, shortcut, zapOrigin, authOrigin } = props\n useEffect(\n () => bootZapPreview({ siteKey, siteId, draftRoute, preview, shortcut, zapOrigin, authOrigin }),\n [siteKey, siteId, draftRoute, preview, shortcut, zapOrigin, authOrigin],\n )\n return createElement('meta', {\n name: CLIENT_MARKER_NAME,\n content: CLIENT_VERSION,\n 'data-site': siteKey,\n })\n}\n","/**\n * The client's version: what `zap:ready` announces to the editor and what the\n * install marker (`<meta name=\"zap-client\">`, `boot/component.ts`) carries for\n * Zap's server-side install check (#1113). Its own module so the boot can\n * name it without bundling the page runtime. Bumped with the package, together\n * with `preview/runtime.ts`'s copy (left in place so the pinned CDN release\n * still builds byte for byte); `version.spec.ts` fails when the two differ.\n */\nexport const CLIENT_VERSION = '0.11.1'\n"],"mappings":"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","names":["key","patch"]}
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
parseRecordRef,
|
|
14
14
|
reveal,
|
|
15
15
|
tagGuard
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-G2JAAZKS.js";
|
|
17
17
|
import {
|
|
18
18
|
SUGGEST_MESSAGES,
|
|
19
19
|
TOKENS,
|
|
@@ -504,6 +504,64 @@ async function leaveDraftSession(ctx, token, tagged) {
|
|
|
504
504
|
return true;
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
+
// src/currency-digits.ts
|
|
508
|
+
var ZERO = " BIF CLP DJF GNF ISK JPY KMF KRW PYG RWF UGX UYI VND VUV XAF XOF XPF ";
|
|
509
|
+
var THREE = " BHD IQD JOD KWD LYD OMR TND ";
|
|
510
|
+
var FOUR = " CLF UYW ";
|
|
511
|
+
function currencyDigits(code) {
|
|
512
|
+
const padded = ` ${(code ?? "").trim().toUpperCase()} `;
|
|
513
|
+
if (padded.length !== 5) return 2;
|
|
514
|
+
return ZERO.includes(padded) ? 0 : THREE.includes(padded) ? 3 : FOUR.includes(padded) ? 4 : 2;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/money-input.ts
|
|
518
|
+
var ZERO_DISPLAY = " COP CLP PYG JPY KRW VND ";
|
|
519
|
+
function moneyLocale(locale) {
|
|
520
|
+
return !locale || locale === "es" ? "es-CO" : locale;
|
|
521
|
+
}
|
|
522
|
+
function decimalMark(locale) {
|
|
523
|
+
const part = new Intl.NumberFormat(moneyLocale(locale)).formatToParts(1.5).find((p) => p.type === "decimal");
|
|
524
|
+
return part?.value === "," ? "," : ".";
|
|
525
|
+
}
|
|
526
|
+
function formatMoneyInput(amountMinor, currency, locale) {
|
|
527
|
+
const exponent = currencyDigits(currency);
|
|
528
|
+
const whole2 = ZERO_DISPLAY.includes(` ${currency.trim().toUpperCase()} `);
|
|
529
|
+
const digits = exponent === 0 || whole2 && amountMinor % 10 ** exponent === 0 ? 0 : exponent;
|
|
530
|
+
const major = exponent === 0 ? amountMinor : Number(`${amountMinor}e-${exponent}`);
|
|
531
|
+
return new Intl.NumberFormat(moneyLocale(locale), {
|
|
532
|
+
minimumFractionDigits: digits,
|
|
533
|
+
maximumFractionDigits: digits
|
|
534
|
+
}).format(major);
|
|
535
|
+
}
|
|
536
|
+
function parseMoneyInput(text, currency, locale) {
|
|
537
|
+
const trimmed = text.trim();
|
|
538
|
+
if (trimmed === "") return { kind: "empty" };
|
|
539
|
+
const negative = /^[^0-9]*[-−]/.test(trimmed);
|
|
540
|
+
const body = trimmed.replace(/[^0-9.,]/g, "");
|
|
541
|
+
if (!/[0-9]/.test(body)) return { kind: "invalid" };
|
|
542
|
+
const exponent = currencyDigits(currency);
|
|
543
|
+
const lastDot = body.lastIndexOf(".");
|
|
544
|
+
const lastComma = body.lastIndexOf(",");
|
|
545
|
+
let mark = null;
|
|
546
|
+
if (lastDot !== -1 && lastComma !== -1) {
|
|
547
|
+
mark = lastDot > lastComma ? "." : ",";
|
|
548
|
+
} else if (lastDot !== -1 || lastComma !== -1) {
|
|
549
|
+
const only = lastDot !== -1 ? "." : ",";
|
|
550
|
+
const once = body.split(only).length === 2;
|
|
551
|
+
const after = body.length - body.lastIndexOf(only) - 1;
|
|
552
|
+
const grouping = !once || after === 3 && (exponent < 3 || only !== decimalMark(locale));
|
|
553
|
+
mark = grouping ? null : only;
|
|
554
|
+
}
|
|
555
|
+
const at = mark ? body.lastIndexOf(mark) : body.length;
|
|
556
|
+
const integer = body.slice(0, at).replace(/[.,]/g, "");
|
|
557
|
+
const fraction = mark ? body.slice(at + 1) : "";
|
|
558
|
+
if (/[.,]/.test(fraction) || fraction.length > exponent) return { kind: "invalid" };
|
|
559
|
+
if (integer === "" && fraction === "") return { kind: "invalid" };
|
|
560
|
+
const magnitude = Number(`${integer || "0"}${fraction.padEnd(exponent, "0")}`);
|
|
561
|
+
if (!Number.isSafeInteger(magnitude)) return { kind: "invalid" };
|
|
562
|
+
return { kind: "amount", amountMinor: negative && magnitude !== 0 ? -magnitude : magnitude };
|
|
563
|
+
}
|
|
564
|
+
|
|
507
565
|
// src/preview/site/suggestion.ts
|
|
508
566
|
var SUGGESTIBLE_TYPES = ["TEXT", "LONG_TEXT", "NUMBER", "URL"];
|
|
509
567
|
var INPUT_TYPES = /* @__PURE__ */ new Set(["URL", "NUMBER", "INTEGER", "CURRENCY", "DATE", "DATETIME"]);
|
|
@@ -518,34 +576,8 @@ function editKindFor(tagged, field) {
|
|
|
518
576
|
if (field.type === "BOOLEAN") return "switch";
|
|
519
577
|
return null;
|
|
520
578
|
}
|
|
521
|
-
var ZERO_DIGITS = /* @__PURE__ */ new Set([
|
|
522
|
-
"BIF",
|
|
523
|
-
"CLP",
|
|
524
|
-
"DJF",
|
|
525
|
-
"GNF",
|
|
526
|
-
"ISK",
|
|
527
|
-
"JPY",
|
|
528
|
-
"KMF",
|
|
529
|
-
"KRW",
|
|
530
|
-
"PYG",
|
|
531
|
-
"RWF",
|
|
532
|
-
"UGX",
|
|
533
|
-
"UYI",
|
|
534
|
-
"VND",
|
|
535
|
-
"VUV",
|
|
536
|
-
"XAF",
|
|
537
|
-
"XOF",
|
|
538
|
-
"XPF"
|
|
539
|
-
]);
|
|
540
|
-
var THREE_DIGITS = /* @__PURE__ */ new Set(["BHD", "IQD", "JOD", "KWD", "LYD", "OMR", "TND"]);
|
|
541
|
-
function currencyDigits(code) {
|
|
542
|
-
const upper = (code ?? "").toUpperCase();
|
|
543
|
-
if (ZERO_DIGITS.has(upper)) return 0;
|
|
544
|
-
if (THREE_DIGITS.has(upper)) return 3;
|
|
545
|
-
return 2;
|
|
546
|
-
}
|
|
547
579
|
var pad = (n) => String(n).padStart(2, "0");
|
|
548
|
-
function inputSpec(field, value) {
|
|
580
|
+
function inputSpec(field, value, locale) {
|
|
549
581
|
const digits = currencyDigits(field.currency);
|
|
550
582
|
const limits = { min: field.min, max: field.max };
|
|
551
583
|
const number = typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
@@ -561,10 +593,9 @@ function inputSpec(field, value) {
|
|
|
561
593
|
return { type: "number", text: number === null ? "" : String(number), ...limits, step: 1 };
|
|
562
594
|
case "CURRENCY":
|
|
563
595
|
return {
|
|
564
|
-
type: "
|
|
565
|
-
text: number === null ? "" : (number
|
|
566
|
-
|
|
567
|
-
step: field.step ?? 10 ** -digits
|
|
596
|
+
type: "text",
|
|
597
|
+
text: number === null ? "" : formatMoneyInput(number, field.currency ?? "", locale),
|
|
598
|
+
inputMode: digits === 0 ? "numeric" : "decimal"
|
|
568
599
|
};
|
|
569
600
|
case "DATE":
|
|
570
601
|
return { type: "date", text: typeof value === "string" ? value.slice(0, 10) : "" };
|
|
@@ -579,18 +610,20 @@ function inputSpec(field, value) {
|
|
|
579
610
|
return { type: "text", text: typeof value === "string" ? value : "" };
|
|
580
611
|
}
|
|
581
612
|
}
|
|
582
|
-
function parseInput(field, raw) {
|
|
613
|
+
function parseInput(field, raw, locale) {
|
|
583
614
|
const text = cleanStega(raw).trim();
|
|
584
615
|
if (field.type === "URL") return { ok: true, value: normalizeText(text) };
|
|
585
616
|
if (text === "") return { ok: true, value: null };
|
|
617
|
+
if (field.type === "CURRENCY") {
|
|
618
|
+
const money = parseMoneyInput(text, field.currency ?? "", locale);
|
|
619
|
+
return money.kind === "amount" ? { ok: true, value: money.amountMinor } : { ok: false };
|
|
620
|
+
}
|
|
586
621
|
const number = Number(text.replace(",", "."));
|
|
587
622
|
switch (field.type) {
|
|
588
623
|
case "NUMBER":
|
|
589
624
|
return Number.isFinite(number) ? { ok: true, value: number } : { ok: false };
|
|
590
625
|
case "INTEGER":
|
|
591
626
|
return Number.isInteger(number) ? { ok: true, value: number } : { ok: false };
|
|
592
|
-
case "CURRENCY":
|
|
593
|
-
return Number.isFinite(number) ? { ok: true, value: Math.round(number * 10 ** currencyDigits(field.currency)) } : { ok: false };
|
|
594
627
|
case "DATE":
|
|
595
628
|
return /^\d{4}-\d{2}-\d{2}$/.test(text) && Number.isFinite(Date.parse(text)) ? { ok: true, value: text } : { ok: false };
|
|
596
629
|
case "DATETIME": {
|
|
@@ -2093,7 +2126,7 @@ function start(ctx, options = {}) {
|
|
|
2093
2126
|
}
|
|
2094
2127
|
function openInputCard(session) {
|
|
2095
2128
|
const { field } = session;
|
|
2096
|
-
const spec = field.type === "URL" ? { type: "url", text: session.before, min: void 0, max: void 0, step: void 0 } : inputSpec(field, session.raw);
|
|
2129
|
+
const spec = field.type === "URL" ? { type: "url", text: session.before, min: void 0, max: void 0, step: void 0 } : inputSpec(field, session.raw, ctx.locale);
|
|
2097
2130
|
const input = el(doc, "input", {
|
|
2098
2131
|
class: "value",
|
|
2099
2132
|
attrs: {
|
|
@@ -2101,7 +2134,8 @@ function start(ctx, options = {}) {
|
|
|
2101
2134
|
type: spec.type,
|
|
2102
2135
|
min: spec.min === void 0 ? null : String(spec.min),
|
|
2103
2136
|
max: spec.max === void 0 ? null : String(spec.max),
|
|
2104
|
-
step: spec.step === void 0 ? null : String(spec.step)
|
|
2137
|
+
step: spec.step === void 0 ? null : String(spec.step),
|
|
2138
|
+
inputmode: spec.inputMode ?? null
|
|
2105
2139
|
}
|
|
2106
2140
|
});
|
|
2107
2141
|
input.value = spec.text;
|
|
@@ -2335,7 +2369,7 @@ function start(ctx, options = {}) {
|
|
|
2335
2369
|
value = parsed.value;
|
|
2336
2370
|
} else if (kind === "input") {
|
|
2337
2371
|
if (session.input?.validity?.badInput) return invalid(session);
|
|
2338
|
-
const parsed = parseInput(field, typedValue(session));
|
|
2372
|
+
const parsed = parseInput(field, typedValue(session), ctx.locale);
|
|
2339
2373
|
if (!parsed.ok) return invalid(session);
|
|
2340
2374
|
value = parsed.value;
|
|
2341
2375
|
} else {
|
|
@@ -2612,4 +2646,4 @@ export {
|
|
|
2612
2646
|
start,
|
|
2613
2647
|
zapUrl
|
|
2614
2648
|
};
|
|
2615
|
-
//# sourceMappingURL=suggest-
|
|
2649
|
+
//# sourceMappingURL=suggest-FDD4MBRP.js.map
|