@alfakit/core 0.1.13 → 0.2.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 (42) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cms.d.ts +10 -1
  3. package/dist/cms.d.ts.map +1 -1
  4. package/dist/cms.js +30 -1
  5. package/dist/cms.js.map +1 -1
  6. package/dist/form.d.ts +10 -1
  7. package/dist/form.d.ts.map +1 -1
  8. package/dist/form.js +26 -4
  9. package/dist/form.js.map +1 -1
  10. package/dist/i18n.d.ts +9 -0
  11. package/dist/i18n.d.ts.map +1 -1
  12. package/dist/i18n.js +14 -1
  13. package/dist/i18n.js.map +1 -1
  14. package/dist/index.d.ts +7 -6
  15. package/dist/index.js +5 -4
  16. package/dist/kanban.d.ts.map +1 -1
  17. package/dist/kanban.js +25 -1
  18. package/dist/kanban.js.map +1 -1
  19. package/dist/prefetch.js +12 -1
  20. package/dist/prefetch.js.map +1 -1
  21. package/dist/realtime.d.ts +3 -3
  22. package/dist/realtime.js +3 -3
  23. package/dist/realtime.js.map +1 -1
  24. package/dist/schema/cms.d.ts +23 -0
  25. package/dist/schema/cms.d.ts.map +1 -1
  26. package/dist/schema/components.d.ts +2 -0
  27. package/dist/schema/components.d.ts.map +1 -1
  28. package/dist/schema/components.js.map +1 -1
  29. package/dist/schema/index.d.ts +3 -3
  30. package/dist/schema/panel.d.ts +14 -0
  31. package/dist/schema/panel.d.ts.map +1 -1
  32. package/dist/schema/panel.js.map +1 -1
  33. package/dist/starter-kit-document.d.ts +32 -0
  34. package/dist/starter-kit-document.d.ts.map +1 -0
  35. package/dist/starter-kit-document.js +57 -0
  36. package/dist/starter-kit-document.js.map +1 -0
  37. package/dist/utils/container-breakpoints.d.ts +9 -0
  38. package/dist/utils/container-breakpoints.d.ts.map +1 -1
  39. package/dist/utils/container-breakpoints.js +63 -1
  40. package/dist/utils/container-breakpoints.js.map +1 -1
  41. package/package.json +1 -1
  42. package/themes.css +92 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @alfakit/core
2
2
 
3
+ ## 0.2.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Starter-kit document direction (`alfakitDocument`, starter-kit-document helpers), `isolationDirection()` for numbers and money in RTL, container-based `contentGridClasses()`, and the reload of a page restored by Back is cancelled on the next Back or Forward.
8
+
9
+ ## 0.2.0
10
+
11
+ ### Minor Changes
12
+
13
+ - CMS site design and block style types and helpers (`cmsDesignCss`, `cmsDesignLinks`, `cmsBlockAttributes`); `FormStore.trackUpload()` so submits wait for uploads in progress; Kanban drops on a refused column return the card with a message; FileUpload accepted-type labels.
14
+
3
15
  ## 0.1.13
4
16
 
5
17
  ### Patch Changes
package/dist/cms.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Translator } from "./i18n.js";
2
- import { CmsBlockSchema, CmsPayload, CmsSeoSchema } from "./schema/cms.js";
2
+ import { CmsBlockSchema, CmsPayload, CmsSeoSchema, CmsSiteDesign } from "./schema/cms.js";
3
3
  //#region src/cms.d.ts
4
4
  /**
5
5
  * The framework-agnostic half of the CMS renderers (alfasix/alfakit-cms): registry keys,
@@ -39,6 +39,15 @@ export interface CmsLinkTag {
39
39
  href: string;
40
40
  hreflang?: string;
41
41
  }
42
+ /** The favicon and font stylesheet of the site design, for `<head>`. */
43
+ export declare function cmsDesignLinks(design: CmsSiteDesign | null | undefined): CmsLinkTag[];
44
+ /** The design's CSS for a `<style>` element; checked again so it can never close the element. */
45
+ export declare function cmsDesignCss(design: CmsSiteDesign | null | undefined): string | null;
46
+ /**
47
+ * The attributes of a block's wrapper (`data-slot="cms-block"`): its `id` (the anchor
48
+ * from **Style**, else the block id) and the `data-cms-*` hooks `themes.css` styles.
49
+ */
50
+ export declare function cmsBlockAttributes(block: CmsBlockSchema): Record<string, string>;
42
51
  export interface CmsHeadTags {
43
52
  title: string;
44
53
  meta: CmsMetaTag[];
package/dist/cms.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cms.d.ts","names":[],"sources":["../src/cms.ts"],"mappings":";;;;;;;;;qBAUa;;qBAGA;;qBAGA;;qBAGA;qBACA;;qBAGA;wBAeG,YAAY;;wBAKZ,UAAU,kBAAkB;;wBAsB5B,aAAa;;wBAWb,gBAAgB;wBAYhB,eAAe;;wBAYf,cAAc;;wBAKd,WAAW;iBAIV;EACf;EACA;EACA;;iBAGe;EACf;EACA;EACA;;iBAGe;EACf;EACA,MAAM;EACN,OAAO;;EAEP;;;wBAIc,YAAY,KAAK,kCAAkC;;wBAsCnD,oBACd,SAAS,KAAK,0DACb;;wBAKa,WAAW,iBAAiB"}
1
+ {"version":3,"file":"cms.d.ts","names":[],"sources":["../src/cms.ts"],"mappings":";;;;;;;;;qBAgBa;;qBAGA;;qBAGA;;qBAGA;qBACA;;qBAGA;wBAeG,YAAY;;wBAKZ,UAAU,kBAAkB;;wBAsB5B,aAAa;;wBAWb,gBAAgB;wBAYhB,eAAe;;wBAYf,cAAc;;wBAKd,WAAW;iBAIV;EACf;EACA;EACA;;iBAGe;EACf;EACA;EACA;;;wBAIc,eAAe,QAAQ,mCAAmC;;wBAU1D,aAAa,QAAQ;;;;;wBAUrB,mBAAmB,OAAO,iBAAiB;iBAU1C;EACf;EACA,MAAM;EACN,OAAO;;EAEP;;;wBAIc,YAAY,KAAK,kCAAkC;;wBAsCnD,oBACd,SAAS,KAAK,0DACb;;wBAKa,WAAW,iBAAiB"}
package/dist/cms.js CHANGED
@@ -80,6 +80,35 @@ function cmsWidthClass(width) {
80
80
  function cmsLinkRel(newTab) {
81
81
  return newTab ? "noopener noreferrer" : void 0;
82
82
  }
83
+ /** The favicon and font stylesheet of the site design, for `<head>`. */
84
+ function cmsDesignLinks(design) {
85
+ const links = [];
86
+ if (design?.favicon) links.push({
87
+ rel: "icon",
88
+ href: design.favicon
89
+ });
90
+ if (design?.fonts) links.push({
91
+ rel: "stylesheet",
92
+ href: design.fonts
93
+ });
94
+ return links;
95
+ }
96
+ /** The design's CSS for a `<style>` element; checked again so it can never close the element. */
97
+ function cmsDesignCss(design) {
98
+ const css = design?.css;
99
+ return typeof css === "string" && css !== "" && !css.includes("<") ? css : null;
100
+ }
101
+ /**
102
+ * The attributes of a block's wrapper (`data-slot="cms-block"`): its `id` (the anchor
103
+ * from **Style**, else the block id) and the `data-cms-*` hooks `themes.css` styles.
104
+ */
105
+ function cmsBlockAttributes(block) {
106
+ const attributes = { id: block.style?.anchor ?? block.id };
107
+ if (block.style?.background) attributes["data-cms-background"] = block.style.background;
108
+ if (block.style?.spacing) attributes["data-cms-spacing"] = block.style.spacing;
109
+ if (block.style?.visibility) attributes["data-cms-visibility"] = block.style.visibility;
110
+ return attributes;
111
+ }
83
112
  /** The `<head>` tags of a page, in the order the PHP `Seo::toHtml()` prints them. */
84
113
  function cmsHeadTags(seo) {
85
114
  if (!seo) return {
@@ -133,6 +162,6 @@ function cmsPayload(props) {
133
162
  return typeof cms === "object" && cms !== null && "page" in cms ? cms : null;
134
163
  }
135
164
  //#endregion
136
- export { CMS_BLOCK_PREFIX, CMS_BUILT_IN_BLOCKS, CMS_FOOTER_KEY, CMS_FORM_KEY, CMS_HEADER_KEY, CMS_PAGE_COMPONENT, cmsAspectClass, cmsBlockKey, cmsBlocks, cmsColumnsClass, cmsGridClass, cmsHeadTags, cmsLinkRel, cmsPayload, cmsWidthClass, createCmsTranslator };
165
+ export { CMS_BLOCK_PREFIX, CMS_BUILT_IN_BLOCKS, CMS_FOOTER_KEY, CMS_FORM_KEY, CMS_HEADER_KEY, CMS_PAGE_COMPONENT, cmsAspectClass, cmsBlockAttributes, cmsBlockKey, cmsBlocks, cmsColumnsClass, cmsDesignCss, cmsDesignLinks, cmsGridClass, cmsHeadTags, cmsLinkRel, cmsPayload, cmsWidthClass, createCmsTranslator };
137
166
 
138
167
  //# sourceMappingURL=cms.js.map
package/dist/cms.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cms.js","names":[],"sources":["../src/cms.ts"],"sourcesContent":["import { Translator } from './i18n'\nimport type { CmsAspectRatio, CmsBlockSchema, CmsPayload, CmsSeoSchema } from './schema/cms'\n\n/**\n * The framework-agnostic half of the CMS renderers (alfasix/alfakit-cms): registry keys,\n * the Tailwind classes block layouts pick from (spelled out so Tailwind generates them),\n * the head tags of a page and the translator of its strings.\n */\n\n/** The Inertia page component public CMS pages render (`CmsPlugin::inertiaComponent()`). */\nexport const CMS_PAGE_COMPONENT = 'Alfakit/CmsPage'\n\n/** Registry keys: `cms:block:{type}` replaces or adds a block renderer. */\nexport const CMS_BLOCK_PREFIX = 'cms:block:'\n\n/** Renders the form block's payload (`CmsPlugin::formUsing()`); nothing is shown without one. */\nexport const CMS_FORM_KEY = 'cms:form'\n\n/** Replace the default page header or footer of `CmsPage`. */\nexport const CMS_HEADER_KEY = 'cms:header'\nexport const CMS_FOOTER_KEY = 'cms:footer'\n\n/** The built-in block types every renderer draws. */\nexport const CMS_BUILT_IN_BLOCKS = [\n 'hero',\n 'rich-text',\n 'image',\n 'gallery',\n 'call-to-action',\n 'features',\n 'faq',\n 'testimonials',\n 'embed',\n 'columns',\n 'form',\n 'section',\n] as const\n\nexport function cmsBlockKey(type: string): string {\n return `${CMS_BLOCK_PREFIX}${type}`\n}\n\n/** Blocks that look like rendered blocks (`{ type, id, data }`), others dropped. */\nexport function cmsBlocks(blocks: unknown): CmsBlockSchema[] {\n if (!Array.isArray(blocks)) return []\n\n return blocks.filter(\n (block): block is CmsBlockSchema =>\n typeof block === 'object' &&\n block !== null &&\n typeof (block as CmsBlockSchema).type === 'string' &&\n typeof (block as CmsBlockSchema).id === 'string' &&\n typeof (block as CmsBlockSchema).data === 'object' &&\n (block as CmsBlockSchema).data !== null,\n )\n}\n\nconst GRID_COLUMNS: Record<number, string> = {\n 1: 'grid grid-cols-1 gap-6',\n 2: 'grid grid-cols-1 gap-6 sm:grid-cols-2',\n 3: 'grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3',\n 4: 'grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4',\n}\n\n/** A responsive grid of 1 to 4 columns (one column on phones). */\nexport function cmsGridClass(columns: number): string {\n return GRID_COLUMNS[Math.min(4, Math.max(1, Math.round(columns)))] ?? GRID_COLUMNS[3]!\n}\n\nconst COLUMNS_LAYOUT: Record<number, string> = {\n 2: 'grid grid-cols-1 gap-8 md:grid-cols-2',\n 3: 'grid grid-cols-1 gap-8 md:grid-cols-3',\n 4: 'grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-4',\n}\n\n/** The columns block layout (stacked below `md`). */\nexport function cmsColumnsClass(count: number): string {\n return COLUMNS_LAYOUT[Math.min(4, Math.max(2, Math.round(count)))] ?? COLUMNS_LAYOUT[2]!\n}\n\nconst ASPECT: Record<CmsAspectRatio, string> = {\n '16/9': 'aspect-video',\n '4/3': 'aspect-[4/3]',\n '1/1': 'aspect-square',\n '21/9': 'aspect-[21/9]',\n '9/16': 'aspect-[9/16] max-w-sm mx-auto',\n}\n\nexport function cmsAspectClass(ratio: string | null | undefined): string {\n return ASPECT[ratio as CmsAspectRatio] ?? ASPECT['16/9']\n}\n\nconst WIDTH: Record<string, string> = {\n prose: 'mx-auto max-w-3xl',\n contained: 'mx-auto max-w-4xl',\n wide: 'mx-auto max-w-6xl',\n full: 'w-full',\n}\n\n/** The content width of a block (`prose`, `contained`, `wide`, `full`). */\nexport function cmsWidthClass(width: string | null | undefined): string {\n return WIDTH[width ?? 'prose'] ?? WIDTH.prose!\n}\n\n/** The `rel` of a link: new-tab links never get a handle on this window. */\nexport function cmsLinkRel(newTab: boolean): string | undefined {\n return newTab ? 'noopener noreferrer' : undefined\n}\n\nexport interface CmsMetaTag {\n name?: string\n property?: string\n content: string\n}\n\nexport interface CmsLinkTag {\n rel: string\n href: string\n hreflang?: string\n}\n\nexport interface CmsHeadTags {\n title: string\n meta: CmsMetaTag[]\n links: CmsLinkTag[]\n /** Pre-encoded JSON-LD (no `<` in it), for a `<script type=\"application/ld+json\">`. */\n jsonLd: string | null\n}\n\n/** The `<head>` tags of a page, in the order the PHP `Seo::toHtml()` prints them. */\nexport function cmsHeadTags(seo: CmsSeoSchema | null | undefined): CmsHeadTags {\n if (!seo) return { title: '', meta: [], links: [], jsonLd: null }\n\n const meta: CmsMetaTag[] = []\n\n if (seo.description) meta.push({ name: 'description', content: seo.description })\n if (seo.robots) meta.push({ name: 'robots', content: seo.robots })\n\n for (const [property, content] of Object.entries(seo.openGraph ?? {})) {\n if (typeof content === 'string' && content !== '') {\n meta.push({\n property: `og:${property.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`)}`,\n content,\n })\n }\n }\n\n for (const [name, content] of Object.entries(seo.twitter ?? {})) {\n if (typeof content === 'string' && content !== '') {\n meta.push({ name: `twitter:${name}`, content })\n }\n }\n\n const links: CmsLinkTag[] = []\n\n if (seo.canonical) links.push({ rel: 'canonical', href: seo.canonical })\n\n for (const alternate of seo.alternates ?? []) {\n links.push({ rel: 'alternate', href: alternate.href, hreflang: alternate.hreflang })\n }\n\n // Encoded by the server; checked again so a `<` can never end the script element.\n const jsonLd = typeof seo.jsonLd === 'string' && !seo.jsonLd.includes('<') ? seo.jsonLd : null\n\n return { title: seo.title, meta, links, jsonLd }\n}\n\n/** A translator over the page's `cms.*` strings, for pages rendered outside panels. */\nexport function createCmsTranslator(\n payload: Pick<CmsPayload, 'translations' | 'site'> | null | undefined,\n): Translator {\n return new Translator(payload?.translations ?? {}, payload?.site?.tag ?? 'en')\n}\n\n/** The `cms` prop, when the Inertia page has one. */\nexport function cmsPayload(props: unknown): CmsPayload | null {\n const cms = (props as { cms?: unknown } | null | undefined)?.cms\n\n return typeof cms === 'object' && cms !== null && 'page' in cms ? (cms as CmsPayload) : null\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cms.js","names":[],"sources":["../src/cms.ts"],"sourcesContent":["import { Translator } from './i18n'\nimport type {\n CmsAspectRatio,\n CmsBlockSchema,\n CmsPayload,\n CmsSeoSchema,\n CmsSiteDesign,\n} from './schema/cms'\n\n/**\n * The framework-agnostic half of the CMS renderers (alfasix/alfakit-cms): registry keys,\n * the Tailwind classes block layouts pick from (spelled out so Tailwind generates them),\n * the head tags of a page and the translator of its strings.\n */\n\n/** The Inertia page component public CMS pages render (`CmsPlugin::inertiaComponent()`). */\nexport const CMS_PAGE_COMPONENT = 'Alfakit/CmsPage'\n\n/** Registry keys: `cms:block:{type}` replaces or adds a block renderer. */\nexport const CMS_BLOCK_PREFIX = 'cms:block:'\n\n/** Renders the form block's payload (`CmsPlugin::formUsing()`); nothing is shown without one. */\nexport const CMS_FORM_KEY = 'cms:form'\n\n/** Replace the default page header or footer of `CmsPage`. */\nexport const CMS_HEADER_KEY = 'cms:header'\nexport const CMS_FOOTER_KEY = 'cms:footer'\n\n/** The built-in block types every renderer draws. */\nexport const CMS_BUILT_IN_BLOCKS = [\n 'hero',\n 'rich-text',\n 'image',\n 'gallery',\n 'call-to-action',\n 'features',\n 'faq',\n 'testimonials',\n 'embed',\n 'columns',\n 'form',\n 'section',\n] as const\n\nexport function cmsBlockKey(type: string): string {\n return `${CMS_BLOCK_PREFIX}${type}`\n}\n\n/** Blocks that look like rendered blocks (`{ type, id, data }`), others dropped. */\nexport function cmsBlocks(blocks: unknown): CmsBlockSchema[] {\n if (!Array.isArray(blocks)) return []\n\n return blocks.filter(\n (block): block is CmsBlockSchema =>\n typeof block === 'object' &&\n block !== null &&\n typeof (block as CmsBlockSchema).type === 'string' &&\n typeof (block as CmsBlockSchema).id === 'string' &&\n typeof (block as CmsBlockSchema).data === 'object' &&\n (block as CmsBlockSchema).data !== null,\n )\n}\n\nconst GRID_COLUMNS: Record<number, string> = {\n 1: 'grid grid-cols-1 gap-6',\n 2: 'grid grid-cols-1 gap-6 sm:grid-cols-2',\n 3: 'grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3',\n 4: 'grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4',\n}\n\n/** A responsive grid of 1 to 4 columns (one column on phones). */\nexport function cmsGridClass(columns: number): string {\n return GRID_COLUMNS[Math.min(4, Math.max(1, Math.round(columns)))] ?? GRID_COLUMNS[3]!\n}\n\nconst COLUMNS_LAYOUT: Record<number, string> = {\n 2: 'grid grid-cols-1 gap-8 md:grid-cols-2',\n 3: 'grid grid-cols-1 gap-8 md:grid-cols-3',\n 4: 'grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-4',\n}\n\n/** The columns block layout (stacked below `md`). */\nexport function cmsColumnsClass(count: number): string {\n return COLUMNS_LAYOUT[Math.min(4, Math.max(2, Math.round(count)))] ?? COLUMNS_LAYOUT[2]!\n}\n\nconst ASPECT: Record<CmsAspectRatio, string> = {\n '16/9': 'aspect-video',\n '4/3': 'aspect-[4/3]',\n '1/1': 'aspect-square',\n '21/9': 'aspect-[21/9]',\n '9/16': 'aspect-[9/16] max-w-sm mx-auto',\n}\n\nexport function cmsAspectClass(ratio: string | null | undefined): string {\n return ASPECT[ratio as CmsAspectRatio] ?? ASPECT['16/9']\n}\n\nconst WIDTH: Record<string, string> = {\n prose: 'mx-auto max-w-3xl',\n contained: 'mx-auto max-w-4xl',\n wide: 'mx-auto max-w-6xl',\n full: 'w-full',\n}\n\n/** The content width of a block (`prose`, `contained`, `wide`, `full`). */\nexport function cmsWidthClass(width: string | null | undefined): string {\n return WIDTH[width ?? 'prose'] ?? WIDTH.prose!\n}\n\n/** The `rel` of a link: new-tab links never get a handle on this window. */\nexport function cmsLinkRel(newTab: boolean): string | undefined {\n return newTab ? 'noopener noreferrer' : undefined\n}\n\nexport interface CmsMetaTag {\n name?: string\n property?: string\n content: string\n}\n\nexport interface CmsLinkTag {\n rel: string\n href: string\n hreflang?: string\n}\n\n/** The favicon and font stylesheet of the site design, for `<head>`. */\nexport function cmsDesignLinks(design: CmsSiteDesign | null | undefined): CmsLinkTag[] {\n const links: CmsLinkTag[] = []\n\n if (design?.favicon) links.push({ rel: 'icon', href: design.favicon })\n if (design?.fonts) links.push({ rel: 'stylesheet', href: design.fonts })\n\n return links\n}\n\n/** The design's CSS for a `<style>` element; checked again so it can never close the element. */\nexport function cmsDesignCss(design: CmsSiteDesign | null | undefined): string | null {\n const css = design?.css\n\n return typeof css === 'string' && css !== '' && !css.includes('<') ? css : null\n}\n\n/**\n * The attributes of a block's wrapper (`data-slot=\"cms-block\"`): its `id` (the anchor\n * from **Style**, else the block id) and the `data-cms-*` hooks `themes.css` styles.\n */\nexport function cmsBlockAttributes(block: CmsBlockSchema): Record<string, string> {\n const attributes: Record<string, string> = { id: block.style?.anchor ?? block.id }\n\n if (block.style?.background) attributes['data-cms-background'] = block.style.background\n if (block.style?.spacing) attributes['data-cms-spacing'] = block.style.spacing\n if (block.style?.visibility) attributes['data-cms-visibility'] = block.style.visibility\n\n return attributes\n}\n\nexport interface CmsHeadTags {\n title: string\n meta: CmsMetaTag[]\n links: CmsLinkTag[]\n /** Pre-encoded JSON-LD (no `<` in it), for a `<script type=\"application/ld+json\">`. */\n jsonLd: string | null\n}\n\n/** The `<head>` tags of a page, in the order the PHP `Seo::toHtml()` prints them. */\nexport function cmsHeadTags(seo: CmsSeoSchema | null | undefined): CmsHeadTags {\n if (!seo) return { title: '', meta: [], links: [], jsonLd: null }\n\n const meta: CmsMetaTag[] = []\n\n if (seo.description) meta.push({ name: 'description', content: seo.description })\n if (seo.robots) meta.push({ name: 'robots', content: seo.robots })\n\n for (const [property, content] of Object.entries(seo.openGraph ?? {})) {\n if (typeof content === 'string' && content !== '') {\n meta.push({\n property: `og:${property.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`)}`,\n content,\n })\n }\n }\n\n for (const [name, content] of Object.entries(seo.twitter ?? {})) {\n if (typeof content === 'string' && content !== '') {\n meta.push({ name: `twitter:${name}`, content })\n }\n }\n\n const links: CmsLinkTag[] = []\n\n if (seo.canonical) links.push({ rel: 'canonical', href: seo.canonical })\n\n for (const alternate of seo.alternates ?? []) {\n links.push({ rel: 'alternate', href: alternate.href, hreflang: alternate.hreflang })\n }\n\n // Encoded by the server; checked again so a `<` can never end the script element.\n const jsonLd = typeof seo.jsonLd === 'string' && !seo.jsonLd.includes('<') ? seo.jsonLd : null\n\n return { title: seo.title, meta, links, jsonLd }\n}\n\n/** A translator over the page's `cms.*` strings, for pages rendered outside panels. */\nexport function createCmsTranslator(\n payload: Pick<CmsPayload, 'translations' | 'site'> | null | undefined,\n): Translator {\n return new Translator(payload?.translations ?? {}, payload?.site?.tag ?? 'en')\n}\n\n/** The `cms` prop, when the Inertia page has one. */\nexport function cmsPayload(props: unknown): CmsPayload | null {\n const cms = (props as { cms?: unknown } | null | undefined)?.cms\n\n return typeof cms === 'object' && cms !== null && 'page' in cms ? (cms as CmsPayload) : null\n}\n"],"mappings":"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"}
package/dist/form.d.ts CHANGED
@@ -182,9 +182,18 @@ export declare class FormStore {
182
182
  * sends the state the server computed (`$set` in `afterStateUpdated`), not the one before.
183
183
  */
184
184
  settle(): Promise<void>;
185
+ /**
186
+ * Register an upload of one of the form's fields (FileUpload, RichEditor attachments)
187
+ * while it runs: `settle()` and `whenSettled()`, and so every submit, wait for it.
188
+ * Returns the same promise; a failed upload stops counting as pending.
189
+ */
190
+ trackUpload<T>(upload: Promise<T>): Promise<T>;
191
+ /** Whether an upload registered with `trackUpload()` is still running. */
192
+ get hasPendingUploads(): boolean;
193
+ private readonly uploadsInFlight;
185
194
  /**
186
195
  * Run `callback` once the form is settled (see `settle()`): straight away when no live
187
- * refresh is waiting or in flight, so a submit stays synchronous in the usual case.
196
+ * refresh or upload is waiting or in flight, so a submit stays synchronous in the usual case.
188
197
  */
189
198
  whenSettled(callback: () => void): void;
190
199
  private refreshInFlight;
@@ -1 +1 @@
1
- {"version":3,"file":"form.d.ts","names":[],"sources":["../src/form.ts"],"mappings":";;;;;;;;iBAsBiB;EACf,YAAY;EACZ,QAAQ;EACR,QAAQ;;EAER,OAAO;;EAEP;EACA;EACA,SAAS;;iBAGM;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;EAKA,eAAe;EACf,WAAW;;EAEX,mBAAmB,eAAe;;EAElC,eAAe;;;;;EAKf;;;wBAIc,aAAa;;wBAKb,WAAW,gBAAgB;;;;wBAO3B,cACd,YAAY,mBACZ,OAAM,gBACL;;;;;;;;wBA2Ba,gBACd,YAAY,mBACZ,cACA,SAAQ,0BACP;;;;;;wBAmEa,mBACd,OAAO,mBACP,gBAAgB,yBAChB,QAAQ;;;;;;wBAkBM,mBACd,WAAW,KAAK,2DAChB,QAAQ;;;;;wBAkBM,cAAc,KAAK,KAAK;;;;;wBAQxB,aACd,MAAM,MAAM,KAAK,mCACjB;;;;;wBAcc,YACd,MAAM,MAAM,KAAK,oEACjB,mBACA,gBAAgB,yBAChB,QAAQ;;;;;;;wBA4BM,WACd,QAAQ,SAAS,gDACjB;;;;;qBAiBW;UAyBD;WAxBM,OAAO,MAAM;mBAEZ;UAET;;;;;UAMA;;;;;;UAOA;;UAGA;EAGN,YAAA,SAAS,aACD,SAAS;MAkBf,SAAS;;;;;EAQb,SAAS;;MAKL;;MAKA;MAIA;MAIA;MAIA;MAIA;;MAKA,uBAAuB;;EAK3B,kBAAkB;;;;;;EAiBlB,gBAAgB;EAahB,UAAU,WAAW,OAAO;EAM5B,SAAS,aAAa,cAAc,WAAW,IAAI;;;;;EAQnD,SAAS,cAAc,gBAAgB;IAAW;;;EAoClD,KAAK;;;;;;UAaG;;EAmBR,KAAK,SAAS,aAAa;IAAW;;EAYtC,UAAU,QAAQ;EAUlB;EAIA,SAAS;EAIT,cAAc;MAIV;EAIJ,UAAU,eAAe;UAMjB;;;;;EAiBR,QAAQ,gBAAgB;;;;;;EAgBlB,UAAU;;;;;EAYhB,YAAY;UAYJ;UAEM;;;;;;;EAoEd,cACE,UAAU,uBACV,MAAM,yBACN,QAAO;;;;;;EAqCH,aAAa,iBAAiB,uBAAuB;;EA8CrD,cACJ,cACA,iBACC,QAAQ;IAAQ;IAAe;;EAe5B,aACJ,cACA,QAAQ,yBACP,QAAQ;;EAgBL,YACJ,cACA;IAAS;IAAiB;IAAqB;KAC/C,SAAS,cACR,QAAQ;;;;;EAqBL,WACJ,cACA,oBACA,OAAO,YACP,SAAS,cACR,QAAQ;EAgBX;;;;;;;;wBAkCc,aACd;EAAQ;EAAgB;sBACxB;EAAS;EAAa;EAAmB;;;iBAY1B;EACf;EACA,KAAK;EACL;EACA;EACA;;;;;;;;wBASc,eAAe,OAAO,cAAc;iBAuBnC;EACf;EACA,OAAO;;;wBAIO,mBACd,kBACA,eAAe;wBAKD,mBAAmB,kBAAkB;wBAIrC,iBAAiB,kBAAkB,cAAc;wBAYjD,kBAAkB,kBAAkB;;;;;YAWxC,aAAa,KAAK,eAAe;wBAiB7B,iBAAiB,GAC/B,OAAO,aAAa,IACpB,cACA,aACC,aAAa;wBAYA,mBAAmB,GAAG,OAAO,aAAa,IAAI,kBAAkB,aAAa;wBAM7E,kBAAkB,GAAG,OAAO,aAAa,IAAI,iBAAiB,aAAa"}
1
+ {"version":3,"file":"form.d.ts","names":[],"sources":["../src/form.ts"],"mappings":";;;;;;;;iBAsBiB;EACf,YAAY;EACZ,QAAQ;EACR,QAAQ;;EAER,OAAO;;EAEP;EACA;EACA,SAAS;;iBAGM;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;EAKA,eAAe;EACf,WAAW;;EAEX,mBAAmB,eAAe;;EAElC,eAAe;;;;;EAKf;;;wBAIc,aAAa;;wBAKb,WAAW,gBAAgB;;;;wBAO3B,cACd,YAAY,mBACZ,OAAM,gBACL;;;;;;;;wBA2Ba,gBACd,YAAY,mBACZ,cACA,SAAQ,0BACP;;;;;;wBAmEa,mBACd,OAAO,mBACP,gBAAgB,yBAChB,QAAQ;;;;;;wBAkBM,mBACd,WAAW,KAAK,2DAChB,QAAQ;;;;;wBAkBM,cAAc,KAAK,KAAK;;;;;wBAQxB,aACd,MAAM,MAAM,KAAK,mCACjB;;;;;wBAcc,YACd,MAAM,MAAM,KAAK,oEACjB,mBACA,gBAAgB,yBAChB,QAAQ;;;;;;;wBA4BM,WACd,QAAQ,SAAS,gDACjB;;;;;qBAiBW;UAyBD;WAxBM,OAAO,MAAM;mBAEZ;UAET;;;;;UAMA;;;;;;UAOA;;UAGA;EAGN,YAAA,SAAS,aACD,SAAS;MAkBf,SAAS;;;;;EAQb,SAAS;;MAKL;;MAKA;MAIA;MAIA;MAIA;MAIA;;MAKA,uBAAuB;;EAK3B,kBAAkB;;;;;;EAiBlB,gBAAgB;EAahB,UAAU,WAAW,OAAO;EAM5B,SAAS,aAAa,cAAc,WAAW,IAAI;;;;;EAQnD,SAAS,cAAc,gBAAgB;IAAW;;;EAoClD,KAAK;;;;;;UAaG;;EAmBR,KAAK,SAAS,aAAa;IAAW;;EAYtC,UAAU,QAAQ;EAUlB;EAIA,SAAS;EAIT,cAAc;MAIV;EAIJ,UAAU,eAAe;UAMjB;;;;;EAiBR,QAAQ,gBAAgB;;;;;;EAgBlB,UAAU;;;;;;EAsBhB,YAAY,GAAG,QAAQ,QAAQ,KAAK,QAAQ;;MAaxC;mBAIa;;;;;EAMjB,YAAY;UAYJ;UAEM;;;;;;;EAoEd,cACE,UAAU,uBACV,MAAM,yBACN,QAAO;;;;;;EAqCH,aAAa,iBAAiB,uBAAuB;;EA8CrD,cACJ,cACA,iBACC,QAAQ;IAAQ;IAAe;;EAe5B,aACJ,cACA,QAAQ,yBACP,QAAQ;;EAgBL,YACJ,cACA;IAAS;IAAiB;IAAqB;KAC/C,SAAS,cACR,QAAQ;;;;;EAqBL,WACJ,cACA,oBACA,OAAO,YACP,SAAS,cACR,QAAQ;EAgBX;;;;;;;;wBAkCc,aACd;EAAQ;EAAgB;sBACxB;EAAS;EAAa;EAAmB;;;iBAY1B;EACf;EACA,KAAK;EACL;EACA;EACA;;;;;;;;wBASc,eAAe,OAAO,cAAc;iBAuBnC;EACf;EACA,OAAO;;;wBAIO,mBACd,kBACA,eAAe;wBAKD,mBAAmB,kBAAkB;wBAIrC,iBAAiB,kBAAkB,cAAc;wBAYjD,kBAAkB,kBAAkB;;;;;YAWxC,aAAa,KAAK,eAAe;wBAiB7B,iBAAiB,GAC/B,OAAO,aAAa,IACpB,cACA,aACC,aAAa;wBAYA,mBAAmB,GAAG,OAAO,aAAa,IAAI,kBAAkB,aAAa;wBAM7E,kBAAkB,GAAG,OAAO,aAAa,IAAI,iBAAiB,aAAa"}
package/dist/form.js CHANGED
@@ -337,15 +337,37 @@ var FormStore = class {
337
337
  * sends the state the server computed (`$set` in `afterStateUpdated`), not the one before.
338
338
  */
339
339
  async settle() {
340
- for (const debounced of this.liveDebouncers.values()) debounced.flush();
341
- while (this.refreshInFlight) await this.refreshInFlight.catch(() => void 0);
340
+ for (;;) {
341
+ for (const debounced of this.liveDebouncers.values()) debounced.flush();
342
+ if (this.refreshInFlight) await this.refreshInFlight.catch(() => void 0);
343
+ else if (this.uploadsInFlight.size > 0) await Promise.allSettled([...this.uploadsInFlight]);
344
+ else return;
345
+ }
346
+ }
347
+ /**
348
+ * Register an upload of one of the form's fields (FileUpload, RichEditor attachments)
349
+ * while it runs: `settle()` and `whenSettled()`, and so every submit, wait for it.
350
+ * Returns the same promise; a failed upload stops counting as pending.
351
+ */
352
+ trackUpload(upload) {
353
+ this.uploadsInFlight.add(upload);
354
+ const forget = () => {
355
+ this.uploadsInFlight.delete(upload);
356
+ };
357
+ upload.then(forget, forget);
358
+ return upload;
359
+ }
360
+ /** Whether an upload registered with `trackUpload()` is still running. */
361
+ get hasPendingUploads() {
362
+ return this.uploadsInFlight.size > 0;
342
363
  }
364
+ uploadsInFlight = /* @__PURE__ */ new Set();
343
365
  /**
344
366
  * Run `callback` once the form is settled (see `settle()`): straight away when no live
345
- * refresh is waiting or in flight, so a submit stays synchronous in the usual case.
367
+ * refresh or upload is waiting or in flight, so a submit stays synchronous in the usual case.
346
368
  */
347
369
  whenSettled(callback) {
348
- if (![...this.liveDebouncers.values()].some((debounced) => debounced.pending) && !this.refreshInFlight) {
370
+ if (![...this.liveDebouncers.values()].some((debounced) => debounced.pending) && !this.refreshInFlight && this.uploadsInFlight.size === 0) {
349
371
  callback();
350
372
  return;
351
373
  }
package/dist/form.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"form.js","names":[],"sources":["../src/form.ts"],"sourcesContent":["import { modalTableQuery } from './advanced-fields'\nimport { debugRecorder } from './debug'\nimport { reportRequestError } from './error-notifications'\nimport { HttpError, requestJson } from './http'\nimport { currentPageQuery } from './page-query'\nimport { mergeRefreshResponse } from './partial'\nimport type { ModalTableSelectResponse } from './schema/advanced-fields'\nimport type { FieldSchema, SchemaComponent, TabSchema } from './schema/components'\nimport { isField } from './schema/components'\nimport type {\n FormPayload,\n SchemaRefreshResponse,\n StepValidationHaltedResponse,\n} from './schema/form'\nimport type { IconPickerResponse } from './schema/icon-picker'\nimport type { NotificationSchema } from './schema/notifications'\nimport { createStore, type Store } from './store'\nimport type { TableQuery } from './table'\nimport { cloneState } from './utils/clone'\nimport { debounce, type Debounced } from './utils/debounce'\nimport { getPath, setPath } from './utils/path'\n\nexport interface FormState {\n components: SchemaComponent[]\n values: Record<string, unknown>\n errors: Record<string, string[]>\n /** State paths that changed since the last fill. */\n dirty: Set<string>\n /** True while a live refresh or submit is in flight. */\n processing: boolean\n refreshing: boolean\n columns: FormPayload['columns']\n}\n\nexport interface FormStoreOptions {\n /** Encrypted page context token. */\n token: string\n /** Context component name (`form`, `action:table.edit`, …). */\n component: string\n /** Record key when the schema belongs to a table action. */\n record?: string | number | null\n /** URL of the schema refresh endpoint. */\n refreshUrl?: string\n /** URL of the select options endpoint. */\n optionsUrl?: string\n /** URL of the step validation endpoint (wizards validate each step on \"Next\"). */\n validateUrl?: string\n /**\n * Extra parameters for every request (refresh, options). The modal form of a field\n * action sends its owning form's state (`ownerState`) so the server can find the field.\n */\n params?: () => Record<string, unknown>\n onError?: (error: unknown) => void\n /** Shows notifications the server sent with a JSON answer (a halted wizard step). */\n onNotifications?: (notifications: NotificationSchema[]) => void\n /** Called after a live refresh was merged (e.g. to reload what depends on the form). */\n onRefreshed?: (path: string | undefined) => void\n /**\n * Prefix of the fields' DOM ids (`{idPrefix}-{statePath}`), default `alfakit`. Forms shown\n * over another form (action modals) use their own, so ids never repeat on the page.\n */\n idPrefix?: string\n}\n\n/** A DOM-safe id prefix for a form, e.g. `action:field.category_id.createOption` -> `alfakit-action-field-category_id-createOption`. */\nexport function formIdPrefix(scope: string): string {\n return `alfakit-${scope.replace(/[^a-zA-Z0-9_-]/g, '-')}`\n}\n\n/** The DOM id of a field: `{prefix}-{statePath}` with unsafe characters replaced. */\nexport function fieldDomId(prefix: string, statePath: string): string {\n return `${prefix}-${statePath.replace(/[^a-zA-Z0-9_-]/g, '-')}`\n}\n\n/**\n * Walk a component tree and collect every field (including those inside Repeater items).\n */\nexport function collectFields(\n components: SchemaComponent[],\n into: FieldSchema[] = [],\n): FieldSchema[] {\n for (const component of components) {\n if (isField(component)) {\n into.push(component)\n\n if (component.type === 'Repeater' || component.type === 'Builder') {\n for (const item of component.items) {\n collectFields(item.schema, into)\n }\n }\n }\n\n if (component.schema) {\n collectFields(component.schema, into)\n }\n }\n\n return into\n}\n\n/**\n * The field at `path`. Items the browser added (or cloned) before the server has seen\n * them have no per-item schema yet: their fields are found in the Repeater's `template`\n * (or the Builder block's schema, preferring the block named by the item's `type` in\n * `values`), whose `__INDEX__` placeholders stand for any item index, so `live()` fields\n * of fresh items refresh like any other.\n */\nexport function findFieldByPath(\n components: SchemaComponent[],\n path: string,\n values: Record<string, unknown> = {},\n): FieldSchema | undefined {\n return (\n collectFields(components).find((field) => field.statePath === path) ??\n findTemplateField(components, path, values)\n )\n}\n\nfunction findTemplateField(\n components: SchemaComponent[],\n path: string,\n values: Record<string, unknown>,\n): FieldSchema | undefined {\n for (const field of collectFields(components)) {\n for (const template of itemTemplates(field, path, values)) {\n const found =\n collectFields(template).find((candidate) =>\n templatePathMatches(candidate.statePath, path),\n ) ?? findTemplateField(template, path, values)\n\n if (found) return found\n }\n }\n\n return undefined\n}\n\n/** The blank item schemas of a Repeater or Builder (the item's own block first). */\nfunction itemTemplates(\n field: FieldSchema,\n path: string,\n values: Record<string, unknown>,\n): SchemaComponent[][] {\n if (field.type === 'Repeater') {\n return [(field as unknown as { template?: SchemaComponent[] }).template ?? []]\n }\n\n if (field.type !== 'Builder') return []\n\n const blocks =\n (field as unknown as { blocks?: Array<{ name: string; schema: SchemaComponent[] }> }).blocks ??\n []\n const item = new RegExp(`^(${templatePathPattern(field.statePath)}\\\\.\\\\d+)\\\\.`).exec(path)\n const type = item ? getPath<unknown>(values, `${item[1]}.type`) : undefined\n\n return [...blocks]\n .sort((a, b) => Number(b.name === type) - Number(a.name === type))\n .map((block) => block.schema)\n}\n\nfunction templatePathPattern(statePath: string): string {\n return statePath\n .split('__INDEX__')\n .map((part) => part.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'))\n .join('\\\\d+')\n}\n\nfunction templatePathMatches(statePath: string, path: string): boolean {\n return (\n statePath.includes('__INDEX__') && new RegExp(`^${templatePathPattern(statePath)}$`).test(path)\n )\n}\n\n/**\n * The wizard step to reveal after the errors change: the first step with an error, but\n * only when new error keys arrived (a submit or validation response), so the user can\n * still move between steps while errors are shown.\n */\nexport function wizardStepToReveal(\n steps: SchemaComponent[],\n previousErrors: Record<string, unknown>,\n errors: Record<string, unknown>,\n): number | null {\n if (!Object.keys(errors).some((key) => !(key in previousErrors))) {\n return null\n }\n\n const index = steps.findIndex((step) =>\n collectFields(step.schema ?? []).some((field) => errors[field.statePath]),\n )\n\n return index === -1 ? null : index\n}\n\n/**\n * Whether a layout component (a tab, a section) holds a field with a validation error:\n * one of its fields, a nested repeater/builder item field (`items.0.sku`), or, while the\n * component is deferred and its fields aren't loaded, one of its `deferredPaths`.\n */\nexport function componentHasErrors(\n component: Pick<SchemaComponent, 'schema' | 'deferred' | 'deferredPaths'>,\n errors: Record<string, unknown>,\n): boolean {\n const keys = Object.keys(errors).filter((key) => errors[key])\n\n if (keys.length === 0) return false\n\n const paths =\n component.deferred && !component.schema?.length\n ? (component.deferredPaths ?? [])\n : collectFields(component.schema ?? []).map((field) => field.statePath)\n\n return paths.some((path) => keys.some((key) => key === path || key.startsWith(`${path}.`)))\n}\n\n/**\n * The value a tab is written to the query string as (`persistTabInQueryString()`): its\n * readable `queryValue` (`billing-address`), else its key.\n */\nexport function tabQueryValue(tab: Pick<TabSchema, 'key' | 'queryValue'>): string {\n return tab.queryValue ?? tab.key\n}\n\n/**\n * The key of the tab a query-string value names: a tab's `queryValue`, or its key (links\n * written before tabs had readable values, `?tab=tabs-0.tab-1`). Null when none matches.\n */\nexport function tabFromQuery(\n tabs: Array<Pick<TabSchema, 'key' | 'queryValue'>>,\n value: string | null | undefined,\n): string | null {\n if (!value) return null\n\n return (\n (tabs.find((tab) => tab.queryValue === value) ?? tabs.find((tab) => tab.key === value))?.key ??\n null\n )\n}\n\n/**\n * The tab to show after the errors change: when new error keys arrived (a submit or\n * validation response) and the active tab has none, the first tab with an error.\n */\nexport function tabToReveal(\n tabs: Array<Pick<SchemaComponent, 'key' | 'schema' | 'deferred' | 'deferredPaths'>>,\n activeKey: string,\n previousErrors: Record<string, unknown>,\n errors: Record<string, unknown>,\n): string | null {\n if (!Object.keys(errors).some((key) => !(key in previousErrors))) {\n return null\n }\n\n const active = tabs.find((tab) => tab.key === activeKey)\n\n if (active && componentHasErrors(active, errors)) return null\n\n return tabs.find((tab) => componentHasErrors(tab, errors))?.key ?? null\n}\n\n/** Whether `key` is the error of one item of the list field at `path` (`labels.2`). */\nfunction isItemErrorKey(key: string, path: string): boolean {\n return (\n key.length > path.length + 1 &&\n key.startsWith(`${path}.`) &&\n /^\\d+$/.test(key.slice(path.length + 1))\n )\n}\n\n/**\n * The validation message a field shows: its own, else the first of its items' (`labels.2`\n * for a tag refused by `nestedRecursiveRules()`, `attachments.0` for one upload). Those\n * errors have no field of their own, so without this the form refused to save with no\n * message anywhere. Deeper paths (`items.0.name`) belong to item fields and are left out.\n */\nexport function fieldError(\n errors: Readonly<Record<string, readonly string[] | undefined>>,\n path: string,\n): string | undefined {\n const own = errors[path]?.[0]\n\n if (own) return own\n\n for (const key of Object.keys(errors)) {\n if (isItemErrorKey(key, path) && errors[key]?.[0]) return errors[key][0]\n }\n\n return undefined\n}\n\n/**\n * Framework-agnostic form state: values addressed by `statePath`, validation errors,\n * dirty tracking and server round-trips for `live()` fields.\n */\nexport class FormStore {\n public readonly store: Store<FormState>\n\n private readonly liveDebouncers = new Map<string, Debounced<[]>>()\n\n private refreshController: AbortController | null = null\n\n /**\n * The values as the server last sent them, so a refresh can tell `afterStateUpdated`\n * hooks what the field held before the change (`$old`).\n */\n private serverValues: Record<string, unknown>\n\n /**\n * Paths the user changed while a live refresh was in flight. Only these keep their\n * client value when the response is merged; every other path takes the server's value\n * (including values set by `$set` in `afterStateUpdated` hooks).\n */\n private editsInFlight: Set<string> | null = null\n\n /** The fingerprints of the components the store holds (see `PartialFormPayload`). */\n private fingerprints: Record<string, string> | undefined\n\n constructor(\n payload: FormPayload,\n private options: FormStoreOptions,\n ) {\n this.serverValues = payload.state ?? {}\n this.fingerprints = payload.fingerprints\n this.store = createStore<FormState>({\n components: payload.components,\n values: payload.state ?? {},\n errors: {},\n dirty: new Set(),\n processing: false,\n refreshing: false,\n columns: payload.columns,\n })\n\n // Listed by the developer debug panel while it is enabled; a no-op otherwise.\n debugRecorder.registerForm(this)\n }\n\n get state(): FormState {\n return this.store.get()\n }\n\n /**\n * Swap in a fresher context token (every response re-issues it) without touching the\n * form state, so live refreshes keep working after unrelated reloads.\n */\n setToken(token: string): void {\n this.options = { ...this.options, token }\n }\n\n /** Context component name of the schema (`form`, `action:table.edit`, …). */\n get component(): string {\n return this.options.component\n }\n\n /** Prefix of the fields' DOM ids (see `FormStoreOptions.idPrefix`). */\n get idPrefix(): string {\n return this.options.idPrefix ?? 'alfakit'\n }\n\n get record(): string | number | null {\n return this.options.record ?? null\n }\n\n get token(): string {\n return this.options.token\n }\n\n get optionsUrl(): string | undefined {\n return this.options.optionsUrl\n }\n\n get validateUrl(): string | undefined {\n return this.options.validateUrl\n }\n\n /** The fingerprints of the held components, sent so responses can be partial. */\n get currentFingerprints(): Record<string, string> | undefined {\n return this.fingerprints\n }\n\n /** The parameters that identify this schema to the internal endpoints. */\n requestContext(): Record<string, unknown> {\n return {\n token: this.options.token,\n component: this.options.component,\n record: this.options.record ?? undefined,\n // A record action's modal on a list tab: the server finds the record through the\n // page's tab and filters, as the modal request itself does.\n tableQuery: currentPageQuery(),\n ...this.options.params?.(),\n }\n }\n\n /**\n * `requestContext()` plus the form state for GET requests (options, picker tables,\n * mentions). States travel as JSON: a query string drops nulls and empty arrays, and with\n * them repeater items that are still blank, so the server couldn't find their fields.\n */\n queryContext(): Record<string, unknown> {\n const context = this.requestContext()\n const ownerState = context.ownerState\n\n return {\n ...context,\n ...(ownerState !== null && typeof ownerState === 'object'\n ? { ownerState: JSON.stringify(ownerState) }\n : {}),\n state: JSON.stringify(this.state.values),\n }\n }\n\n subscribe(listener: (state: FormState) => void): () => void {\n return this.store.subscribe(listener)\n }\n\n // ---------------------------------------------------------------- Values\n\n getValue<T = unknown>(path: string, fallback?: T): T {\n return getPath<T>(this.state.values, path, fallback)\n }\n\n /**\n * Set a value. Live fields schedule a server refresh (debounced); `onBlur` fields\n * refresh via `blur()` instead.\n */\n setValue(path: string, value: unknown, options: { silent?: boolean } = {}): void {\n const current = this.getValue(path)\n\n if (Object.is(current, value)) {\n return\n }\n\n this.store.set((state) => {\n const dirty = new Set(state.dirty)\n dirty.add(path)\n\n const errors = { ...state.errors }\n delete errors[path]\n\n // Item errors of a list field (`labels.2` from `nestedRecursiveRules()`) go with it.\n for (const key of Object.keys(errors)) {\n if (isItemErrorKey(key, path)) delete errors[key]\n }\n\n return { ...state, values: setPath(state.values, path, value), dirty, errors }\n })\n\n this.editsInFlight?.add(path)\n\n if (options.silent) {\n return\n }\n\n const target = this.liveTarget(path)\n\n if (target?.field.live && !target.field.live.onBlur) {\n this.scheduleRefresh(target.path, target.field.live.debounce ?? 150)\n }\n }\n\n /** Call when a field loses focus so `live(onBlur: true)` fields refresh. */\n blur(path: string): void {\n const target = this.liveTarget(path)\n\n if (target?.field.live?.onBlur && this.state.dirty.has(path)) {\n void this.refresh(target.path)\n }\n }\n\n /**\n * The field whose `live()` a change at `path` triggers, and the path to refresh: a\n * translatable field's language input (`title.en`) reports its field (`title`), whose\n * `afterStateUpdated()` gets the whole `{ code: value }` state.\n */\n private liveTarget(path: string): { field: FieldSchema; path: string } | undefined {\n const field = this.findField(path)\n\n if (field) return { field, path }\n\n const dot = path.lastIndexOf('.')\n\n if (dot <= 0) return undefined\n\n const parentPath = path.slice(0, dot)\n const parent = this.findField(parentPath)\n const code = path.slice(dot + 1)\n\n return parent?.translatable?.locales.some((locale) => locale.code === code)\n ? { field: parent, path: parentPath }\n : undefined\n }\n\n /** Replace the whole state (e.g. after the server returns a refreshed payload). */\n fill(payload: FormPayload, options: { keepDirty?: boolean } = {}): void {\n this.serverValues = payload.state ?? {}\n this.fingerprints = payload.fingerprints\n this.store.set((state) => ({\n ...state,\n components: payload.components,\n values: payload.state ?? {},\n columns: payload.columns ?? state.columns,\n dirty: options.keepDirty ? state.dirty : new Set(),\n }))\n }\n\n setErrors(errors: Record<string, string[] | string>): void {\n const normalized: Record<string, string[]> = {}\n\n for (const [key, value] of Object.entries(errors)) {\n normalized[key] = Array.isArray(value) ? value : [value]\n }\n\n this.store.update({ errors: normalized })\n }\n\n clearErrors(): void {\n this.store.update({ errors: {} })\n }\n\n getError(path: string): string | undefined {\n return fieldError(this.state.errors, path)\n }\n\n setProcessing(processing: boolean): void {\n this.store.update({ processing })\n }\n\n get isDirty(): boolean {\n return this.state.dirty.size > 0\n }\n\n findField(path: string): FieldSchema | undefined {\n return findFieldByPath(this.state.components, path, this.state.values)\n }\n\n // ---------------------------------------------------------------- Live refresh\n\n private scheduleRefresh(path: string, wait: number): void {\n let debounced = this.liveDebouncers.get(path)\n\n if (!debounced) {\n debounced = debounce(() => {\n void this.refresh(path)\n }, wait)\n this.liveDebouncers.set(path, debounced)\n }\n\n debounced()\n }\n\n /**\n * Send the current state to the server, which re-evaluates closures and\n * `afterStateUpdated` hooks, and replace components + values with the response.\n */\n refresh(path?: string): Promise<void> {\n const request = this.sendRefresh(path)\n this.refreshInFlight = request\n\n void request.finally(() => {\n if (this.refreshInFlight === request) this.refreshInFlight = null\n })\n\n return request\n }\n\n /**\n * Resolves once no live refresh is waiting or in flight: debounced refreshes are sent\n * now and awaited. Submits wait for it, so a click that blurs a `live(onBlur: true)` field\n * sends the state the server computed (`$set` in `afterStateUpdated`), not the one before.\n */\n async settle(): Promise<void> {\n for (const debounced of this.liveDebouncers.values()) debounced.flush()\n\n while (this.refreshInFlight) {\n await this.refreshInFlight.catch(() => undefined)\n }\n }\n\n /**\n * Run `callback` once the form is settled (see `settle()`): straight away when no live\n * refresh is waiting or in flight, so a submit stays synchronous in the usual case.\n */\n whenSettled(callback: () => void): void {\n const waiting = [...this.liveDebouncers.values()].some((debounced) => debounced.pending)\n\n if (!waiting && !this.refreshInFlight) {\n callback()\n\n return\n }\n\n void this.settle().then(callback)\n }\n\n private refreshInFlight: Promise<void> | null = null\n\n private async sendRefresh(path?: string): Promise<void> {\n if (!this.options.refreshUrl) {\n return\n }\n\n this.refreshController?.abort()\n const controller = new AbortController()\n this.refreshController = controller\n // The request carries the current state, so only edits made from now on are newer\n // than what the server sees.\n const edits = new Set<string>()\n this.editsInFlight = edits\n\n this.store.update({ refreshing: true })\n const sent = this.state.values\n\n try {\n const response = await requestJson<SchemaRefreshResponse>(this.options.refreshUrl, {\n method: 'POST',\n body: {\n ...this.requestContext(),\n state: sent,\n path,\n old: path ? (getPath(this.serverValues, path) ?? null) : undefined,\n // With fingerprints the server answers with only what changed.\n fingerprints: this.fingerprints,\n },\n signal: controller.signal,\n })\n\n if (controller.signal.aborted) {\n return\n }\n\n if (this.editsInFlight === edits) {\n this.editsInFlight = null\n }\n\n if (!this.applyResponse(response, sent, edits)) {\n // The patches didn't fit the tree: ask for the whole form instead.\n this.fingerprints = undefined\n await this.sendRefresh(path)\n\n return\n }\n\n this.options.onRefreshed?.(path)\n } catch (error) {\n if ((error as Error).name === 'AbortError') {\n return\n }\n\n if (this.editsInFlight === edits) {\n this.editsInFlight = null\n }\n\n this.store.update({ refreshing: false })\n // Without an `onError`, the panel's error notification for the status is shown.\n ;(this.options.onError ?? reportRequestError)(error)\n }\n }\n\n /**\n * Merge a refresh response (full or partial) for the state `sent` with the request.\n * Paths in `edits` changed while the request was in flight and keep their client value;\n * everything else takes the server's (including values set by `$set`). Returns false\n * when a partial response doesn't fit the tree the store holds.\n */\n applyResponse(\n response: SchemaRefreshResponse,\n sent: Record<string, unknown>,\n edits: Iterable<string> = [],\n ): boolean {\n const merged = mergeRefreshResponse(this.state.components, sent, response)\n\n if (!merged) {\n this.store.update({ refreshing: false })\n\n return false\n }\n\n this.serverValues = merged.values\n this.fingerprints = merged.fingerprints\n\n this.store.set((state) => {\n let values = merged.values\n\n for (const editedPath of edits) {\n values = setPath(values, editedPath, getPath(state.values, editedPath))\n }\n\n return {\n ...state,\n components: merged.components,\n values,\n columns: merged.columns ?? state.columns,\n refreshing: false,\n }\n })\n\n return true\n }\n\n /**\n * Validate the fields of one wizard step (by the Step component's `key`) on the server.\n * Resolves with true when they pass; otherwise the step's errors replace the errors of\n * those fields and false is returned. Without a validation endpoint every step passes.\n */\n async validateStep(stepKey: string, fieldPaths: string[]): Promise<boolean> {\n if (!this.options.validateUrl) {\n return true\n }\n\n try {\n const response = await requestJson<StepValidationHaltedResponse | null>(\n this.options.validateUrl,\n {\n method: 'POST',\n body: {\n ...this.requestContext(),\n state: this.state.values,\n step: stepKey,\n },\n },\n )\n\n // A step hook halted: stay on the step, keep the field errors as they are.\n if (response !== null && typeof response === 'object' && response.halted) {\n const notifications = response.notifications ?? []\n\n if (notifications.length > 0) this.options.onNotifications?.(notifications)\n\n return false\n }\n } catch (error) {\n if (error instanceof HttpError && error.status === 422) {\n this.store.update({\n errors: { ...withoutStepErrors(this.state.errors, fieldPaths), ...error.errors },\n })\n\n return false\n }\n\n ;(this.options.onError ?? reportRequestError)(error)\n\n return false\n }\n\n this.store.update({ errors: withoutStepErrors(this.state.errors, fieldPaths) })\n\n return true\n }\n\n /** Search options for a searchable select at `path`. */\n async searchOptions(\n path: string,\n search: string,\n ): Promise<Array<{ value: string; label: string }>> {\n if (!this.options.optionsUrl) {\n return []\n }\n\n const response = await requestJson<{ options?: Array<{ value: string; label: string }> }>(\n this.options.optionsUrl,\n {\n query: { ...this.queryContext(), path, search },\n },\n )\n\n return response.options ?? []\n }\n\n async optionLabels(\n path: string,\n values: Array<string | number>,\n ): Promise<Record<string, string>> {\n if (!this.options.optionsUrl || values.length === 0) {\n return {}\n }\n\n const response = await requestJson<{ labels?: Record<string, string> }>(\n this.options.optionsUrl,\n {\n query: { ...this.queryContext(), path, values },\n },\n )\n\n return response.labels ?? {}\n }\n\n /** One page of icons for the IconPicker at `path` (`set`: one of its sets, or all). */\n async searchIcons(\n path: string,\n query: { search?: string; set?: string | null; page?: number },\n signal?: AbortSignal,\n ): Promise<IconPickerResponse | null> {\n if (!this.options.optionsUrl) {\n return null\n }\n\n return requestJson<IconPickerResponse>(this.options.optionsUrl, {\n query: {\n ...this.queryContext(),\n path,\n search: query.search ?? '',\n ...(query.set ? { set: query.set } : {}),\n page: query.page ?? 1,\n },\n signal,\n })\n }\n\n /**\n * The picker table of a ModalTableSelect at `path` (`query` is its table state) and the\n * labels of the records it lists.\n */\n async modalTable(\n path: string,\n identifier: string,\n query: TableQuery,\n signal?: AbortSignal,\n ): Promise<ModalTableSelectResponse | null> {\n if (!this.options.optionsUrl) {\n return null\n }\n\n return requestJson<ModalTableSelectResponse>(this.options.optionsUrl, {\n query: {\n ...this.queryContext(),\n path,\n table: 1,\n ...modalTableQuery(identifier, query),\n },\n signal,\n })\n }\n\n destroy(): void {\n debugRecorder.unregisterForm(this)\n this.refreshController?.abort()\n\n for (const debounced of this.liveDebouncers.values()) {\n debounced.cancel()\n }\n\n this.liveDebouncers.clear()\n }\n}\n\n/** The errors without those of the given fields (and of their nested paths). */\nfunction withoutStepErrors(\n errors: Record<string, string[]>,\n fieldPaths: string[],\n): Record<string, string[]> {\n const kept: Record<string, string[]> = {}\n\n for (const [key, messages] of Object.entries(errors)) {\n if (!fieldPaths.some((path) => key === path || key.startsWith(`${path}.`))) {\n kept[key] = messages\n }\n }\n\n return kept\n}\n\n/**\n * A stable key for a page form: the page class, record and the form's own key/component.\n * The context token is re-encrypted on every response, so it cannot identify a form.\n * Renderers re-initialise a FormStore from server state only when this key changes (or\n * after the form's own successful submit), never on unrelated reloads of the same page.\n */\nexport function formIdentity(\n page: { class?: string; record?: string | number | null } | null | undefined,\n block: { key: string; component: string; operation?: string | null },\n): string {\n return JSON.stringify([\n page?.class ?? null,\n page?.record ?? null,\n block.key,\n block.component,\n block.operation ?? null,\n ])\n}\n\n/** The parts of an Inertia visit the unsaved-changes guard needs. */\nexport interface GuardedVisit {\n method: string\n url: URL | string\n only?: string[]\n except?: string[]\n preserveState?: unknown\n}\n\n/**\n * True when an Inertia visit would navigate away from the current form, i.e. when the\n * unsaved-changes guard should ask. Partial reloads (`only`/`except`, used by table\n * sorting, paging and polling) and state-preserving visits to the same path stay on the\n * page and keep the form, so they never prompt. Non-GET visits are submissions.\n */\nexport function isLeavingVisit(visit: GuardedVisit, currentUrl?: string): boolean {\n if (visit.method.toLowerCase() !== 'get') {\n return false\n }\n\n if ((visit.only?.length ?? 0) > 0 || (visit.except?.length ?? 0) > 0) {\n return false\n }\n\n const base =\n currentUrl ?? (typeof window !== 'undefined' ? window.location.href : 'http://localhost/')\n const here = new URL(base)\n const target = new URL(String(visit.url), here)\n\n return !(\n visit.preserveState === true &&\n target.origin === here.origin &&\n target.pathname === here.pathname\n )\n}\n\n// ---------------------------------------------------------------- Repeater helpers\n\nexport interface RepeaterItemState {\n key: string\n state: Record<string, unknown>\n}\n\n/** Append a blank item using the Repeater's template state. */\nexport function appendRepeaterItem(\n items: unknown[],\n templateState: Record<string, unknown>,\n): unknown[] {\n return [...items, cloneState(templateState)]\n}\n\nexport function removeRepeaterItem(items: unknown[], index: number): unknown[] {\n return items.filter((_, i) => i !== index)\n}\n\nexport function moveRepeaterItem(items: unknown[], from: number, to: number): unknown[] {\n if (from === to || from < 0 || to < 0 || from >= items.length || to >= items.length) {\n return items\n }\n\n const copy = [...items]\n const [item] = copy.splice(from, 1)\n copy.splice(to, 0, item)\n\n return copy\n}\n\nexport function cloneRepeaterItem(items: unknown[], index: number): unknown[] {\n const copy = [...items]\n copy.splice(index + 1, 0, cloneState(items[index]))\n\n return copy\n}\n\n/**\n * Per-item UI state (collapsed flags, …) keyed by item index, remapped after the items\n * are reordered, removed or cloned with the helpers above so it stays with its item.\n */\nexport type IndexedState<T> = Record<number, T>\n\nfunction remapIndexedState<T>(\n state: IndexedState<T>,\n newIndex: (index: number) => number | null,\n): IndexedState<T> {\n const result: IndexedState<T> = {}\n\n for (const [key, value] of Object.entries(state) as [string, T][]) {\n const index = newIndex(Number(key))\n\n if (index !== null) result[index] = value\n }\n\n return result\n}\n\nexport function moveIndexedState<T>(\n state: IndexedState<T>,\n from: number,\n to: number,\n): IndexedState<T> {\n if (from === to) return state\n\n return remapIndexedState(state, (index) => {\n if (index === from) return to\n if (from < to && index > from && index <= to) return index - 1\n if (from > to && index >= to && index < from) return index + 1\n\n return index\n })\n}\n\nexport function removeIndexedState<T>(state: IndexedState<T>, removed: number): IndexedState<T> {\n return remapIndexedState(state, (index) =>\n index === removed ? null : index > removed ? index - 1 : index,\n )\n}\n\nexport function cloneIndexedState<T>(state: IndexedState<T>, cloned: number): IndexedState<T> {\n const result = remapIndexedState(state, (index) => (index > cloned ? index + 1 : index))\n\n if (cloned in state) result[cloned + 1] = state[cloned] as T\n\n return result\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"form.js","names":[],"sources":["../src/form.ts"],"sourcesContent":["import { modalTableQuery } from './advanced-fields'\nimport { debugRecorder } from './debug'\nimport { reportRequestError } from './error-notifications'\nimport { HttpError, requestJson } from './http'\nimport { currentPageQuery } from './page-query'\nimport { mergeRefreshResponse } from './partial'\nimport type { ModalTableSelectResponse } from './schema/advanced-fields'\nimport type { FieldSchema, SchemaComponent, TabSchema } from './schema/components'\nimport { isField } from './schema/components'\nimport type {\n FormPayload,\n SchemaRefreshResponse,\n StepValidationHaltedResponse,\n} from './schema/form'\nimport type { IconPickerResponse } from './schema/icon-picker'\nimport type { NotificationSchema } from './schema/notifications'\nimport { createStore, type Store } from './store'\nimport type { TableQuery } from './table'\nimport { cloneState } from './utils/clone'\nimport { debounce, type Debounced } from './utils/debounce'\nimport { getPath, setPath } from './utils/path'\n\nexport interface FormState {\n components: SchemaComponent[]\n values: Record<string, unknown>\n errors: Record<string, string[]>\n /** State paths that changed since the last fill. */\n dirty: Set<string>\n /** True while a live refresh or submit is in flight. */\n processing: boolean\n refreshing: boolean\n columns: FormPayload['columns']\n}\n\nexport interface FormStoreOptions {\n /** Encrypted page context token. */\n token: string\n /** Context component name (`form`, `action:table.edit`, …). */\n component: string\n /** Record key when the schema belongs to a table action. */\n record?: string | number | null\n /** URL of the schema refresh endpoint. */\n refreshUrl?: string\n /** URL of the select options endpoint. */\n optionsUrl?: string\n /** URL of the step validation endpoint (wizards validate each step on \"Next\"). */\n validateUrl?: string\n /**\n * Extra parameters for every request (refresh, options). The modal form of a field\n * action sends its owning form's state (`ownerState`) so the server can find the field.\n */\n params?: () => Record<string, unknown>\n onError?: (error: unknown) => void\n /** Shows notifications the server sent with a JSON answer (a halted wizard step). */\n onNotifications?: (notifications: NotificationSchema[]) => void\n /** Called after a live refresh was merged (e.g. to reload what depends on the form). */\n onRefreshed?: (path: string | undefined) => void\n /**\n * Prefix of the fields' DOM ids (`{idPrefix}-{statePath}`), default `alfakit`. Forms shown\n * over another form (action modals) use their own, so ids never repeat on the page.\n */\n idPrefix?: string\n}\n\n/** A DOM-safe id prefix for a form, e.g. `action:field.category_id.createOption` -> `alfakit-action-field-category_id-createOption`. */\nexport function formIdPrefix(scope: string): string {\n return `alfakit-${scope.replace(/[^a-zA-Z0-9_-]/g, '-')}`\n}\n\n/** The DOM id of a field: `{prefix}-{statePath}` with unsafe characters replaced. */\nexport function fieldDomId(prefix: string, statePath: string): string {\n return `${prefix}-${statePath.replace(/[^a-zA-Z0-9_-]/g, '-')}`\n}\n\n/**\n * Walk a component tree and collect every field (including those inside Repeater items).\n */\nexport function collectFields(\n components: SchemaComponent[],\n into: FieldSchema[] = [],\n): FieldSchema[] {\n for (const component of components) {\n if (isField(component)) {\n into.push(component)\n\n if (component.type === 'Repeater' || component.type === 'Builder') {\n for (const item of component.items) {\n collectFields(item.schema, into)\n }\n }\n }\n\n if (component.schema) {\n collectFields(component.schema, into)\n }\n }\n\n return into\n}\n\n/**\n * The field at `path`. Items the browser added (or cloned) before the server has seen\n * them have no per-item schema yet: their fields are found in the Repeater's `template`\n * (or the Builder block's schema, preferring the block named by the item's `type` in\n * `values`), whose `__INDEX__` placeholders stand for any item index, so `live()` fields\n * of fresh items refresh like any other.\n */\nexport function findFieldByPath(\n components: SchemaComponent[],\n path: string,\n values: Record<string, unknown> = {},\n): FieldSchema | undefined {\n return (\n collectFields(components).find((field) => field.statePath === path) ??\n findTemplateField(components, path, values)\n )\n}\n\nfunction findTemplateField(\n components: SchemaComponent[],\n path: string,\n values: Record<string, unknown>,\n): FieldSchema | undefined {\n for (const field of collectFields(components)) {\n for (const template of itemTemplates(field, path, values)) {\n const found =\n collectFields(template).find((candidate) =>\n templatePathMatches(candidate.statePath, path),\n ) ?? findTemplateField(template, path, values)\n\n if (found) return found\n }\n }\n\n return undefined\n}\n\n/** The blank item schemas of a Repeater or Builder (the item's own block first). */\nfunction itemTemplates(\n field: FieldSchema,\n path: string,\n values: Record<string, unknown>,\n): SchemaComponent[][] {\n if (field.type === 'Repeater') {\n return [(field as unknown as { template?: SchemaComponent[] }).template ?? []]\n }\n\n if (field.type !== 'Builder') return []\n\n const blocks =\n (field as unknown as { blocks?: Array<{ name: string; schema: SchemaComponent[] }> }).blocks ??\n []\n const item = new RegExp(`^(${templatePathPattern(field.statePath)}\\\\.\\\\d+)\\\\.`).exec(path)\n const type = item ? getPath<unknown>(values, `${item[1]}.type`) : undefined\n\n return [...blocks]\n .sort((a, b) => Number(b.name === type) - Number(a.name === type))\n .map((block) => block.schema)\n}\n\nfunction templatePathPattern(statePath: string): string {\n return statePath\n .split('__INDEX__')\n .map((part) => part.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'))\n .join('\\\\d+')\n}\n\nfunction templatePathMatches(statePath: string, path: string): boolean {\n return (\n statePath.includes('__INDEX__') && new RegExp(`^${templatePathPattern(statePath)}$`).test(path)\n )\n}\n\n/**\n * The wizard step to reveal after the errors change: the first step with an error, but\n * only when new error keys arrived (a submit or validation response), so the user can\n * still move between steps while errors are shown.\n */\nexport function wizardStepToReveal(\n steps: SchemaComponent[],\n previousErrors: Record<string, unknown>,\n errors: Record<string, unknown>,\n): number | null {\n if (!Object.keys(errors).some((key) => !(key in previousErrors))) {\n return null\n }\n\n const index = steps.findIndex((step) =>\n collectFields(step.schema ?? []).some((field) => errors[field.statePath]),\n )\n\n return index === -1 ? null : index\n}\n\n/**\n * Whether a layout component (a tab, a section) holds a field with a validation error:\n * one of its fields, a nested repeater/builder item field (`items.0.sku`), or, while the\n * component is deferred and its fields aren't loaded, one of its `deferredPaths`.\n */\nexport function componentHasErrors(\n component: Pick<SchemaComponent, 'schema' | 'deferred' | 'deferredPaths'>,\n errors: Record<string, unknown>,\n): boolean {\n const keys = Object.keys(errors).filter((key) => errors[key])\n\n if (keys.length === 0) return false\n\n const paths =\n component.deferred && !component.schema?.length\n ? (component.deferredPaths ?? [])\n : collectFields(component.schema ?? []).map((field) => field.statePath)\n\n return paths.some((path) => keys.some((key) => key === path || key.startsWith(`${path}.`)))\n}\n\n/**\n * The value a tab is written to the query string as (`persistTabInQueryString()`): its\n * readable `queryValue` (`billing-address`), else its key.\n */\nexport function tabQueryValue(tab: Pick<TabSchema, 'key' | 'queryValue'>): string {\n return tab.queryValue ?? tab.key\n}\n\n/**\n * The key of the tab a query-string value names: a tab's `queryValue`, or its key (links\n * written before tabs had readable values, `?tab=tabs-0.tab-1`). Null when none matches.\n */\nexport function tabFromQuery(\n tabs: Array<Pick<TabSchema, 'key' | 'queryValue'>>,\n value: string | null | undefined,\n): string | null {\n if (!value) return null\n\n return (\n (tabs.find((tab) => tab.queryValue === value) ?? tabs.find((tab) => tab.key === value))?.key ??\n null\n )\n}\n\n/**\n * The tab to show after the errors change: when new error keys arrived (a submit or\n * validation response) and the active tab has none, the first tab with an error.\n */\nexport function tabToReveal(\n tabs: Array<Pick<SchemaComponent, 'key' | 'schema' | 'deferred' | 'deferredPaths'>>,\n activeKey: string,\n previousErrors: Record<string, unknown>,\n errors: Record<string, unknown>,\n): string | null {\n if (!Object.keys(errors).some((key) => !(key in previousErrors))) {\n return null\n }\n\n const active = tabs.find((tab) => tab.key === activeKey)\n\n if (active && componentHasErrors(active, errors)) return null\n\n return tabs.find((tab) => componentHasErrors(tab, errors))?.key ?? null\n}\n\n/** Whether `key` is the error of one item of the list field at `path` (`labels.2`). */\nfunction isItemErrorKey(key: string, path: string): boolean {\n return (\n key.length > path.length + 1 &&\n key.startsWith(`${path}.`) &&\n /^\\d+$/.test(key.slice(path.length + 1))\n )\n}\n\n/**\n * The validation message a field shows: its own, else the first of its items' (`labels.2`\n * for a tag refused by `nestedRecursiveRules()`, `attachments.0` for one upload). Those\n * errors have no field of their own, so without this the form refused to save with no\n * message anywhere. Deeper paths (`items.0.name`) belong to item fields and are left out.\n */\nexport function fieldError(\n errors: Readonly<Record<string, readonly string[] | undefined>>,\n path: string,\n): string | undefined {\n const own = errors[path]?.[0]\n\n if (own) return own\n\n for (const key of Object.keys(errors)) {\n if (isItemErrorKey(key, path) && errors[key]?.[0]) return errors[key][0]\n }\n\n return undefined\n}\n\n/**\n * Framework-agnostic form state: values addressed by `statePath`, validation errors,\n * dirty tracking and server round-trips for `live()` fields.\n */\nexport class FormStore {\n public readonly store: Store<FormState>\n\n private readonly liveDebouncers = new Map<string, Debounced<[]>>()\n\n private refreshController: AbortController | null = null\n\n /**\n * The values as the server last sent them, so a refresh can tell `afterStateUpdated`\n * hooks what the field held before the change (`$old`).\n */\n private serverValues: Record<string, unknown>\n\n /**\n * Paths the user changed while a live refresh was in flight. Only these keep their\n * client value when the response is merged; every other path takes the server's value\n * (including values set by `$set` in `afterStateUpdated` hooks).\n */\n private editsInFlight: Set<string> | null = null\n\n /** The fingerprints of the components the store holds (see `PartialFormPayload`). */\n private fingerprints: Record<string, string> | undefined\n\n constructor(\n payload: FormPayload,\n private options: FormStoreOptions,\n ) {\n this.serverValues = payload.state ?? {}\n this.fingerprints = payload.fingerprints\n this.store = createStore<FormState>({\n components: payload.components,\n values: payload.state ?? {},\n errors: {},\n dirty: new Set(),\n processing: false,\n refreshing: false,\n columns: payload.columns,\n })\n\n // Listed by the developer debug panel while it is enabled; a no-op otherwise.\n debugRecorder.registerForm(this)\n }\n\n get state(): FormState {\n return this.store.get()\n }\n\n /**\n * Swap in a fresher context token (every response re-issues it) without touching the\n * form state, so live refreshes keep working after unrelated reloads.\n */\n setToken(token: string): void {\n this.options = { ...this.options, token }\n }\n\n /** Context component name of the schema (`form`, `action:table.edit`, …). */\n get component(): string {\n return this.options.component\n }\n\n /** Prefix of the fields' DOM ids (see `FormStoreOptions.idPrefix`). */\n get idPrefix(): string {\n return this.options.idPrefix ?? 'alfakit'\n }\n\n get record(): string | number | null {\n return this.options.record ?? null\n }\n\n get token(): string {\n return this.options.token\n }\n\n get optionsUrl(): string | undefined {\n return this.options.optionsUrl\n }\n\n get validateUrl(): string | undefined {\n return this.options.validateUrl\n }\n\n /** The fingerprints of the held components, sent so responses can be partial. */\n get currentFingerprints(): Record<string, string> | undefined {\n return this.fingerprints\n }\n\n /** The parameters that identify this schema to the internal endpoints. */\n requestContext(): Record<string, unknown> {\n return {\n token: this.options.token,\n component: this.options.component,\n record: this.options.record ?? undefined,\n // A record action's modal on a list tab: the server finds the record through the\n // page's tab and filters, as the modal request itself does.\n tableQuery: currentPageQuery(),\n ...this.options.params?.(),\n }\n }\n\n /**\n * `requestContext()` plus the form state for GET requests (options, picker tables,\n * mentions). States travel as JSON: a query string drops nulls and empty arrays, and with\n * them repeater items that are still blank, so the server couldn't find their fields.\n */\n queryContext(): Record<string, unknown> {\n const context = this.requestContext()\n const ownerState = context.ownerState\n\n return {\n ...context,\n ...(ownerState !== null && typeof ownerState === 'object'\n ? { ownerState: JSON.stringify(ownerState) }\n : {}),\n state: JSON.stringify(this.state.values),\n }\n }\n\n subscribe(listener: (state: FormState) => void): () => void {\n return this.store.subscribe(listener)\n }\n\n // ---------------------------------------------------------------- Values\n\n getValue<T = unknown>(path: string, fallback?: T): T {\n return getPath<T>(this.state.values, path, fallback)\n }\n\n /**\n * Set a value. Live fields schedule a server refresh (debounced); `onBlur` fields\n * refresh via `blur()` instead.\n */\n setValue(path: string, value: unknown, options: { silent?: boolean } = {}): void {\n const current = this.getValue(path)\n\n if (Object.is(current, value)) {\n return\n }\n\n this.store.set((state) => {\n const dirty = new Set(state.dirty)\n dirty.add(path)\n\n const errors = { ...state.errors }\n delete errors[path]\n\n // Item errors of a list field (`labels.2` from `nestedRecursiveRules()`) go with it.\n for (const key of Object.keys(errors)) {\n if (isItemErrorKey(key, path)) delete errors[key]\n }\n\n return { ...state, values: setPath(state.values, path, value), dirty, errors }\n })\n\n this.editsInFlight?.add(path)\n\n if (options.silent) {\n return\n }\n\n const target = this.liveTarget(path)\n\n if (target?.field.live && !target.field.live.onBlur) {\n this.scheduleRefresh(target.path, target.field.live.debounce ?? 150)\n }\n }\n\n /** Call when a field loses focus so `live(onBlur: true)` fields refresh. */\n blur(path: string): void {\n const target = this.liveTarget(path)\n\n if (target?.field.live?.onBlur && this.state.dirty.has(path)) {\n void this.refresh(target.path)\n }\n }\n\n /**\n * The field whose `live()` a change at `path` triggers, and the path to refresh: a\n * translatable field's language input (`title.en`) reports its field (`title`), whose\n * `afterStateUpdated()` gets the whole `{ code: value }` state.\n */\n private liveTarget(path: string): { field: FieldSchema; path: string } | undefined {\n const field = this.findField(path)\n\n if (field) return { field, path }\n\n const dot = path.lastIndexOf('.')\n\n if (dot <= 0) return undefined\n\n const parentPath = path.slice(0, dot)\n const parent = this.findField(parentPath)\n const code = path.slice(dot + 1)\n\n return parent?.translatable?.locales.some((locale) => locale.code === code)\n ? { field: parent, path: parentPath }\n : undefined\n }\n\n /** Replace the whole state (e.g. after the server returns a refreshed payload). */\n fill(payload: FormPayload, options: { keepDirty?: boolean } = {}): void {\n this.serverValues = payload.state ?? {}\n this.fingerprints = payload.fingerprints\n this.store.set((state) => ({\n ...state,\n components: payload.components,\n values: payload.state ?? {},\n columns: payload.columns ?? state.columns,\n dirty: options.keepDirty ? state.dirty : new Set(),\n }))\n }\n\n setErrors(errors: Record<string, string[] | string>): void {\n const normalized: Record<string, string[]> = {}\n\n for (const [key, value] of Object.entries(errors)) {\n normalized[key] = Array.isArray(value) ? value : [value]\n }\n\n this.store.update({ errors: normalized })\n }\n\n clearErrors(): void {\n this.store.update({ errors: {} })\n }\n\n getError(path: string): string | undefined {\n return fieldError(this.state.errors, path)\n }\n\n setProcessing(processing: boolean): void {\n this.store.update({ processing })\n }\n\n get isDirty(): boolean {\n return this.state.dirty.size > 0\n }\n\n findField(path: string): FieldSchema | undefined {\n return findFieldByPath(this.state.components, path, this.state.values)\n }\n\n // ---------------------------------------------------------------- Live refresh\n\n private scheduleRefresh(path: string, wait: number): void {\n let debounced = this.liveDebouncers.get(path)\n\n if (!debounced) {\n debounced = debounce(() => {\n void this.refresh(path)\n }, wait)\n this.liveDebouncers.set(path, debounced)\n }\n\n debounced()\n }\n\n /**\n * Send the current state to the server, which re-evaluates closures and\n * `afterStateUpdated` hooks, and replace components + values with the response.\n */\n refresh(path?: string): Promise<void> {\n const request = this.sendRefresh(path)\n this.refreshInFlight = request\n\n void request.finally(() => {\n if (this.refreshInFlight === request) this.refreshInFlight = null\n })\n\n return request\n }\n\n /**\n * Resolves once no live refresh is waiting or in flight: debounced refreshes are sent\n * now and awaited. Submits wait for it, so a click that blurs a `live(onBlur: true)` field\n * sends the state the server computed (`$set` in `afterStateUpdated`), not the one before.\n */\n async settle(): Promise<void> {\n for (;;) {\n for (const debounced of this.liveDebouncers.values()) debounced.flush()\n\n if (this.refreshInFlight) {\n await this.refreshInFlight.catch(() => undefined)\n } else if (this.uploadsInFlight.size > 0) {\n // A file still uploading isn't in the state yet: saving now would drop it. Its\n // field commits the upload's key when it completes (which may refresh a live\n // field, so the loop runs again).\n await Promise.allSettled([...this.uploadsInFlight])\n } else {\n return\n }\n }\n }\n\n /**\n * Register an upload of one of the form's fields (FileUpload, RichEditor attachments)\n * while it runs: `settle()` and `whenSettled()`, and so every submit, wait for it.\n * Returns the same promise; a failed upload stops counting as pending.\n */\n trackUpload<T>(upload: Promise<T>): Promise<T> {\n this.uploadsInFlight.add(upload)\n\n const forget = () => {\n this.uploadsInFlight.delete(upload)\n }\n\n upload.then(forget, forget)\n\n return upload\n }\n\n /** Whether an upload registered with `trackUpload()` is still running. */\n get hasPendingUploads(): boolean {\n return this.uploadsInFlight.size > 0\n }\n\n private readonly uploadsInFlight = new Set<Promise<unknown>>()\n\n /**\n * Run `callback` once the form is settled (see `settle()`): straight away when no live\n * refresh or upload is waiting or in flight, so a submit stays synchronous in the usual case.\n */\n whenSettled(callback: () => void): void {\n const waiting = [...this.liveDebouncers.values()].some((debounced) => debounced.pending)\n\n if (!waiting && !this.refreshInFlight && this.uploadsInFlight.size === 0) {\n callback()\n\n return\n }\n\n void this.settle().then(callback)\n }\n\n private refreshInFlight: Promise<void> | null = null\n\n private async sendRefresh(path?: string): Promise<void> {\n if (!this.options.refreshUrl) {\n return\n }\n\n this.refreshController?.abort()\n const controller = new AbortController()\n this.refreshController = controller\n // The request carries the current state, so only edits made from now on are newer\n // than what the server sees.\n const edits = new Set<string>()\n this.editsInFlight = edits\n\n this.store.update({ refreshing: true })\n const sent = this.state.values\n\n try {\n const response = await requestJson<SchemaRefreshResponse>(this.options.refreshUrl, {\n method: 'POST',\n body: {\n ...this.requestContext(),\n state: sent,\n path,\n old: path ? (getPath(this.serverValues, path) ?? null) : undefined,\n // With fingerprints the server answers with only what changed.\n fingerprints: this.fingerprints,\n },\n signal: controller.signal,\n })\n\n if (controller.signal.aborted) {\n return\n }\n\n if (this.editsInFlight === edits) {\n this.editsInFlight = null\n }\n\n if (!this.applyResponse(response, sent, edits)) {\n // The patches didn't fit the tree: ask for the whole form instead.\n this.fingerprints = undefined\n await this.sendRefresh(path)\n\n return\n }\n\n this.options.onRefreshed?.(path)\n } catch (error) {\n if ((error as Error).name === 'AbortError') {\n return\n }\n\n if (this.editsInFlight === edits) {\n this.editsInFlight = null\n }\n\n this.store.update({ refreshing: false })\n // Without an `onError`, the panel's error notification for the status is shown.\n ;(this.options.onError ?? reportRequestError)(error)\n }\n }\n\n /**\n * Merge a refresh response (full or partial) for the state `sent` with the request.\n * Paths in `edits` changed while the request was in flight and keep their client value;\n * everything else takes the server's (including values set by `$set`). Returns false\n * when a partial response doesn't fit the tree the store holds.\n */\n applyResponse(\n response: SchemaRefreshResponse,\n sent: Record<string, unknown>,\n edits: Iterable<string> = [],\n ): boolean {\n const merged = mergeRefreshResponse(this.state.components, sent, response)\n\n if (!merged) {\n this.store.update({ refreshing: false })\n\n return false\n }\n\n this.serverValues = merged.values\n this.fingerprints = merged.fingerprints\n\n this.store.set((state) => {\n let values = merged.values\n\n for (const editedPath of edits) {\n values = setPath(values, editedPath, getPath(state.values, editedPath))\n }\n\n return {\n ...state,\n components: merged.components,\n values,\n columns: merged.columns ?? state.columns,\n refreshing: false,\n }\n })\n\n return true\n }\n\n /**\n * Validate the fields of one wizard step (by the Step component's `key`) on the server.\n * Resolves with true when they pass; otherwise the step's errors replace the errors of\n * those fields and false is returned. Without a validation endpoint every step passes.\n */\n async validateStep(stepKey: string, fieldPaths: string[]): Promise<boolean> {\n if (!this.options.validateUrl) {\n return true\n }\n\n try {\n const response = await requestJson<StepValidationHaltedResponse | null>(\n this.options.validateUrl,\n {\n method: 'POST',\n body: {\n ...this.requestContext(),\n state: this.state.values,\n step: stepKey,\n },\n },\n )\n\n // A step hook halted: stay on the step, keep the field errors as they are.\n if (response !== null && typeof response === 'object' && response.halted) {\n const notifications = response.notifications ?? []\n\n if (notifications.length > 0) this.options.onNotifications?.(notifications)\n\n return false\n }\n } catch (error) {\n if (error instanceof HttpError && error.status === 422) {\n this.store.update({\n errors: { ...withoutStepErrors(this.state.errors, fieldPaths), ...error.errors },\n })\n\n return false\n }\n\n ;(this.options.onError ?? reportRequestError)(error)\n\n return false\n }\n\n this.store.update({ errors: withoutStepErrors(this.state.errors, fieldPaths) })\n\n return true\n }\n\n /** Search options for a searchable select at `path`. */\n async searchOptions(\n path: string,\n search: string,\n ): Promise<Array<{ value: string; label: string }>> {\n if (!this.options.optionsUrl) {\n return []\n }\n\n const response = await requestJson<{ options?: Array<{ value: string; label: string }> }>(\n this.options.optionsUrl,\n {\n query: { ...this.queryContext(), path, search },\n },\n )\n\n return response.options ?? []\n }\n\n async optionLabels(\n path: string,\n values: Array<string | number>,\n ): Promise<Record<string, string>> {\n if (!this.options.optionsUrl || values.length === 0) {\n return {}\n }\n\n const response = await requestJson<{ labels?: Record<string, string> }>(\n this.options.optionsUrl,\n {\n query: { ...this.queryContext(), path, values },\n },\n )\n\n return response.labels ?? {}\n }\n\n /** One page of icons for the IconPicker at `path` (`set`: one of its sets, or all). */\n async searchIcons(\n path: string,\n query: { search?: string; set?: string | null; page?: number },\n signal?: AbortSignal,\n ): Promise<IconPickerResponse | null> {\n if (!this.options.optionsUrl) {\n return null\n }\n\n return requestJson<IconPickerResponse>(this.options.optionsUrl, {\n query: {\n ...this.queryContext(),\n path,\n search: query.search ?? '',\n ...(query.set ? { set: query.set } : {}),\n page: query.page ?? 1,\n },\n signal,\n })\n }\n\n /**\n * The picker table of a ModalTableSelect at `path` (`query` is its table state) and the\n * labels of the records it lists.\n */\n async modalTable(\n path: string,\n identifier: string,\n query: TableQuery,\n signal?: AbortSignal,\n ): Promise<ModalTableSelectResponse | null> {\n if (!this.options.optionsUrl) {\n return null\n }\n\n return requestJson<ModalTableSelectResponse>(this.options.optionsUrl, {\n query: {\n ...this.queryContext(),\n path,\n table: 1,\n ...modalTableQuery(identifier, query),\n },\n signal,\n })\n }\n\n destroy(): void {\n debugRecorder.unregisterForm(this)\n this.refreshController?.abort()\n\n for (const debounced of this.liveDebouncers.values()) {\n debounced.cancel()\n }\n\n this.liveDebouncers.clear()\n }\n}\n\n/** The errors without those of the given fields (and of their nested paths). */\nfunction withoutStepErrors(\n errors: Record<string, string[]>,\n fieldPaths: string[],\n): Record<string, string[]> {\n const kept: Record<string, string[]> = {}\n\n for (const [key, messages] of Object.entries(errors)) {\n if (!fieldPaths.some((path) => key === path || key.startsWith(`${path}.`))) {\n kept[key] = messages\n }\n }\n\n return kept\n}\n\n/**\n * A stable key for a page form: the page class, record and the form's own key/component.\n * The context token is re-encrypted on every response, so it cannot identify a form.\n * Renderers re-initialise a FormStore from server state only when this key changes (or\n * after the form's own successful submit), never on unrelated reloads of the same page.\n */\nexport function formIdentity(\n page: { class?: string; record?: string | number | null } | null | undefined,\n block: { key: string; component: string; operation?: string | null },\n): string {\n return JSON.stringify([\n page?.class ?? null,\n page?.record ?? null,\n block.key,\n block.component,\n block.operation ?? null,\n ])\n}\n\n/** The parts of an Inertia visit the unsaved-changes guard needs. */\nexport interface GuardedVisit {\n method: string\n url: URL | string\n only?: string[]\n except?: string[]\n preserveState?: unknown\n}\n\n/**\n * True when an Inertia visit would navigate away from the current form, i.e. when the\n * unsaved-changes guard should ask. Partial reloads (`only`/`except`, used by table\n * sorting, paging and polling) and state-preserving visits to the same path stay on the\n * page and keep the form, so they never prompt. Non-GET visits are submissions.\n */\nexport function isLeavingVisit(visit: GuardedVisit, currentUrl?: string): boolean {\n if (visit.method.toLowerCase() !== 'get') {\n return false\n }\n\n if ((visit.only?.length ?? 0) > 0 || (visit.except?.length ?? 0) > 0) {\n return false\n }\n\n const base =\n currentUrl ?? (typeof window !== 'undefined' ? window.location.href : 'http://localhost/')\n const here = new URL(base)\n const target = new URL(String(visit.url), here)\n\n return !(\n visit.preserveState === true &&\n target.origin === here.origin &&\n target.pathname === here.pathname\n )\n}\n\n// ---------------------------------------------------------------- Repeater helpers\n\nexport interface RepeaterItemState {\n key: string\n state: Record<string, unknown>\n}\n\n/** Append a blank item using the Repeater's template state. */\nexport function appendRepeaterItem(\n items: unknown[],\n templateState: Record<string, unknown>,\n): unknown[] {\n return [...items, cloneState(templateState)]\n}\n\nexport function removeRepeaterItem(items: unknown[], index: number): unknown[] {\n return items.filter((_, i) => i !== index)\n}\n\nexport function moveRepeaterItem(items: unknown[], from: number, to: number): unknown[] {\n if (from === to || from < 0 || to < 0 || from >= items.length || to >= items.length) {\n return items\n }\n\n const copy = [...items]\n const [item] = copy.splice(from, 1)\n copy.splice(to, 0, item)\n\n return copy\n}\n\nexport function cloneRepeaterItem(items: unknown[], index: number): unknown[] {\n const copy = [...items]\n copy.splice(index + 1, 0, cloneState(items[index]))\n\n return copy\n}\n\n/**\n * Per-item UI state (collapsed flags, …) keyed by item index, remapped after the items\n * are reordered, removed or cloned with the helpers above so it stays with its item.\n */\nexport type IndexedState<T> = Record<number, T>\n\nfunction remapIndexedState<T>(\n state: IndexedState<T>,\n newIndex: (index: number) => number | null,\n): IndexedState<T> {\n const result: IndexedState<T> = {}\n\n for (const [key, value] of Object.entries(state) as [string, T][]) {\n const index = newIndex(Number(key))\n\n if (index !== null) result[index] = value\n }\n\n return result\n}\n\nexport function moveIndexedState<T>(\n state: IndexedState<T>,\n from: number,\n to: number,\n): IndexedState<T> {\n if (from === to) return state\n\n return remapIndexedState(state, (index) => {\n if (index === from) return to\n if (from < to && index > from && index <= to) return index - 1\n if (from > to && index >= to && index < from) return index + 1\n\n return index\n })\n}\n\nexport function removeIndexedState<T>(state: IndexedState<T>, removed: number): IndexedState<T> {\n return remapIndexedState(state, (index) =>\n index === removed ? null : index > removed ? index - 1 : index,\n )\n}\n\nexport function cloneIndexedState<T>(state: IndexedState<T>, cloned: number): IndexedState<T> {\n const result = remapIndexedState(state, (index) => (index > cloned ? index + 1 : index))\n\n if (cloned in state) result[cloned + 1] = state[cloned] as T\n\n return result\n}\n"],"mappings":"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"}
package/dist/i18n.d.ts CHANGED
@@ -97,5 +97,14 @@ export declare function showsLanguageSwitcher(localization: Pick<LocalizationSch
97
97
  export declare function collapsesLanguageSwitcher(localization: Pick<LocalizationSchema, 'switcher' | 'switcherPosition' | 'locales'> | null | undefined, hasUserMenu: boolean): boolean;
98
98
  /** Switch the panel language and reload the current page in it. */
99
99
  export declare function switchLocale(endpoint: string, locale: string): void;
100
+ /**
101
+ * The `dir` for a `<bdi>` around a display value: `auto` (the value's first strong letter
102
+ * decides) when it has right-to-left letters, else `ltr`. Numbers and money formatted for a
103
+ * right-to-left locale start with an invisible right-to-left mark (`‏0.00 US$` in Arabic),
104
+ * which `auto` reads as right to left and reorders into `$US 0.00`; without right-to-left
105
+ * letters the value is laid out as written. Values with them (`٠٫٠٠٠ د.ب.‏`, Arabic
106
+ * text) keep their own direction.
107
+ */
108
+ export declare function isolationDirection(value: unknown): 'ltr' | 'auto';
100
109
  //#endregion
101
110
  //# sourceMappingURL=i18n.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"i18n.d.ts","names":[],"sources":["../src/i18n.ts"],"mappings":";;KASK,eAAe;;;;;qBAMP;UAID;UACA;UAJF;EAGE,YAAA,eAAc,yBACd;EAGV,gBAAgB,cAAc;;EAK9B,UAAU;EAOV;EAIA,IAAI;;EAKJ,EAAE,aAAa,eAAc,cAAmB;;;;;;;EAahD,OAAO,aAAa,eAAe,eAAc,cAAmB;UAO5D;;;wBAQM,aAAa,cAAc,eAAe,OAAO,KAAK;qBAkDzD,YAAU;qBAEV,IAAC,aAhFC,eAAgB,cAAY;iBAoF1B,qBAAqB,KAAK;;EAEzC;;;;;;qBAOW;WAEA;WACA;WACA;EAFA,YAAA,iBACA,uBACA;SAGJ,WAAW,cAAc,iCAAiC;EAQjE,OAAO,eAAe,UAAS,KAAK;EAIpC,QAAQ,eAAe;EAIvB,MAAM,gBAAgB,mBAA8B,UAAS;;EAU7D,KACE,OAAO,wBACP,UAAS,KAAK;EAWhB,SAAS,OAAO;;EAKhB,SAAS,OAAO,wBAAwB,MAAK;EAyB7C,KAAK,iBAAiB,OAAM,KAAK;;EAKjC,gBAAgB;IACd;IACA;IACA;;;EAeF;IAAgB;IAAiB;;;;;;;;EAejC,YAAY;;;wBAuCE,iBAAiB;;wBAmBjB,cAAc;;;;;wBAiBd,cAAc,MAAM,MAAM,gBAAgB;;wBA0D1C,oBACd,cAAc,KAAK;;;;;wBAgBL,sBACd,cACE,KAAK,qFACP;;;;;;;wBAqBc,0BACd,cACE,KAAK,qFACP;;wBAMc,aAAa,kBAAkB"}
1
+ {"version":3,"file":"i18n.d.ts","names":[],"sources":["../src/i18n.ts"],"mappings":";;KASK,eAAe;;;;;qBAMP;UAID;UACA;UAJF;EAGE,YAAA,eAAc,yBACd;EAGV,gBAAgB,cAAc;;EAK9B,UAAU;EAOV;EAIA,IAAI;;EAKJ,EAAE,aAAa,eAAc,cAAmB;;;;;;;EAahD,OAAO,aAAa,eAAe,eAAc,cAAmB;UAO5D;;;wBAQM,aAAa,cAAc,eAAe,OAAO,KAAK;qBAkDzD,YAAU;qBAEV,IAAC,aAhFC,eAAgB,cAAY;iBAoF1B,qBAAqB,KAAK;;EAEzC;;;;;;qBAOW;WAEA;WACA;WACA;EAFA,YAAA,iBACA,uBACA;SAGJ,WAAW,cAAc,iCAAiC;EAQjE,OAAO,eAAe,UAAS,KAAK;EAIpC,QAAQ,eAAe;EAIvB,MAAM,gBAAgB,mBAA8B,UAAS;;EAU7D,KACE,OAAO,wBACP,UAAS,KAAK;EAWhB,SAAS,OAAO;;EAKhB,SAAS,OAAO,wBAAwB,MAAK;EAyB7C,KAAK,iBAAiB,OAAM,KAAK;;EAKjC,gBAAgB;IACd;IACA;IACA;;;EAeF;IAAgB;IAAiB;;;;;;;;EAejC,YAAY;;;wBAuCE,iBAAiB;;wBAmBjB,cAAc;;;;;wBAiBd,cAAc,MAAM,MAAM,gBAAgB;;wBA0D1C,oBACd,cAAc,KAAK;;;;;wBAgBL,sBACd,cACE,KAAK,qFACP;;;;;;;wBAqBc,0BACd,cACE,KAAK,qFACP;;wBAMc,aAAa,kBAAkB;;;;;;;;;wBAqB/B,mBAAmB"}
package/dist/i18n.js CHANGED
@@ -351,7 +351,20 @@ function switchLocale(endpoint, locale) {
351
351
  preserveState: false
352
352
  });
353
353
  }
354
+ /** A letter of a script written right to left (not its digits or separators). */
355
+ const RTL_LETTER = /(?=\p{L})(?:\p{Script=Arabic}|\p{Script=Hebrew}|\p{Script=Syriac}|\p{Script=Thaana}|\p{Script=Nko}|\p{Script=Adlam}|\p{Script=Samaritan}|\p{Script=Mandaic}|\p{Script=Hanifi_Rohingya}|\p{Script=Yezidi}|\p{Script=Mende_Kikakui})/u;
356
+ /**
357
+ * The `dir` for a `<bdi>` around a display value: `auto` (the value's first strong letter
358
+ * decides) when it has right-to-left letters, else `ltr`. Numbers and money formatted for a
359
+ * right-to-left locale start with an invisible right-to-left mark (`‏0.00 US$` in Arabic),
360
+ * which `auto` reads as right to left and reorders into `$US 0.00`; without right-to-left
361
+ * letters the value is laid out as written. Values with them (`٠٫٠٠٠ د.ب.‏`, Arabic
362
+ * text) keep their own direction.
363
+ */
364
+ function isolationDirection(value) {
365
+ return typeof value === "string" && RTL_LETTER.test(value) ? "auto" : "ltr";
366
+ }
354
367
  //#endregion
355
- export { LocaleFormatter, Translator, applyDocumentLocale, collapsesLanguageSwitcher, currencyDecimals, formatPhpDate, selectPlural, showsLanguageSwitcher, switchLocale, t, toAsciiDigits, translator };
368
+ export { LocaleFormatter, Translator, applyDocumentLocale, collapsesLanguageSwitcher, currencyDecimals, formatPhpDate, isolationDirection, selectPlural, showsLanguageSwitcher, switchLocale, t, toAsciiDigits, translator };
356
369
 
357
370
  //# sourceMappingURL=i18n.js.map
package/dist/i18n.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"i18n.js","names":[],"sources":["../src/i18n.ts"],"sourcesContent":["import { router } from '@inertiajs/core'\nimport { lazyComponentCache } from './lazy'\nimport type { LocalizationSchema } from './schema/panel'\nimport { interpolate } from './utils/format'\nimport { getPath } from './utils/path'\n\n/** CLDR plural categories in the order Laravel language files list their forms. */\nconst PLURAL_ORDER: Intl.LDMLPluralRule[] = ['zero', 'one', 'two', 'few', 'many', 'other']\n\ntype Replacements = Record<string, string | number>\n\n/**\n * Translator over the `translations` prop (Laravel's `alfakit::ui` group plus any groups\n * the panel adds with `frontendTranslations()`).\n */\nexport class Translator {\n private pluralRules: Intl.PluralRules | null = null\n\n constructor(\n private translations: Record<string, unknown> = {},\n private locale = 'en',\n ) {}\n\n setTranslations(translations: Record<string, unknown>): void {\n this.translations = translations\n }\n\n /** BCP 47 tag used for plural rules. */\n setLocale(locale: string): void {\n if (locale !== this.locale) {\n this.locale = locale\n this.pluralRules = null\n }\n }\n\n getLocale(): string {\n return this.locale\n }\n\n has(key: string): boolean {\n return typeof getPath<unknown>(this.translations, key) === 'string'\n }\n\n /** `t('table.showing', { first: 1, last: 10, total: 42 })` */\n t(key: string, replacements: Replacements = {}, fallback?: string): string {\n const value = getPath<unknown>(this.translations, key)\n const template = typeof value === 'string' ? value : (fallback ?? key)\n\n return interpolate(template, replacements)\n }\n\n /**\n * Pluralised line, compatible with Laravel's `trans_choice()` syntax:\n * `'{0} No items|{1} One item|[2,*] :count items'` or `'one item|:count items'`.\n * Forms without explicit ranges follow CLDR plural rules for the current locale, in the\n * order zero, one, two, few, many, other (as Laravel language files list them).\n */\n choice(key: string, count: number, replacements: Replacements = {}, fallback?: string): string {\n const value = getPath<unknown>(this.translations, key)\n const line = typeof value === 'string' ? value : (fallback ?? key)\n\n return interpolate(selectPlural(line, count, this.rules()), { count, ...replacements })\n }\n\n private rules(): Intl.PluralRules {\n this.pluralRules ??= new Intl.PluralRules(this.locale)\n\n return this.pluralRules\n }\n}\n\n/** Pick the plural form of a Laravel `trans_choice` line for `count`. */\nexport function selectPlural(line: string, count: number, rules: Intl.PluralRules): string {\n const segments = line.split('|')\n\n for (const segment of segments) {\n const explicit = matchExplicit(segment, count)\n\n if (explicit !== null) {\n return explicit\n }\n }\n\n const forms = segments.map((segment) => segment.replace(/^\\s*[{[][^\\]}]*[}\\]]\\s*/, ''))\n\n if (forms.length === 1) {\n return forms[0] ?? ''\n }\n\n const categories = PLURAL_ORDER.filter((category) =>\n rules.resolvedOptions().pluralCategories.includes(category),\n )\n const index = Math.max(0, categories.indexOf(rules.select(count)))\n\n return forms[Math.min(index, forms.length - 1)] ?? ''\n}\n\nfunction matchExplicit(segment: string, count: number): string | null {\n const match = /^\\s*([{[])([^\\]}]*)([}\\]])\\s*([\\s\\S]*)$/.exec(segment)\n\n if (!match) {\n return null\n }\n\n const [, open, condition = '', , text = ''] = match\n\n if (open === '{') {\n return condition\n .split(',')\n .map((part) => Number(part.trim()))\n .includes(count)\n ? text\n : null\n }\n\n const [from = '*', to = '*'] = condition.split(',').map((part) => part.trim())\n const above = from === '*' || count >= Number(from)\n const below = to === '*' || count <= Number(to)\n\n return above && below ? text : null\n}\n\nexport const translator = new Translator()\n\nexport const t = translator.t.bind(translator)\n\n// ---------------------------------------------------------------------------- Formatting\n\nexport interface MoneyOptions extends Intl.NumberFormatOptions {\n /** The amount is an integer of the smallest unit (cents, fils, yen). */\n minorUnits?: boolean\n}\n\n/**\n * Locale-aware formatting for values rendered in the browser (custom components,\n * charts, inputs). Server-rendered columns and entries are already formatted in PHP.\n */\nexport class LocaleFormatter {\n constructor(\n readonly locale = 'en',\n readonly currencyCode = 'USD',\n readonly timeZone?: string,\n ) {}\n\n static fromSchema(localization: LocalizationSchema | undefined): LocaleFormatter {\n return new LocaleFormatter(\n localization?.tag ?? 'en',\n localization?.currency ?? 'USD',\n localization?.timezone,\n )\n }\n\n number(value: number, options: Intl.NumberFormatOptions = {}): string {\n return new Intl.NumberFormat(this.locale, options).format(value)\n }\n\n percent(value: number, maximumFractionDigits = 0): string {\n return this.number(value, { style: 'percent', maximumFractionDigits })\n }\n\n money(amount: number, currency = this.currencyCode, options: MoneyOptions = {}): string {\n const { minorUnits, ...format } = options\n const value = minorUnits ? amount / 10 ** currencyDecimals(currency) : amount\n\n return new Intl.NumberFormat(this.locale, { style: 'currency', currency, ...format }).format(\n value,\n )\n }\n\n /** Unparseable input is returned unchanged. */\n date(\n value: Date | string | number,\n options: Intl.DateTimeFormatOptions = { dateStyle: 'medium' },\n ): string {\n const date = toDate(value)\n\n if (Number.isNaN(date.getTime())) return String(value)\n\n return new Intl.DateTimeFormat(this.locale, { timeZone: this.timeZone, ...options }).format(\n date,\n )\n }\n\n dateTime(value: Date | string | number): string {\n return this.date(value, { dateStyle: 'medium', timeStyle: 'short' })\n }\n\n /** \"3 days ago\", \"in 2 hours\", in the user's language. */\n relative(value: Date | string | number, now: Date = new Date()): string {\n const time = toDate(value).getTime()\n\n if (Number.isNaN(time)) return String(value)\n\n const seconds = Math.round((time - now.getTime()) / 1000)\n const units: Array<[Intl.RelativeTimeFormatUnit, number]> = [\n ['year', 31_536_000],\n ['month', 2_592_000],\n ['week', 604_800],\n ['day', 86_400],\n ['hour', 3_600],\n ['minute', 60],\n ]\n const formatter = new Intl.RelativeTimeFormat(this.locale, { numeric: 'auto' })\n\n for (const [unit, size] of units) {\n if (Math.abs(seconds) >= size) {\n return formatter.format(Math.round(seconds / size), unit)\n }\n }\n\n return formatter.format(seconds, 'second')\n }\n\n list(items: string[], type: Intl.ListFormatType = 'conjunction'): string {\n return new Intl.ListFormat(this.locale, { type }).format(items)\n }\n\n /** Where the locale writes the currency symbol for `currency`. */\n currencyAffixes(currency = this.currencyCode): {\n prefix: string\n suffix: string\n symbol: string\n } {\n const parts = new Intl.NumberFormat(this.locale, { style: 'currency', currency }).formatToParts(\n 1,\n )\n const symbolIndex = parts.findIndex((part) => part.type === 'currency')\n const numberIndex = parts.findIndex((part) => part.type === 'integer')\n const symbol = parts[symbolIndex]?.value ?? currency\n\n return symbolIndex < numberIndex\n ? { prefix: symbol, suffix: '', symbol }\n : { prefix: '', suffix: symbol, symbol }\n }\n\n /** The locale's decimal and grouping separators. */\n separators(): { decimal: string; group: string } {\n const parts = new Intl.NumberFormat(this.locale).formatToParts(12345.6)\n\n return {\n decimal: parts.find((part) => part.type === 'decimal')?.value ?? '.',\n group: parts.find((part) => part.type === 'group')?.value ?? ',',\n }\n }\n\n /**\n * Parse what a user typed in their locale (\"1.234,5\" in German, \"١٬٢٣٤٫٥\" in Arabic,\n * \"1 234,5\" in French) into a number. A lone dot followed by other than three digits is\n * read as a decimal point even where the locale uses a comma, since that is what people\n * mean when they type \"12.5\". Returns null for empty or invalid input.\n */\n parseNumber(input: string): number | null {\n const { decimal } = this.separators()\n let value = toAsciiDigits(input)\n .replace(/[\\s\\u00a0\\u202f'’]/g, '')\n .replace(/٫/g, decimal)\n .replace(/[٬،]/g, decimal === ',' ? '.' : ',')\n\n // Locales with their own separator (Arabic \"٫\"): ASCII \".\" is the decimal point.\n if (decimal !== '.' && decimal !== ',') {\n value = value.replace(/,/g, '').replace('.', decimal)\n }\n\n if (decimal === ',' && !value.includes(decimal) && /^-?\\d*\\.\\d*$/.test(value)) {\n const fraction = value.split('.')[1] ?? ''\n\n if (fraction.length !== 3) {\n value = value.replace('.', decimal)\n }\n }\n\n const [integer = '', fraction, ...rest] = value.split(decimal)\n\n if (rest.length > 0) {\n return null\n }\n\n const normalised = integer.replace(/[.,]/g, '') + (fraction === undefined ? '' : `.${fraction}`)\n\n if (!/^-?(\\d+\\.?\\d*|\\.\\d+)$/.test(normalised)) {\n return null\n }\n\n const number = Number(normalised)\n\n return Number.isFinite(number) ? number : null\n }\n}\n\n/** Minor-unit digits of an ISO 4217 currency (USD 2, BHD 3, JPY 0). */\nexport function currencyDecimals(currency: string): number {\n try {\n return (\n new Intl.NumberFormat('en', { style: 'currency', currency }).resolvedOptions()\n .maximumFractionDigits ?? 2\n )\n } catch {\n return 2\n }\n}\n\n/** Code points of \"0\" in Unicode decimal digit blocks (Arabic-Indic, Persian, Devanagari, …). */\nconst DIGIT_ZEROS = [\n 0x660, 0x6f0, 0x7c0, 0x966, 0x9e6, 0xa66, 0xae6, 0xb66, 0xbe6, 0xc66, 0xce6, 0xd66, 0xde6, 0xe50,\n 0xed0, 0xf20, 0x1040, 0x1090, 0x17e0, 0x1810, 0x1946, 0x19d0, 0x1a80, 0x1a90, 0x1b50, 0x1bb0,\n 0x1c40, 0x1c50, 0xa620, 0xa8d0, 0xa900, 0xa9d0, 0xa9f0, 0xaa50, 0xabf0, 0xff10,\n]\n\n/** Convert Arabic-Indic, Persian and other Unicode decimal digits to 0-9. */\nexport function toAsciiDigits(input: string): string {\n return input.replace(/\\P{ASCII}/gu, (character) => {\n const code = character.codePointAt(0) ?? 0\n const zero = DIGIT_ZEROS.find((start) => code >= start && code <= start + 9)\n\n return zero === undefined ? character : String(code - zero)\n })\n}\n\nfunction toDate(value: Date | string | number): Date {\n return value instanceof Date ? value : new Date(value)\n}\n\n/**\n * Render a date with a PHP `date()` format string, using the locale's month and weekday\n * names. Numeric tokens keep ASCII digits, like Carbon's `translatedFormat()`.\n */\nexport function formatPhpDate(date: Date, format: string, locale = 'en'): string {\n const name = (options: Intl.DateTimeFormatOptions) =>\n new Intl.DateTimeFormat(locale, options).format(date)\n const pad = (value: number) => String(value).padStart(2, '0')\n const hours = date.getHours()\n const dayPeriod = () =>\n new Intl.DateTimeFormat(locale, { hour: 'numeric', hour12: true })\n .formatToParts(date)\n .find((part) => part.type === 'dayPeriod')?.value ?? (hours < 12 ? 'AM' : 'PM')\n\n const tokens: Record<string, () => string> = {\n d: () => pad(date.getDate()),\n j: () => String(date.getDate()),\n D: () => name({ weekday: 'short' }),\n l: () => name({ weekday: 'long' }),\n N: () => String(date.getDay() === 0 ? 7 : date.getDay()),\n w: () => String(date.getDay()),\n S: () => (locale.startsWith('en') ? ordinalSuffix(date.getDate()) : ''),\n F: () => name({ month: 'long' }),\n M: () => name({ month: 'short' }),\n m: () => pad(date.getMonth() + 1),\n n: () => String(date.getMonth() + 1),\n Y: () => String(date.getFullYear()),\n y: () => String(date.getFullYear()).slice(-2),\n a: () => dayPeriod().toLowerCase(),\n A: () => dayPeriod(),\n g: () => String(hours % 12 || 12),\n G: () => String(hours),\n h: () => pad(hours % 12 || 12),\n H: () => pad(hours),\n i: () => pad(date.getMinutes()),\n s: () => pad(date.getSeconds()),\n }\n\n let output = ''\n\n for (let index = 0; index < format.length; index++) {\n const character = format[index] ?? ''\n\n if (character === '\\\\') {\n output += format[++index] ?? ''\n } else {\n output += tokens[character]?.() ?? character\n }\n }\n\n return output\n}\n\nfunction ordinalSuffix(day: number): string {\n if (day % 100 >= 11 && day % 100 <= 13) return 'th'\n\n return ({ 1: 'st', 2: 'nd', 3: 'rd' } as Record<number, string>)[day % 10] ?? 'th'\n}\n\n// ---------------------------------------------------------------------------- Document\n\n/** Keep `<html lang dir>` in sync after Inertia navigations (e.g. a language switch). */\nexport function applyDocumentLocale(\n localization: Pick<LocalizationSchema, 'tag' | 'direction'> | undefined,\n): void {\n if (typeof document === 'undefined' || !localization) {\n return\n }\n\n const root = document.documentElement\n\n if (root.lang !== localization.tag) root.lang = localization.tag\n if (root.dir !== localization.direction) root.dir = localization.direction\n}\n\n/**\n * Whether a language menu belongs in `place`: the panel has a switcher with two or more\n * languages and `languageSwitcher()` puts it there. Auth pages always show it.\n */\nexport function showsLanguageSwitcher(\n localization:\n Pick<LocalizationSchema, 'switcher' | 'switcherPosition' | 'locales'> | null | undefined,\n place: 'topbar' | 'user-menu' | 'auth',\n): boolean {\n if (!localization?.switcher || (localization.locales?.length ?? 0) < 2) {\n return false\n }\n\n if (place === 'auth') {\n return true\n }\n\n const position = localization.switcherPosition ?? 'user-menu'\n\n return position === 'both' || position === place\n}\n\n/**\n * Whether the top bar's language button collapses into the user menu on phones (below\n * `sm`): with `languageSwitcher('topbar' | 'both')` and a user menu to hold it. The top bar\n * then hides its button below `sm`, and the user menu shows its \"Language\" entry there\n * (always, for `'both'`). Without a user menu (guests, `userMenu(false)`) it stays put.\n */\nexport function collapsesLanguageSwitcher(\n localization:\n Pick<LocalizationSchema, 'switcher' | 'switcherPosition' | 'locales'> | null | undefined,\n hasUserMenu: boolean,\n): boolean {\n return hasUserMenu && showsLanguageSwitcher(localization, 'topbar')\n}\n\n/** Switch the panel language and reload the current page in it. */\nexport function switchLocale(endpoint: string, locale: string): void {\n // Lazy widgets cached in the old language would otherwise show until their deferred\n // schemas arrive in the new one.\n lazyComponentCache.clear()\n router.post(endpoint, { locale }, { preserveScroll: true, preserveState: false })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"i18n.js","names":[],"sources":["../src/i18n.ts"],"sourcesContent":["import { router } from '@inertiajs/core'\nimport { lazyComponentCache } from './lazy'\nimport type { LocalizationSchema } from './schema/panel'\nimport { interpolate } from './utils/format'\nimport { getPath } from './utils/path'\n\n/** CLDR plural categories in the order Laravel language files list their forms. */\nconst PLURAL_ORDER: Intl.LDMLPluralRule[] = ['zero', 'one', 'two', 'few', 'many', 'other']\n\ntype Replacements = Record<string, string | number>\n\n/**\n * Translator over the `translations` prop (Laravel's `alfakit::ui` group plus any groups\n * the panel adds with `frontendTranslations()`).\n */\nexport class Translator {\n private pluralRules: Intl.PluralRules | null = null\n\n constructor(\n private translations: Record<string, unknown> = {},\n private locale = 'en',\n ) {}\n\n setTranslations(translations: Record<string, unknown>): void {\n this.translations = translations\n }\n\n /** BCP 47 tag used for plural rules. */\n setLocale(locale: string): void {\n if (locale !== this.locale) {\n this.locale = locale\n this.pluralRules = null\n }\n }\n\n getLocale(): string {\n return this.locale\n }\n\n has(key: string): boolean {\n return typeof getPath<unknown>(this.translations, key) === 'string'\n }\n\n /** `t('table.showing', { first: 1, last: 10, total: 42 })` */\n t(key: string, replacements: Replacements = {}, fallback?: string): string {\n const value = getPath<unknown>(this.translations, key)\n const template = typeof value === 'string' ? value : (fallback ?? key)\n\n return interpolate(template, replacements)\n }\n\n /**\n * Pluralised line, compatible with Laravel's `trans_choice()` syntax:\n * `'{0} No items|{1} One item|[2,*] :count items'` or `'one item|:count items'`.\n * Forms without explicit ranges follow CLDR plural rules for the current locale, in the\n * order zero, one, two, few, many, other (as Laravel language files list them).\n */\n choice(key: string, count: number, replacements: Replacements = {}, fallback?: string): string {\n const value = getPath<unknown>(this.translations, key)\n const line = typeof value === 'string' ? value : (fallback ?? key)\n\n return interpolate(selectPlural(line, count, this.rules()), { count, ...replacements })\n }\n\n private rules(): Intl.PluralRules {\n this.pluralRules ??= new Intl.PluralRules(this.locale)\n\n return this.pluralRules\n }\n}\n\n/** Pick the plural form of a Laravel `trans_choice` line for `count`. */\nexport function selectPlural(line: string, count: number, rules: Intl.PluralRules): string {\n const segments = line.split('|')\n\n for (const segment of segments) {\n const explicit = matchExplicit(segment, count)\n\n if (explicit !== null) {\n return explicit\n }\n }\n\n const forms = segments.map((segment) => segment.replace(/^\\s*[{[][^\\]}]*[}\\]]\\s*/, ''))\n\n if (forms.length === 1) {\n return forms[0] ?? ''\n }\n\n const categories = PLURAL_ORDER.filter((category) =>\n rules.resolvedOptions().pluralCategories.includes(category),\n )\n const index = Math.max(0, categories.indexOf(rules.select(count)))\n\n return forms[Math.min(index, forms.length - 1)] ?? ''\n}\n\nfunction matchExplicit(segment: string, count: number): string | null {\n const match = /^\\s*([{[])([^\\]}]*)([}\\]])\\s*([\\s\\S]*)$/.exec(segment)\n\n if (!match) {\n return null\n }\n\n const [, open, condition = '', , text = ''] = match\n\n if (open === '{') {\n return condition\n .split(',')\n .map((part) => Number(part.trim()))\n .includes(count)\n ? text\n : null\n }\n\n const [from = '*', to = '*'] = condition.split(',').map((part) => part.trim())\n const above = from === '*' || count >= Number(from)\n const below = to === '*' || count <= Number(to)\n\n return above && below ? text : null\n}\n\nexport const translator = new Translator()\n\nexport const t = translator.t.bind(translator)\n\n// ---------------------------------------------------------------------------- Formatting\n\nexport interface MoneyOptions extends Intl.NumberFormatOptions {\n /** The amount is an integer of the smallest unit (cents, fils, yen). */\n minorUnits?: boolean\n}\n\n/**\n * Locale-aware formatting for values rendered in the browser (custom components,\n * charts, inputs). Server-rendered columns and entries are already formatted in PHP.\n */\nexport class LocaleFormatter {\n constructor(\n readonly locale = 'en',\n readonly currencyCode = 'USD',\n readonly timeZone?: string,\n ) {}\n\n static fromSchema(localization: LocalizationSchema | undefined): LocaleFormatter {\n return new LocaleFormatter(\n localization?.tag ?? 'en',\n localization?.currency ?? 'USD',\n localization?.timezone,\n )\n }\n\n number(value: number, options: Intl.NumberFormatOptions = {}): string {\n return new Intl.NumberFormat(this.locale, options).format(value)\n }\n\n percent(value: number, maximumFractionDigits = 0): string {\n return this.number(value, { style: 'percent', maximumFractionDigits })\n }\n\n money(amount: number, currency = this.currencyCode, options: MoneyOptions = {}): string {\n const { minorUnits, ...format } = options\n const value = minorUnits ? amount / 10 ** currencyDecimals(currency) : amount\n\n return new Intl.NumberFormat(this.locale, { style: 'currency', currency, ...format }).format(\n value,\n )\n }\n\n /** Unparseable input is returned unchanged. */\n date(\n value: Date | string | number,\n options: Intl.DateTimeFormatOptions = { dateStyle: 'medium' },\n ): string {\n const date = toDate(value)\n\n if (Number.isNaN(date.getTime())) return String(value)\n\n return new Intl.DateTimeFormat(this.locale, { timeZone: this.timeZone, ...options }).format(\n date,\n )\n }\n\n dateTime(value: Date | string | number): string {\n return this.date(value, { dateStyle: 'medium', timeStyle: 'short' })\n }\n\n /** \"3 days ago\", \"in 2 hours\", in the user's language. */\n relative(value: Date | string | number, now: Date = new Date()): string {\n const time = toDate(value).getTime()\n\n if (Number.isNaN(time)) return String(value)\n\n const seconds = Math.round((time - now.getTime()) / 1000)\n const units: Array<[Intl.RelativeTimeFormatUnit, number]> = [\n ['year', 31_536_000],\n ['month', 2_592_000],\n ['week', 604_800],\n ['day', 86_400],\n ['hour', 3_600],\n ['minute', 60],\n ]\n const formatter = new Intl.RelativeTimeFormat(this.locale, { numeric: 'auto' })\n\n for (const [unit, size] of units) {\n if (Math.abs(seconds) >= size) {\n return formatter.format(Math.round(seconds / size), unit)\n }\n }\n\n return formatter.format(seconds, 'second')\n }\n\n list(items: string[], type: Intl.ListFormatType = 'conjunction'): string {\n return new Intl.ListFormat(this.locale, { type }).format(items)\n }\n\n /** Where the locale writes the currency symbol for `currency`. */\n currencyAffixes(currency = this.currencyCode): {\n prefix: string\n suffix: string\n symbol: string\n } {\n const parts = new Intl.NumberFormat(this.locale, { style: 'currency', currency }).formatToParts(\n 1,\n )\n const symbolIndex = parts.findIndex((part) => part.type === 'currency')\n const numberIndex = parts.findIndex((part) => part.type === 'integer')\n const symbol = parts[symbolIndex]?.value ?? currency\n\n return symbolIndex < numberIndex\n ? { prefix: symbol, suffix: '', symbol }\n : { prefix: '', suffix: symbol, symbol }\n }\n\n /** The locale's decimal and grouping separators. */\n separators(): { decimal: string; group: string } {\n const parts = new Intl.NumberFormat(this.locale).formatToParts(12345.6)\n\n return {\n decimal: parts.find((part) => part.type === 'decimal')?.value ?? '.',\n group: parts.find((part) => part.type === 'group')?.value ?? ',',\n }\n }\n\n /**\n * Parse what a user typed in their locale (\"1.234,5\" in German, \"١٬٢٣٤٫٥\" in Arabic,\n * \"1 234,5\" in French) into a number. A lone dot followed by other than three digits is\n * read as a decimal point even where the locale uses a comma, since that is what people\n * mean when they type \"12.5\". Returns null for empty or invalid input.\n */\n parseNumber(input: string): number | null {\n const { decimal } = this.separators()\n let value = toAsciiDigits(input)\n .replace(/[\\s\\u00a0\\u202f'’]/g, '')\n .replace(/٫/g, decimal)\n .replace(/[٬،]/g, decimal === ',' ? '.' : ',')\n\n // Locales with their own separator (Arabic \"٫\"): ASCII \".\" is the decimal point.\n if (decimal !== '.' && decimal !== ',') {\n value = value.replace(/,/g, '').replace('.', decimal)\n }\n\n if (decimal === ',' && !value.includes(decimal) && /^-?\\d*\\.\\d*$/.test(value)) {\n const fraction = value.split('.')[1] ?? ''\n\n if (fraction.length !== 3) {\n value = value.replace('.', decimal)\n }\n }\n\n const [integer = '', fraction, ...rest] = value.split(decimal)\n\n if (rest.length > 0) {\n return null\n }\n\n const normalised = integer.replace(/[.,]/g, '') + (fraction === undefined ? '' : `.${fraction}`)\n\n if (!/^-?(\\d+\\.?\\d*|\\.\\d+)$/.test(normalised)) {\n return null\n }\n\n const number = Number(normalised)\n\n return Number.isFinite(number) ? number : null\n }\n}\n\n/** Minor-unit digits of an ISO 4217 currency (USD 2, BHD 3, JPY 0). */\nexport function currencyDecimals(currency: string): number {\n try {\n return (\n new Intl.NumberFormat('en', { style: 'currency', currency }).resolvedOptions()\n .maximumFractionDigits ?? 2\n )\n } catch {\n return 2\n }\n}\n\n/** Code points of \"0\" in Unicode decimal digit blocks (Arabic-Indic, Persian, Devanagari, …). */\nconst DIGIT_ZEROS = [\n 0x660, 0x6f0, 0x7c0, 0x966, 0x9e6, 0xa66, 0xae6, 0xb66, 0xbe6, 0xc66, 0xce6, 0xd66, 0xde6, 0xe50,\n 0xed0, 0xf20, 0x1040, 0x1090, 0x17e0, 0x1810, 0x1946, 0x19d0, 0x1a80, 0x1a90, 0x1b50, 0x1bb0,\n 0x1c40, 0x1c50, 0xa620, 0xa8d0, 0xa900, 0xa9d0, 0xa9f0, 0xaa50, 0xabf0, 0xff10,\n]\n\n/** Convert Arabic-Indic, Persian and other Unicode decimal digits to 0-9. */\nexport function toAsciiDigits(input: string): string {\n return input.replace(/\\P{ASCII}/gu, (character) => {\n const code = character.codePointAt(0) ?? 0\n const zero = DIGIT_ZEROS.find((start) => code >= start && code <= start + 9)\n\n return zero === undefined ? character : String(code - zero)\n })\n}\n\nfunction toDate(value: Date | string | number): Date {\n return value instanceof Date ? value : new Date(value)\n}\n\n/**\n * Render a date with a PHP `date()` format string, using the locale's month and weekday\n * names. Numeric tokens keep ASCII digits, like Carbon's `translatedFormat()`.\n */\nexport function formatPhpDate(date: Date, format: string, locale = 'en'): string {\n const name = (options: Intl.DateTimeFormatOptions) =>\n new Intl.DateTimeFormat(locale, options).format(date)\n const pad = (value: number) => String(value).padStart(2, '0')\n const hours = date.getHours()\n const dayPeriod = () =>\n new Intl.DateTimeFormat(locale, { hour: 'numeric', hour12: true })\n .formatToParts(date)\n .find((part) => part.type === 'dayPeriod')?.value ?? (hours < 12 ? 'AM' : 'PM')\n\n const tokens: Record<string, () => string> = {\n d: () => pad(date.getDate()),\n j: () => String(date.getDate()),\n D: () => name({ weekday: 'short' }),\n l: () => name({ weekday: 'long' }),\n N: () => String(date.getDay() === 0 ? 7 : date.getDay()),\n w: () => String(date.getDay()),\n S: () => (locale.startsWith('en') ? ordinalSuffix(date.getDate()) : ''),\n F: () => name({ month: 'long' }),\n M: () => name({ month: 'short' }),\n m: () => pad(date.getMonth() + 1),\n n: () => String(date.getMonth() + 1),\n Y: () => String(date.getFullYear()),\n y: () => String(date.getFullYear()).slice(-2),\n a: () => dayPeriod().toLowerCase(),\n A: () => dayPeriod(),\n g: () => String(hours % 12 || 12),\n G: () => String(hours),\n h: () => pad(hours % 12 || 12),\n H: () => pad(hours),\n i: () => pad(date.getMinutes()),\n s: () => pad(date.getSeconds()),\n }\n\n let output = ''\n\n for (let index = 0; index < format.length; index++) {\n const character = format[index] ?? ''\n\n if (character === '\\\\') {\n output += format[++index] ?? ''\n } else {\n output += tokens[character]?.() ?? character\n }\n }\n\n return output\n}\n\nfunction ordinalSuffix(day: number): string {\n if (day % 100 >= 11 && day % 100 <= 13) return 'th'\n\n return ({ 1: 'st', 2: 'nd', 3: 'rd' } as Record<number, string>)[day % 10] ?? 'th'\n}\n\n// ---------------------------------------------------------------------------- Document\n\n/** Keep `<html lang dir>` in sync after Inertia navigations (e.g. a language switch). */\nexport function applyDocumentLocale(\n localization: Pick<LocalizationSchema, 'tag' | 'direction'> | undefined,\n): void {\n if (typeof document === 'undefined' || !localization) {\n return\n }\n\n const root = document.documentElement\n\n if (root.lang !== localization.tag) root.lang = localization.tag\n if (root.dir !== localization.direction) root.dir = localization.direction\n}\n\n/**\n * Whether a language menu belongs in `place`: the panel has a switcher with two or more\n * languages and `languageSwitcher()` puts it there. Auth pages always show it.\n */\nexport function showsLanguageSwitcher(\n localization:\n Pick<LocalizationSchema, 'switcher' | 'switcherPosition' | 'locales'> | null | undefined,\n place: 'topbar' | 'user-menu' | 'auth',\n): boolean {\n if (!localization?.switcher || (localization.locales?.length ?? 0) < 2) {\n return false\n }\n\n if (place === 'auth') {\n return true\n }\n\n const position = localization.switcherPosition ?? 'user-menu'\n\n return position === 'both' || position === place\n}\n\n/**\n * Whether the top bar's language button collapses into the user menu on phones (below\n * `sm`): with `languageSwitcher('topbar' | 'both')` and a user menu to hold it. The top bar\n * then hides its button below `sm`, and the user menu shows its \"Language\" entry there\n * (always, for `'both'`). Without a user menu (guests, `userMenu(false)`) it stays put.\n */\nexport function collapsesLanguageSwitcher(\n localization:\n Pick<LocalizationSchema, 'switcher' | 'switcherPosition' | 'locales'> | null | undefined,\n hasUserMenu: boolean,\n): boolean {\n return hasUserMenu && showsLanguageSwitcher(localization, 'topbar')\n}\n\n/** Switch the panel language and reload the current page in it. */\nexport function switchLocale(endpoint: string, locale: string): void {\n // Lazy widgets cached in the old language would otherwise show until their deferred\n // schemas arrive in the new one.\n lazyComponentCache.clear()\n router.post(endpoint, { locale }, { preserveScroll: true, preserveState: false })\n}\n\n// ---------------------------------------------------------------------------- Bidi\n\n/** A letter of a script written right to left (not its digits or separators). */\nconst RTL_LETTER =\n /(?=\\p{L})(?:\\p{Script=Arabic}|\\p{Script=Hebrew}|\\p{Script=Syriac}|\\p{Script=Thaana}|\\p{Script=Nko}|\\p{Script=Adlam}|\\p{Script=Samaritan}|\\p{Script=Mandaic}|\\p{Script=Hanifi_Rohingya}|\\p{Script=Yezidi}|\\p{Script=Mende_Kikakui})/u\n\n/**\n * The `dir` for a `<bdi>` around a display value: `auto` (the value's first strong letter\n * decides) when it has right-to-left letters, else `ltr`. Numbers and money formatted for a\n * right-to-left locale start with an invisible right-to-left mark (`‏0.00 US$` in Arabic),\n * which `auto` reads as right to left and reorders into `$US 0.00`; without right-to-left\n * letters the value is laid out as written. Values with them (`٠٫٠٠٠ د.ب.‏`, Arabic\n * text) keep their own direction.\n */\nexport function isolationDirection(value: unknown): 'ltr' | 'auto' {\n return typeof value === 'string' && RTL_LETTER.test(value) ? 'auto' : 'ltr'\n}\n"],"mappings":"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"}