@krizaka/ui 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +1 -1
  2. package/dist/form/server.d.ts +96 -0
  3. package/dist/form/server.js +82 -0
  4. package/dist/form/server.js.map +1 -0
  5. package/dist/form.d.ts +503 -0
  6. package/dist/form.js +760 -0
  7. package/dist/form.js.map +1 -0
  8. package/dist/native/form.d.ts +376 -0
  9. package/dist/native/form.js +650 -0
  10. package/dist/native/form.js.map +1 -0
  11. package/dist/native.d.ts +113 -2
  12. package/dist/native.js +618 -388
  13. package/dist/native.js.map +1 -1
  14. package/package.json +48 -5
  15. package/registry/achievement.json +1 -1
  16. package/registry/action-bar.json +1 -1
  17. package/registry/alert.json +1 -1
  18. package/registry/app-shell.json +1 -1
  19. package/registry/argument-card.json +1 -1
  20. package/registry/audio-player.json +1 -1
  21. package/registry/auth-layout.json +1 -1
  22. package/registry/avatar.json +1 -1
  23. package/registry/badge.json +1 -1
  24. package/registry/button.json +1 -1
  25. package/registry/card-grid.json +1 -1
  26. package/registry/card.json +1 -1
  27. package/registry/catalogue-page.json +1 -1
  28. package/registry/changelog-page.json +1 -1
  29. package/registry/checkbox.json +89 -4
  30. package/registry/chip-link.json +1 -1
  31. package/registry/chip.json +1 -1
  32. package/registry/choice-grid.json +1 -1
  33. package/registry/code-block.json +1 -1
  34. package/registry/combobox.json +1 -1
  35. package/registry/command.json +1 -1
  36. package/registry/component-page.json +1 -1
  37. package/registry/confirm-button.json +1 -1
  38. package/registry/contact-page.json +1 -1
  39. package/registry/continue-prompt.json +1 -1
  40. package/registry/countdown.json +1 -1
  41. package/registry/cta-panel.json +1 -1
  42. package/registry/dashboard-layout.json +1 -1
  43. package/registry/data-table.json +1 -1
  44. package/registry/date-picker.json +142 -4
  45. package/registry/decision-list.json +1 -1
  46. package/registry/detail-layout.json +1 -1
  47. package/registry/dialog.json +1 -1
  48. package/registry/docs-article-page.json +1 -1
  49. package/registry/docs-hub-page.json +1 -1
  50. package/registry/docs-layout.json +1 -1
  51. package/registry/dropdown-menu.json +1 -1
  52. package/registry/empty-state.json +1 -1
  53. package/registry/error-page.json +1 -1
  54. package/registry/examples/checkbox/native/terms.d.ts +4 -0
  55. package/registry/examples/checkbox/native/terms.dark.png +0 -0
  56. package/registry/examples/checkbox/native/terms.js +26 -0
  57. package/registry/examples/checkbox/native/terms.light.png +0 -0
  58. package/registry/examples/checkbox/native/terms.tsx +28 -0
  59. package/registry/examples/date-picker/native/birth-date.d.ts +4 -0
  60. package/registry/examples/date-picker/native/birth-date.dark.png +0 -0
  61. package/registry/examples/date-picker/native/birth-date.js +30 -0
  62. package/registry/examples/date-picker/native/birth-date.light.png +0 -0
  63. package/registry/examples/date-picker/native/birth-date.tsx +27 -0
  64. package/registry/examples/field/native/default.d.ts +4 -0
  65. package/registry/examples/field/native/default.dark.png +0 -0
  66. package/registry/examples/field/native/default.js +29 -0
  67. package/registry/examples/field/native/default.light.png +0 -0
  68. package/registry/examples/field/native/default.tsx +23 -0
  69. package/registry/examples/form/amount.d.ts +4 -0
  70. package/registry/examples/form/amount.js +30 -0
  71. package/registry/examples/form/amount.tsx +36 -0
  72. package/registry/examples/form/audience.d.ts +4 -0
  73. package/registry/examples/form/audience.js +29 -0
  74. package/registry/examples/form/audience.tsx +31 -0
  75. package/registry/examples/form/native/amount.d.ts +4 -0
  76. package/registry/examples/form/native/amount.dark.png +0 -0
  77. package/registry/examples/form/native/amount.js +26 -0
  78. package/registry/examples/form/native/amount.light.png +0 -0
  79. package/registry/examples/form/native/amount.tsx +30 -0
  80. package/registry/examples/form/native/audience.d.ts +4 -0
  81. package/registry/examples/form/native/audience.dark.png +0 -0
  82. package/registry/examples/form/native/audience.js +24 -0
  83. package/registry/examples/form/native/audience.light.png +0 -0
  84. package/registry/examples/form/native/audience.tsx +24 -0
  85. package/registry/examples/form/native/profile.d.ts +4 -0
  86. package/registry/examples/form/native/profile.dark.png +0 -0
  87. package/registry/examples/form/native/profile.js +33 -0
  88. package/registry/examples/form/native/profile.light.png +0 -0
  89. package/registry/examples/form/native/profile.tsx +36 -0
  90. package/registry/examples/form/native/settings.d.ts +4 -0
  91. package/registry/examples/form/native/settings.dark.png +0 -0
  92. package/registry/examples/form/native/settings.js +25 -0
  93. package/registry/examples/form/native/settings.light.png +0 -0
  94. package/registry/examples/form/native/settings.tsx +23 -0
  95. package/registry/examples/form/native/sign-up.d.ts +4 -0
  96. package/registry/examples/form/native/sign-up.dark.png +0 -0
  97. package/registry/examples/form/native/sign-up.js +38 -0
  98. package/registry/examples/form/native/sign-up.light.png +0 -0
  99. package/registry/examples/form/native/sign-up.tsx +34 -0
  100. package/registry/examples/form/native/upload.d.ts +4 -0
  101. package/registry/examples/form/native/upload.dark.png +0 -0
  102. package/registry/examples/form/native/upload.js +23 -0
  103. package/registry/examples/form/native/upload.light.png +0 -0
  104. package/registry/examples/form/native/upload.tsx +22 -0
  105. package/registry/examples/form/profile.d.ts +4 -0
  106. package/registry/examples/form/profile.js +35 -0
  107. package/registry/examples/form/profile.tsx +40 -0
  108. package/registry/examples/form/report.d.ts +4 -0
  109. package/registry/examples/form/report.js +30 -0
  110. package/registry/examples/form/report.tsx +30 -0
  111. package/registry/examples/form/settings.d.ts +4 -0
  112. package/registry/examples/form/settings.js +30 -0
  113. package/registry/examples/form/settings.tsx +31 -0
  114. package/registry/examples/form/sign-up.d.ts +4 -0
  115. package/registry/examples/form/sign-up.js +38 -0
  116. package/registry/examples/form/sign-up.tsx +37 -0
  117. package/registry/examples/form/upload.d.ts +4 -0
  118. package/registry/examples/form/upload.js +32 -0
  119. package/registry/examples/form/upload.tsx +32 -0
  120. package/registry/field.json +104 -4
  121. package/registry/file-upload.json +1 -1
  122. package/registry/filter-bar.json +1 -1
  123. package/registry/form.json +2940 -0
  124. package/registry/gallery-page.json +1 -1
  125. package/registry/glow-card.json +1 -1
  126. package/registry/goal-gauge.json +1 -1
  127. package/registry/how-it-works-page.json +1 -1
  128. package/registry/image-cropper.json +1 -1
  129. package/registry/index.json +39 -7
  130. package/registry/iso.json +1 -1
  131. package/registry/kbd.json +1 -1
  132. package/registry/landing-page.json +1 -1
  133. package/registry/landscape.json +1 -1
  134. package/registry/legal-page.json +1 -1
  135. package/registry/link-card.json +1 -1
  136. package/registry/loader.json +1 -1
  137. package/registry/longform-page.json +1 -1
  138. package/registry/marketing-layout.json +1 -1
  139. package/registry/marquee.json +1 -1
  140. package/registry/mascot-quote.json +1 -1
  141. package/registry/mascot-state.json +2 -2
  142. package/registry/mascots.json +2 -2
  143. package/registry/media-capture.json +1 -1
  144. package/registry/meter.json +1 -1
  145. package/registry/nav-bar.json +1 -1
  146. package/registry/number-ticker.json +1 -1
  147. package/registry/page-header.json +1 -1
  148. package/registry/page-hero.json +1 -1
  149. package/registry/page.json +1 -1
  150. package/registry/pagination.json +1 -1
  151. package/registry/pointer-light.json +1 -1
  152. package/registry/popover.json +1 -1
  153. package/registry/pricing-page.json +1 -1
  154. package/registry/product-page.json +1 -1
  155. package/registry/profile-card.json +1 -1
  156. package/registry/progress.json +1 -1
  157. package/registry/promise-card.json +1 -1
  158. package/registry/radio-group.json +1 -1
  159. package/registry/scroll-stage.json +1 -1
  160. package/registry/search-field.json +1 -1
  161. package/registry/section-backdrop.json +1 -1
  162. package/registry/separator.json +1 -1
  163. package/registry/settings-layout.json +1 -1
  164. package/registry/skeleton.json +1 -1
  165. package/registry/slider.json +1 -1
  166. package/registry/slot.json +1 -1
  167. package/registry/spinner.json +1 -1
  168. package/registry/split-view.json +1 -1
  169. package/registry/stage-banner.json +1 -1
  170. package/registry/stat.json +1 -1
  171. package/registry/status-page.json +1 -1
  172. package/registry/stepper.json +1 -1
  173. package/registry/switch.json +1 -1
  174. package/registry/tabs.json +1 -1
  175. package/registry/tag-input.json +1 -1
  176. package/registry/terminal-replay.json +1 -1
  177. package/registry/theme.json +1 -1
  178. package/registry/toast.json +1 -1
  179. package/registry/toc.json +1 -1
  180. package/registry/tooltip.json +1 -1
  181. package/registry/video-player.json +1 -1
  182. package/registry/video-trimmer.json +1 -1
  183. package/registry/virtual.json +1 -1
  184. package/skills/form/SKILL.md +44 -0
package/README.md CHANGED
@@ -228,7 +228,7 @@ light or a product's — is a set of tokens.
228
228
  | `@krizaka/ui/native/list` | React Native `List` on FlashList v2 and `useInfiniteList` on `useInfiniteQuery` of `@krizaka/query` (optional peers): separators, skeleton rows, empty/error, pull to refresh, the next page near the end | native |
229
229
  | `@krizaka/ui/native/media-capture` | React Native `MediaCapture` on expo-camera (optional peer) | native |
230
230
  | `@krizaka/ui` | the marks, `MotionObserver`, `RotatingWord`, `cx` (unchanged) | client |
231
- | `@krizaka/ui/native` | React Native: `ThemeProvider`/`useTheme`, `Txt`, `Button`, `IconButton`, `Card.*`, `Badge`, `Chip` (+ `Group`), `Avatar` (+ `Group`), `Skeleton`, `EmptyState`, `Spinner`, `Countdown`, `Segmented`, `Progress`, `Toaster`/`toast`, `Pagination`, `SearchField`, the marks — see [React Native](#react-native) | native |
231
+ | `@krizaka/ui/native` | React Native: `ThemeProvider`/`useTheme`, `Txt`, `Button`, `IconButton`, `Card.*`, `Badge`, `Chip` (+ `Group`), `Avatar` (+ `Group`), `Skeleton`, `EmptyState`, `Spinner`, `Countdown`, `Segmented`, `Progress`, `Toaster`/`toast`, `Pagination`, `SearchField`, `TextField`, `Checkbox`, `DateField` (day · month · year), the marks — see [React Native](#react-native) | native |
232
232
  | `@krizaka/ui/motion.css`, `@krizaka/ui/blocks.css`, `@krizaka/ui/tailwind.css` | the motion signature; the page blocks' styles (imported once by the app); the `@source` of the primitives' classes | CSS |
233
233
  | `@krizaka/ui/registry/<name>`, `@krizaka/ui/registry/examples/<name>/<example>` | the registry (JSON) and the examples — see [Registry](#registry--the-code-drives-the-documentation) | data · client |
234
234
 
@@ -0,0 +1,96 @@
1
+ /** A value a message placeholder takes (`{minimum}`, `{suggestion}`). */
2
+ type FormMessageValue = string | number;
3
+ /** An error of a field or of the whole form: the key of its words in the app's catalogue, and what fills them in. */
4
+ interface FormFieldError {
5
+ /** The i18n key: `form.errors.required`, or the key a schema names (`z.string().min(3, "signUp.username.short")`). */
6
+ key: string;
7
+ /** The placeholders of the message (`{ minimum: 3 }`). */
8
+ values?: Readonly<Record<string, FormMessageValue>>;
9
+ /** The server's own words, in English: shown only when the catalogue has no message for the key. */
10
+ message?: string;
11
+ /** A value to offer instead (a free username): a button under the error sets it. Its label is passed translated. */
12
+ suggestion?: {
13
+ readonly label: string;
14
+ readonly value: unknown;
15
+ };
16
+ /** Shown at once, while the person types (an availability check), not only once they leave the field. */
17
+ immediate?: boolean;
18
+ /** Where it comes from: the schema (default), a field's check, or the server's answer (kept until the field changes). */
19
+ source?: "schema" | "check" | "server";
20
+ }
21
+ /** One issue of a Standard Schema (zod 4, valibot, arktype): zod adds its `code` and limits, read when present. */
22
+ interface SchemaIssue {
23
+ readonly message: string;
24
+ readonly path?: ReadonlyArray<PropertyKey | {
25
+ readonly key: PropertyKey;
26
+ }>;
27
+ readonly code?: string;
28
+ readonly minimum?: number | bigint;
29
+ readonly maximum?: number | bigint;
30
+ readonly origin?: string;
31
+ readonly format?: string;
32
+ readonly input?: unknown;
33
+ readonly expected?: string;
34
+ }
35
+ /**
36
+ * A refusal the form shows, serialisable (a Server Action returns it): the shape of `KrizakaApiError` of @krizaka/query —
37
+ * a `KrizakaApiError` *is* a `FormProblem`. `fields` go under their fields, the rest above the submit button.
38
+ */
39
+ interface FormProblem {
40
+ /** The HTTP status (0 when no response arrived). */
41
+ status?: number;
42
+ /** The stable code of the refusal (`rate-limited`, `conflict`, `ACCOUNT_EXISTS`): the key of its words. */
43
+ code?: string;
44
+ /** The server's sentence, in English: shown only when the catalogue has no message for the code. */
45
+ detail?: string;
46
+ /** The refused fields, by name: a server's `{ code, message }`, or an error the form already knows (`key`). */
47
+ fields?: Readonly<Record<string, {
48
+ code?: string;
49
+ message: string;
50
+ } | FormFieldError | string>>;
51
+ /** Epoch ms before which the same call is refused (a 429). */
52
+ retryAt?: number;
53
+ }
54
+
55
+ /**
56
+ * @krizaka/ui/form/server — the server side of a Krizaka form, for a Server Action or a route handler (no "use client",
57
+ * no React): `validateForm` parses with the form's own schema and answers the errors with the same keys the form shows,
58
+ * `formProblem` turns a thrown error (a `KrizakaApiError` of @krizaka/query) into the serialisable refusal a form reads.
59
+ */
60
+
61
+ /** What a Server Action answers a form: done (with what it made), or a refusal (`fields` under their fields). */
62
+ type FormActionResult<T = unknown> = {
63
+ ok: true;
64
+ data?: T;
65
+ } | {
66
+ ok: false;
67
+ problem: FormProblem;
68
+ };
69
+ type Schema<O> = {
70
+ readonly "~standard": {
71
+ validate: (value: unknown) => {
72
+ value?: O;
73
+ issues?: readonly SchemaIssue[];
74
+ } | Promise<{
75
+ value?: O;
76
+ issues?: readonly SchemaIssue[];
77
+ }>;
78
+ };
79
+ };
80
+ /**
81
+ * Parses what a form sent with its schema (never trust the client): the value, or a `validation-failed` problem whose
82
+ * fields carry the keys the form would show (`form.errors.tooShort`, the schema's own keys).
83
+ */
84
+ declare function validateForm<O>(schema: Schema<O>, input: unknown, options?: {
85
+ prefix?: string;
86
+ }): Promise<{
87
+ ok: true;
88
+ value: O;
89
+ } | {
90
+ ok: false;
91
+ problem: FormProblem;
92
+ }>;
93
+ /** A thrown error as a refusal a form reads — a `KrizakaApiError` keeps its status, code, fields and retry time. */
94
+ declare function formProblem(error: unknown): FormProblem;
95
+
96
+ export { type FormActionResult, type FormFieldError, type FormProblem, formProblem, validateForm };
@@ -0,0 +1,82 @@
1
+ // src/form/errors.ts
2
+ var KEY = /^[\w-]+(\.[\w-]+)+$/;
3
+ function pathName(path) {
4
+ let name = "";
5
+ for (const part of path ?? []) {
6
+ const key = typeof part === "object" && part !== null ? part.key : part;
7
+ if (typeof key === "number") name += `[${key}]`;
8
+ else name += name ? `.${String(key)}` : String(key);
9
+ }
10
+ return name;
11
+ }
12
+ var num = (value) => value === void 0 ? void 0 : Number(value);
13
+ function issueError(issue, prefix = "form.errors") {
14
+ if (KEY.test(issue.message)) return { key: issue.message };
15
+ const key = (code, values) => ({ key: `${prefix}.${code}`, ...values ? { values } : {} });
16
+ const collection = issue.origin === "array" || issue.origin === "set";
17
+ switch (issue.code) {
18
+ case "invalid_type":
19
+ return issue.input === void 0 || issue.input === null || issue.input === "" ? key("required") : issue.expected === "number" ? key("invalidNumber") : issue.expected === "date" ? key("invalidDate") : key("invalid");
20
+ case "too_small": {
21
+ const minimum = num(issue.minimum);
22
+ if (issue.origin === "string") return minimum <= 1 ? key("required") : key("tooShort", { minimum });
23
+ if (collection) return minimum <= 1 ? key("required") : key("tooFew", { minimum });
24
+ return key("tooSmall", { minimum });
25
+ }
26
+ case "too_big": {
27
+ const maximum = num(issue.maximum);
28
+ return issue.origin === "string" ? key("tooLong", { maximum }) : collection ? key("tooMany", { maximum }) : key("tooBig", { maximum });
29
+ }
30
+ case "invalid_format":
31
+ if (issue.format === "email") return key("invalidEmail");
32
+ if (issue.format === "url") return key("invalidUrl");
33
+ if (issue.format === "date" || issue.format === "datetime") return key("invalidDate");
34
+ return key("invalidFormat");
35
+ case "invalid_value":
36
+ return issue.input === false || issue.input === void 0 ? key("required") : key("invalidChoice");
37
+ case "invalid_union":
38
+ case "invalid_key":
39
+ case "invalid_element":
40
+ return key("invalidChoice");
41
+ default:
42
+ return key("invalid");
43
+ }
44
+ }
45
+ function issuesToFields(issues, prefix) {
46
+ const fields = {};
47
+ for (const issue of issues) {
48
+ const name = pathName(issue.path);
49
+ if (!(name in fields)) fields[name] = { ...issueError(issue, prefix), source: "schema" };
50
+ }
51
+ return fields;
52
+ }
53
+
54
+ // src/form/server.ts
55
+ async function validateForm(schema, input, options = {}) {
56
+ const result = await schema["~standard"].validate(input);
57
+ if (!result.issues?.length) return { ok: true, value: result.value };
58
+ const fields = issuesToFields(result.issues, options.prefix);
59
+ const general = fields[""];
60
+ delete fields[""];
61
+ return { ok: false, problem: { status: 422, code: general ? general.key : "validation-failed", fields } };
62
+ }
63
+ function formProblem(error) {
64
+ if (error && typeof error === "object") {
65
+ const e = error;
66
+ if (typeof e.status === "number" || typeof e.code === "string") {
67
+ return {
68
+ ...typeof e.status === "number" ? { status: e.status } : {},
69
+ ...typeof e.code === "string" ? { code: e.code } : {},
70
+ ...typeof e.detail === "string" ? { detail: e.detail } : {},
71
+ ...e.fields && typeof e.fields === "object" ? { fields: JSON.parse(JSON.stringify(e.fields)) } : {},
72
+ ...typeof e.retryAt === "number" ? { retryAt: e.retryAt } : {}
73
+ };
74
+ }
75
+ }
76
+ return { status: 500, code: "server" };
77
+ }
78
+ export {
79
+ formProblem,
80
+ validateForm
81
+ };
82
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/form/errors.ts","../../src/form/server.ts"],"sourcesContent":["// Platform-neutral and server-safe: no React, no DOM, no React Native. The errors of a Krizaka form — a schema's issues,\n// a server's Problem Details, an availability check — as one shape, `FormFieldError`, whose words come from a key of the\n// app's catalogue (`form.errors.tooShort`), never from the validation engine's English.\n\n/** A value a message placeholder takes (`{minimum}`, `{suggestion}`). */\nexport type FormMessageValue = string | number;\n\n/** An error of a field or of the whole form: the key of its words in the app's catalogue, and what fills them in. */\nexport interface FormFieldError {\n /** The i18n key: `form.errors.required`, or the key a schema names (`z.string().min(3, \"signUp.username.short\")`). */\n key: string;\n /** The placeholders of the message (`{ minimum: 3 }`). */\n values?: Readonly<Record<string, FormMessageValue>>;\n /** The server's own words, in English: shown only when the catalogue has no message for the key. */\n message?: string;\n /** A value to offer instead (a free username): a button under the error sets it. Its label is passed translated. */\n suggestion?: { readonly label: string; readonly value: unknown };\n /** Shown at once, while the person types (an availability check), not only once they leave the field. */\n immediate?: boolean;\n /** Where it comes from: the schema (default), a field's check, or the server's answer (kept until the field changes). */\n source?: \"schema\" | \"check\" | \"server\";\n}\n\n/** The English defaults of the generic errors, by code — the words a form shows when the app's catalogue has none. */\nexport const formMessages: Readonly<Record<string, string>> = {\n required: \"This field is required.\",\n tooShort: \"Use at least {minimum} characters.\",\n tooLong: \"Use at most {maximum} characters.\",\n tooSmall: \"Enter {minimum} or more.\",\n tooBig: \"Enter {maximum} or less.\",\n tooFew: \"Choose at least {minimum}.\",\n tooMany: \"Choose at most {maximum}.\",\n invalidEmail: \"Enter an e-mail address, like name@example.com.\",\n invalidUrl: \"Enter a web address, like https://example.com.\",\n invalidDate: \"Enter a real date.\",\n invalidNumber: \"Enter a number.\",\n invalidFormat: \"Check the format of this field.\",\n invalidChoice: \"Choose one of the options.\",\n invalid: \"Check this field.\",\n // The whole form (a server's answer without fields): the codes of @krizaka/query's KrizakaApiError.\n network: \"No connection. Check your network and try again.\",\n \"rate-limited\": \"Too many tries. Wait a moment and try again.\",\n unauthorized: \"Your session has ended. Sign in again.\",\n forbidden: \"You are not allowed to do this.\",\n conflict: \"This was changed meanwhile. Reload and try again.\",\n \"validation-failed\": \"Some fields need your attention.\",\n unavailable: \"The service is unavailable for a moment. Try again shortly.\",\n server: \"Something went wrong on our side. Try again.\",\n};\n\n/** How a form finds its words: the app's translate function, and the prefixes of its keys. */\nexport interface FormI18n {\n /** The app's translate function (`(key, values) => t(key, values)`): a string, or `undefined` when the key is missing. */\n t?: (key: string, values?: Readonly<Record<string, FormMessageValue>>) => string | undefined;\n /** The prefix of the generic field errors. Default `form.errors` (→ `form.errors.tooShort`). */\n prefix?: string;\n /** The prefix of the server's codes for the whole form, as `KrizakaApiError.messageKey()`. Default `apiErrors`. */\n apiPrefix?: string;\n /** The locale of numbers, money and dates. Default `en`. */\n locale?: string;\n}\n\nconst KEY = /^[\\w-]+(\\.[\\w-]+)+$/;\n\nconst fill = (text: string, values?: Readonly<Record<string, FormMessageValue>>) =>\n values ? text.replace(/\\{(\\w+)\\}/g, (all, name: string) => (values[name] === undefined ? all : String(values[name]))) : text;\n\n/** The words of an error: the app's catalogue, else the English default of its code, else the server's words, else the key. */\nexport function formMessage(error: FormFieldError | string, i18n: FormI18n = {}): string {\n const e = typeof error === \"string\" ? { key: error } : error;\n const translated = i18n.t?.(e.key, e.values);\n if (translated && translated !== e.key) return translated;\n const code = e.key.slice(e.key.lastIndexOf(\".\") + 1);\n const fallback = formMessages[code];\n // The server's words beat a generic default (a refusal without a code), never a default for its own code.\n if (e.message && (code === \"invalid\" || !fallback)) return e.message;\n if (fallback && (e.key.startsWith(`${i18n.prefix ?? \"form.errors\"}.`) || e.key.startsWith(`${i18n.apiPrefix ?? \"apiErrors\"}.`))) return fill(fallback, e.values);\n return e.message ?? (KEY.test(e.key) ? fill(fallback ?? formMessages.invalid!, e.values) : e.key);\n}\n\n/** One issue of a Standard Schema (zod 4, valibot, arktype): zod adds its `code` and limits, read when present. */\nexport interface SchemaIssue {\n readonly message: string;\n readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }>;\n readonly code?: string;\n readonly minimum?: number | bigint;\n readonly maximum?: number | bigint;\n readonly origin?: string;\n readonly format?: string;\n readonly input?: unknown;\n readonly expected?: string;\n}\n\n/** A field's name from an issue's path, as the form writes it: `links[0].url`. */\nexport function pathName(path: SchemaIssue[\"path\"]): string {\n let name = \"\";\n for (const part of path ?? []) {\n const key = typeof part === \"object\" && part !== null ? part.key : part;\n if (typeof key === \"number\") name += `[${key}]`;\n else name += name ? `.${String(key)}` : String(key);\n }\n return name;\n}\n\nconst num = (value: number | bigint | undefined) => (value === undefined ? undefined : Number(value));\n\n/** The key and values of an issue: the schema's own key when it names one, else the generic code of what failed. */\nexport function issueError(issue: SchemaIssue, prefix = \"form.errors\"): FormFieldError {\n if (KEY.test(issue.message)) return { key: issue.message };\n const key = (code: string, values?: Record<string, FormMessageValue>) => ({ key: `${prefix}.${code}`, ...(values ? { values } : {}) });\n const collection = issue.origin === \"array\" || issue.origin === \"set\";\n switch (issue.code) {\n case \"invalid_type\":\n return issue.input === undefined || issue.input === null || issue.input === \"\" ? key(\"required\") : issue.expected === \"number\" ? key(\"invalidNumber\") : issue.expected === \"date\" ? key(\"invalidDate\") : key(\"invalid\");\n case \"too_small\": {\n const minimum = num(issue.minimum)!;\n if (issue.origin === \"string\") return minimum <= 1 ? key(\"required\") : key(\"tooShort\", { minimum });\n if (collection) return minimum <= 1 ? key(\"required\") : key(\"tooFew\", { minimum });\n return key(\"tooSmall\", { minimum });\n }\n case \"too_big\": {\n const maximum = num(issue.maximum)!;\n return issue.origin === \"string\" ? key(\"tooLong\", { maximum }) : collection ? key(\"tooMany\", { maximum }) : key(\"tooBig\", { maximum });\n }\n case \"invalid_format\":\n if (issue.format === \"email\") return key(\"invalidEmail\");\n if (issue.format === \"url\") return key(\"invalidUrl\");\n if (issue.format === \"date\" || issue.format === \"datetime\") return key(\"invalidDate\");\n return key(\"invalidFormat\");\n case \"invalid_value\":\n // `z.literal(true)`: a box that must be ticked.\n return issue.input === false || issue.input === undefined ? key(\"required\") : key(\"invalidChoice\");\n case \"invalid_union\":\n case \"invalid_key\":\n case \"invalid_element\":\n return key(\"invalidChoice\");\n default:\n return key(\"invalid\");\n }\n}\n\n/** The first error of each field of a schema's issues (an issue without a path is the whole form's: `\"\"`). */\nexport function issuesToFields(issues: readonly SchemaIssue[], prefix?: string): Record<string, FormFieldError> {\n const fields: Record<string, FormFieldError> = {};\n for (const issue of issues) {\n const name = pathName(issue.path);\n if (!(name in fields)) fields[name] = { ...issueError(issue, prefix), source: \"schema\" };\n }\n return fields;\n}\n\n/**\n * A refusal the form shows, serialisable (a Server Action returns it): the shape of `KrizakaApiError` of @krizaka/query —\n * a `KrizakaApiError` *is* a `FormProblem`. `fields` go under their fields, the rest above the submit button.\n */\nexport interface FormProblem {\n /** The HTTP status (0 when no response arrived). */\n status?: number;\n /** The stable code of the refusal (`rate-limited`, `conflict`, `ACCOUNT_EXISTS`): the key of its words. */\n code?: string;\n /** The server's sentence, in English: shown only when the catalogue has no message for the code. */\n detail?: string;\n /** The refused fields, by name: a server's `{ code, message }`, or an error the form already knows (`key`). */\n fields?: Readonly<Record<string, { code?: string; message: string } | FormFieldError | string>>;\n /** Epoch ms before which the same call is refused (a 429). */\n retryAt?: number;\n}\n\n/** True for what a form can read as a problem: a `KrizakaApiError`, a `FormProblem`, an `{ ok: false, problem }` result. */\nexport function isFormProblem(value: unknown): value is FormProblem {\n return typeof value === \"object\" && value !== null && (\"fields\" in value || \"code\" in value || \"status\" in value);\n}\n\n/** A problem as field errors (by name) and the whole form's error (or `null` when every refusal has its field). */\nexport function problemToErrors(\n problem: FormProblem,\n i18n: FormI18n = {},\n known?: (name: string) => boolean,\n codeFields: Readonly<Record<string, string>> = {},\n): { fields: Record<string, FormFieldError>; form: FormFieldError | null } {\n const prefix = i18n.prefix ?? \"form.errors\";\n const fields: Record<string, FormFieldError> = {};\n let unplaced = false;\n // A refusal named by a code that belongs to a field (`UNDERAGE` → `dateOfBirth`): under that field, in its words.\n const owner = problem.code ? codeFields[problem.code] : undefined;\n if (owner && (!known || known(owner)) && !problem.fields?.[owner]) {\n fields[owner] = { key: `${prefix}.${problem.code}`, ...(problem.detail ? { message: problem.detail } : {}), source: \"server\" };\n if (!problem.fields || Object.keys(problem.fields).length === 0) return { fields, form: null };\n }\n for (const [name, refusal] of Object.entries(problem.fields ?? {})) {\n if (known && !known(name)) {\n unplaced = true;\n continue;\n }\n const error: FormFieldError =\n typeof refusal === \"string\"\n ? { key: refusal }\n : \"key\" in refusal\n ? refusal\n : { key: refusal.code ? (KEY.test(refusal.code) ? refusal.code : `${prefix}.${refusal.code}`) : `${prefix}.invalid`, message: refusal.message };\n fields[name] = { ...error, source: \"server\" };\n }\n const placed = Object.keys(fields).length > 0;\n if (placed && !unplaced) return { fields, form: null };\n const code = problem.code ?? (problem.status === 0 ? \"network\" : problem.status && problem.status >= 500 ? \"server\" : \"invalid\");\n const retryIn = problem.retryAt ? Math.max(1, Math.ceil((problem.retryAt - Date.now()) / 1000)) : undefined;\n return {\n fields,\n form: { key: KEY.test(code) ? code : `${i18n.apiPrefix ?? \"apiErrors\"}.${code}`, ...(problem.detail ? { message: problem.detail } : {}), ...(retryIn ? { values: { seconds: retryIn } } : {}), source: \"server\" },\n };\n}\n","/**\n * @krizaka/ui/form/server — the server side of a Krizaka form, for a Server Action or a route handler (no \"use client\",\n * no React): `validateForm` parses with the form's own schema and answers the errors with the same keys the form shows,\n * `formProblem` turns a thrown error (a `KrizakaApiError` of @krizaka/query) into the serialisable refusal a form reads.\n */\nimport { type FormFieldError, type FormProblem, issuesToFields, type SchemaIssue } from \"./errors\";\n\nexport type { FormFieldError, FormProblem } from \"./errors\";\n\n/** What a Server Action answers a form: done (with what it made), or a refusal (`fields` under their fields). */\nexport type FormActionResult<T = unknown> = { ok: true; data?: T } | { ok: false; problem: FormProblem };\n\ntype Schema<O> = { readonly \"~standard\": { validate: (value: unknown) => { value?: O; issues?: readonly SchemaIssue[] } | Promise<{ value?: O; issues?: readonly SchemaIssue[] }> } };\n\n/**\n * Parses what a form sent with its schema (never trust the client): the value, or a `validation-failed` problem whose\n * fields carry the keys the form would show (`form.errors.tooShort`, the schema's own keys).\n */\nexport async function validateForm<O>(schema: Schema<O>, input: unknown, options: { prefix?: string } = {}): Promise<{ ok: true; value: O } | { ok: false; problem: FormProblem }> {\n const result = await schema[\"~standard\"].validate(input);\n if (!result.issues?.length) return { ok: true, value: result.value as O };\n const fields: Record<string, FormFieldError> = issuesToFields(result.issues, options.prefix);\n const general = fields[\"\"];\n delete fields[\"\"];\n return { ok: false, problem: { status: 422, code: general ? general.key : \"validation-failed\", fields } };\n}\n\n/** A thrown error as a refusal a form reads — a `KrizakaApiError` keeps its status, code, fields and retry time. */\nexport function formProblem(error: unknown): FormProblem {\n if (error && typeof error === \"object\") {\n const e = error as { status?: unknown; code?: unknown; detail?: unknown; fields?: unknown; retryAt?: unknown };\n if (typeof e.status === \"number\" || typeof e.code === \"string\") {\n return {\n ...(typeof e.status === \"number\" ? { status: e.status } : {}),\n ...(typeof e.code === \"string\" ? { code: e.code } : {}),\n ...(typeof e.detail === \"string\" ? { detail: e.detail } : {}),\n ...(e.fields && typeof e.fields === \"object\" ? { fields: JSON.parse(JSON.stringify(e.fields)) as FormProblem[\"fields\"] } : {}),\n ...(typeof e.retryAt === \"number\" ? { retryAt: e.retryAt } : {}),\n };\n }\n }\n return { status: 500, code: \"server\" };\n}\n"],"mappings":"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","names":[]}