@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.
- package/README.md +1 -1
- package/dist/form/server.d.ts +96 -0
- package/dist/form/server.js +82 -0
- package/dist/form/server.js.map +1 -0
- package/dist/form.d.ts +503 -0
- package/dist/form.js +760 -0
- package/dist/form.js.map +1 -0
- package/dist/native/form.d.ts +376 -0
- package/dist/native/form.js +650 -0
- package/dist/native/form.js.map +1 -0
- package/dist/native.d.ts +113 -2
- package/dist/native.js +618 -388
- package/dist/native.js.map +1 -1
- package/package.json +48 -5
- package/registry/achievement.json +1 -1
- package/registry/action-bar.json +1 -1
- package/registry/alert.json +1 -1
- package/registry/app-shell.json +1 -1
- package/registry/argument-card.json +1 -1
- package/registry/audio-player.json +1 -1
- package/registry/auth-layout.json +1 -1
- package/registry/avatar.json +1 -1
- package/registry/badge.json +1 -1
- package/registry/button.json +1 -1
- package/registry/card-grid.json +1 -1
- package/registry/card.json +1 -1
- package/registry/catalogue-page.json +1 -1
- package/registry/changelog-page.json +1 -1
- package/registry/checkbox.json +89 -4
- package/registry/chip-link.json +1 -1
- package/registry/chip.json +1 -1
- package/registry/choice-grid.json +1 -1
- package/registry/code-block.json +1 -1
- package/registry/combobox.json +1 -1
- package/registry/command.json +1 -1
- package/registry/component-page.json +1 -1
- package/registry/confirm-button.json +1 -1
- package/registry/contact-page.json +1 -1
- package/registry/continue-prompt.json +1 -1
- package/registry/countdown.json +1 -1
- package/registry/cta-panel.json +1 -1
- package/registry/dashboard-layout.json +1 -1
- package/registry/data-table.json +1 -1
- package/registry/date-picker.json +142 -4
- package/registry/decision-list.json +1 -1
- package/registry/detail-layout.json +1 -1
- package/registry/dialog.json +1 -1
- package/registry/docs-article-page.json +1 -1
- package/registry/docs-hub-page.json +1 -1
- package/registry/docs-layout.json +1 -1
- package/registry/dropdown-menu.json +1 -1
- package/registry/empty-state.json +1 -1
- package/registry/error-page.json +1 -1
- package/registry/examples/checkbox/native/terms.d.ts +4 -0
- package/registry/examples/checkbox/native/terms.dark.png +0 -0
- package/registry/examples/checkbox/native/terms.js +26 -0
- package/registry/examples/checkbox/native/terms.light.png +0 -0
- package/registry/examples/checkbox/native/terms.tsx +28 -0
- package/registry/examples/date-picker/native/birth-date.d.ts +4 -0
- package/registry/examples/date-picker/native/birth-date.dark.png +0 -0
- package/registry/examples/date-picker/native/birth-date.js +30 -0
- package/registry/examples/date-picker/native/birth-date.light.png +0 -0
- package/registry/examples/date-picker/native/birth-date.tsx +27 -0
- package/registry/examples/field/native/default.d.ts +4 -0
- package/registry/examples/field/native/default.dark.png +0 -0
- package/registry/examples/field/native/default.js +29 -0
- package/registry/examples/field/native/default.light.png +0 -0
- package/registry/examples/field/native/default.tsx +23 -0
- package/registry/examples/form/amount.d.ts +4 -0
- package/registry/examples/form/amount.js +30 -0
- package/registry/examples/form/amount.tsx +36 -0
- package/registry/examples/form/audience.d.ts +4 -0
- package/registry/examples/form/audience.js +29 -0
- package/registry/examples/form/audience.tsx +31 -0
- package/registry/examples/form/native/amount.d.ts +4 -0
- package/registry/examples/form/native/amount.dark.png +0 -0
- package/registry/examples/form/native/amount.js +26 -0
- package/registry/examples/form/native/amount.light.png +0 -0
- package/registry/examples/form/native/amount.tsx +30 -0
- package/registry/examples/form/native/audience.d.ts +4 -0
- package/registry/examples/form/native/audience.dark.png +0 -0
- package/registry/examples/form/native/audience.js +24 -0
- package/registry/examples/form/native/audience.light.png +0 -0
- package/registry/examples/form/native/audience.tsx +24 -0
- package/registry/examples/form/native/profile.d.ts +4 -0
- package/registry/examples/form/native/profile.dark.png +0 -0
- package/registry/examples/form/native/profile.js +33 -0
- package/registry/examples/form/native/profile.light.png +0 -0
- package/registry/examples/form/native/profile.tsx +36 -0
- package/registry/examples/form/native/settings.d.ts +4 -0
- package/registry/examples/form/native/settings.dark.png +0 -0
- package/registry/examples/form/native/settings.js +25 -0
- package/registry/examples/form/native/settings.light.png +0 -0
- package/registry/examples/form/native/settings.tsx +23 -0
- package/registry/examples/form/native/sign-up.d.ts +4 -0
- package/registry/examples/form/native/sign-up.dark.png +0 -0
- package/registry/examples/form/native/sign-up.js +38 -0
- package/registry/examples/form/native/sign-up.light.png +0 -0
- package/registry/examples/form/native/sign-up.tsx +34 -0
- package/registry/examples/form/native/upload.d.ts +4 -0
- package/registry/examples/form/native/upload.dark.png +0 -0
- package/registry/examples/form/native/upload.js +23 -0
- package/registry/examples/form/native/upload.light.png +0 -0
- package/registry/examples/form/native/upload.tsx +22 -0
- package/registry/examples/form/profile.d.ts +4 -0
- package/registry/examples/form/profile.js +35 -0
- package/registry/examples/form/profile.tsx +40 -0
- package/registry/examples/form/report.d.ts +4 -0
- package/registry/examples/form/report.js +30 -0
- package/registry/examples/form/report.tsx +30 -0
- package/registry/examples/form/settings.d.ts +4 -0
- package/registry/examples/form/settings.js +30 -0
- package/registry/examples/form/settings.tsx +31 -0
- package/registry/examples/form/sign-up.d.ts +4 -0
- package/registry/examples/form/sign-up.js +38 -0
- package/registry/examples/form/sign-up.tsx +37 -0
- package/registry/examples/form/upload.d.ts +4 -0
- package/registry/examples/form/upload.js +32 -0
- package/registry/examples/form/upload.tsx +32 -0
- package/registry/field.json +104 -4
- package/registry/file-upload.json +1 -1
- package/registry/filter-bar.json +1 -1
- package/registry/form.json +2940 -0
- package/registry/gallery-page.json +1 -1
- package/registry/glow-card.json +1 -1
- package/registry/goal-gauge.json +1 -1
- package/registry/how-it-works-page.json +1 -1
- package/registry/image-cropper.json +1 -1
- package/registry/index.json +39 -7
- package/registry/iso.json +1 -1
- package/registry/kbd.json +1 -1
- package/registry/landing-page.json +1 -1
- package/registry/landscape.json +1 -1
- package/registry/legal-page.json +1 -1
- package/registry/link-card.json +1 -1
- package/registry/loader.json +1 -1
- package/registry/longform-page.json +1 -1
- package/registry/marketing-layout.json +1 -1
- package/registry/marquee.json +1 -1
- package/registry/mascot-quote.json +1 -1
- package/registry/mascot-state.json +2 -2
- package/registry/mascots.json +2 -2
- package/registry/media-capture.json +1 -1
- package/registry/meter.json +1 -1
- package/registry/nav-bar.json +1 -1
- package/registry/number-ticker.json +1 -1
- package/registry/page-header.json +1 -1
- package/registry/page-hero.json +1 -1
- package/registry/page.json +1 -1
- package/registry/pagination.json +1 -1
- package/registry/pointer-light.json +1 -1
- package/registry/popover.json +1 -1
- package/registry/pricing-page.json +1 -1
- package/registry/product-page.json +1 -1
- package/registry/profile-card.json +1 -1
- package/registry/progress.json +1 -1
- package/registry/promise-card.json +1 -1
- package/registry/radio-group.json +1 -1
- package/registry/scroll-stage.json +1 -1
- package/registry/search-field.json +1 -1
- package/registry/section-backdrop.json +1 -1
- package/registry/separator.json +1 -1
- package/registry/settings-layout.json +1 -1
- package/registry/skeleton.json +1 -1
- package/registry/slider.json +1 -1
- package/registry/slot.json +1 -1
- package/registry/spinner.json +1 -1
- package/registry/split-view.json +1 -1
- package/registry/stage-banner.json +1 -1
- package/registry/stat.json +1 -1
- package/registry/status-page.json +1 -1
- package/registry/stepper.json +1 -1
- package/registry/switch.json +1 -1
- package/registry/tabs.json +1 -1
- package/registry/tag-input.json +1 -1
- package/registry/terminal-replay.json +1 -1
- package/registry/theme.json +1 -1
- package/registry/toast.json +1 -1
- package/registry/toc.json +1 -1
- package/registry/tooltip.json +1 -1
- package/registry/video-player.json +1 -1
- package/registry/video-trimmer.json +1 -1
- package/registry/virtual.json +1 -1
- package/skills/form/SKILL.md +44 -0
package/dist/form.js
ADDED
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/date-picker/parts.ts
|
|
4
|
+
var DATE_PART_LENGTH = { day: 2, month: 2, year: 4 };
|
|
5
|
+
function datePartsOrder(locale) {
|
|
6
|
+
try {
|
|
7
|
+
const parts = new Intl.DateTimeFormat(locale, { day: "2-digit", month: "2-digit", year: "numeric" }).formatToParts(new Date(2001, 10, 22));
|
|
8
|
+
const order = parts.map((p) => p.type).filter((t) => t === "day" || t === "month" || t === "year");
|
|
9
|
+
if (order.length === 3) return order;
|
|
10
|
+
} catch {
|
|
11
|
+
}
|
|
12
|
+
return ["day", "month", "year"];
|
|
13
|
+
}
|
|
14
|
+
function composeDate(parts) {
|
|
15
|
+
if (!parts.day && !parts.month && !parts.year) return "";
|
|
16
|
+
const pad = (v) => v.length === 1 ? `0${v}` : v;
|
|
17
|
+
return `${parts.year}-${pad(parts.month)}-${pad(parts.day)}`;
|
|
18
|
+
}
|
|
19
|
+
function splitDate(value) {
|
|
20
|
+
const m = /^(\d*)-(\d*)-(\d*)$/.exec(value ?? "");
|
|
21
|
+
return m ? { year: m[1], month: m[2], day: m[3] } : { day: "", month: "", year: "" };
|
|
22
|
+
}
|
|
23
|
+
function isRealDate(value) {
|
|
24
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
|
25
|
+
const date = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
|
|
26
|
+
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value;
|
|
27
|
+
}
|
|
28
|
+
function latestBirthDate(age, today = /* @__PURE__ */ new Date()) {
|
|
29
|
+
return new Date(Date.UTC(today.getFullYear() - age, today.getMonth(), today.getDate())).toISOString().slice(0, 10);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/form/core.tsx
|
|
33
|
+
import { useField, useForm, useStore } from "@tanstack/react-form";
|
|
34
|
+
import * as React from "react";
|
|
35
|
+
|
|
36
|
+
// src/form/errors.ts
|
|
37
|
+
var formMessages = {
|
|
38
|
+
required: "This field is required.",
|
|
39
|
+
tooShort: "Use at least {minimum} characters.",
|
|
40
|
+
tooLong: "Use at most {maximum} characters.",
|
|
41
|
+
tooSmall: "Enter {minimum} or more.",
|
|
42
|
+
tooBig: "Enter {maximum} or less.",
|
|
43
|
+
tooFew: "Choose at least {minimum}.",
|
|
44
|
+
tooMany: "Choose at most {maximum}.",
|
|
45
|
+
invalidEmail: "Enter an e-mail address, like name@example.com.",
|
|
46
|
+
invalidUrl: "Enter a web address, like https://example.com.",
|
|
47
|
+
invalidDate: "Enter a real date.",
|
|
48
|
+
invalidNumber: "Enter a number.",
|
|
49
|
+
invalidFormat: "Check the format of this field.",
|
|
50
|
+
invalidChoice: "Choose one of the options.",
|
|
51
|
+
invalid: "Check this field.",
|
|
52
|
+
// The whole form (a server's answer without fields): the codes of @krizaka/query's KrizakaApiError.
|
|
53
|
+
network: "No connection. Check your network and try again.",
|
|
54
|
+
"rate-limited": "Too many tries. Wait a moment and try again.",
|
|
55
|
+
unauthorized: "Your session has ended. Sign in again.",
|
|
56
|
+
forbidden: "You are not allowed to do this.",
|
|
57
|
+
conflict: "This was changed meanwhile. Reload and try again.",
|
|
58
|
+
"validation-failed": "Some fields need your attention.",
|
|
59
|
+
unavailable: "The service is unavailable for a moment. Try again shortly.",
|
|
60
|
+
server: "Something went wrong on our side. Try again."
|
|
61
|
+
};
|
|
62
|
+
var KEY = /^[\w-]+(\.[\w-]+)+$/;
|
|
63
|
+
var fill = (text, values) => values ? text.replace(/\{(\w+)\}/g, (all, name) => values[name] === void 0 ? all : String(values[name])) : text;
|
|
64
|
+
function formMessage(error, i18n = {}) {
|
|
65
|
+
const e = typeof error === "string" ? { key: error } : error;
|
|
66
|
+
const translated = i18n.t?.(e.key, e.values);
|
|
67
|
+
if (translated && translated !== e.key) return translated;
|
|
68
|
+
const code = e.key.slice(e.key.lastIndexOf(".") + 1);
|
|
69
|
+
const fallback = formMessages[code];
|
|
70
|
+
if (e.message && (code === "invalid" || !fallback)) return e.message;
|
|
71
|
+
if (fallback && (e.key.startsWith(`${i18n.prefix ?? "form.errors"}.`) || e.key.startsWith(`${i18n.apiPrefix ?? "apiErrors"}.`))) return fill(fallback, e.values);
|
|
72
|
+
return e.message ?? (KEY.test(e.key) ? fill(fallback ?? formMessages.invalid, e.values) : e.key);
|
|
73
|
+
}
|
|
74
|
+
function pathName(path) {
|
|
75
|
+
let name = "";
|
|
76
|
+
for (const part of path ?? []) {
|
|
77
|
+
const key = typeof part === "object" && part !== null ? part.key : part;
|
|
78
|
+
if (typeof key === "number") name += `[${key}]`;
|
|
79
|
+
else name += name ? `.${String(key)}` : String(key);
|
|
80
|
+
}
|
|
81
|
+
return name;
|
|
82
|
+
}
|
|
83
|
+
var num = (value) => value === void 0 ? void 0 : Number(value);
|
|
84
|
+
function issueError(issue, prefix = "form.errors") {
|
|
85
|
+
if (KEY.test(issue.message)) return { key: issue.message };
|
|
86
|
+
const key = (code, values) => ({ key: `${prefix}.${code}`, ...values ? { values } : {} });
|
|
87
|
+
const collection = issue.origin === "array" || issue.origin === "set";
|
|
88
|
+
switch (issue.code) {
|
|
89
|
+
case "invalid_type":
|
|
90
|
+
return issue.input === void 0 || issue.input === null || issue.input === "" ? key("required") : issue.expected === "number" ? key("invalidNumber") : issue.expected === "date" ? key("invalidDate") : key("invalid");
|
|
91
|
+
case "too_small": {
|
|
92
|
+
const minimum = num(issue.minimum);
|
|
93
|
+
if (issue.origin === "string") return minimum <= 1 ? key("required") : key("tooShort", { minimum });
|
|
94
|
+
if (collection) return minimum <= 1 ? key("required") : key("tooFew", { minimum });
|
|
95
|
+
return key("tooSmall", { minimum });
|
|
96
|
+
}
|
|
97
|
+
case "too_big": {
|
|
98
|
+
const maximum = num(issue.maximum);
|
|
99
|
+
return issue.origin === "string" ? key("tooLong", { maximum }) : collection ? key("tooMany", { maximum }) : key("tooBig", { maximum });
|
|
100
|
+
}
|
|
101
|
+
case "invalid_format":
|
|
102
|
+
if (issue.format === "email") return key("invalidEmail");
|
|
103
|
+
if (issue.format === "url") return key("invalidUrl");
|
|
104
|
+
if (issue.format === "date" || issue.format === "datetime") return key("invalidDate");
|
|
105
|
+
return key("invalidFormat");
|
|
106
|
+
case "invalid_value":
|
|
107
|
+
return issue.input === false || issue.input === void 0 ? key("required") : key("invalidChoice");
|
|
108
|
+
case "invalid_union":
|
|
109
|
+
case "invalid_key":
|
|
110
|
+
case "invalid_element":
|
|
111
|
+
return key("invalidChoice");
|
|
112
|
+
default:
|
|
113
|
+
return key("invalid");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function issuesToFields(issues, prefix) {
|
|
117
|
+
const fields2 = {};
|
|
118
|
+
for (const issue of issues) {
|
|
119
|
+
const name = pathName(issue.path);
|
|
120
|
+
if (!(name in fields2)) fields2[name] = { ...issueError(issue, prefix), source: "schema" };
|
|
121
|
+
}
|
|
122
|
+
return fields2;
|
|
123
|
+
}
|
|
124
|
+
function isFormProblem(value) {
|
|
125
|
+
return typeof value === "object" && value !== null && ("fields" in value || "code" in value || "status" in value);
|
|
126
|
+
}
|
|
127
|
+
function problemToErrors(problem, i18n = {}, known, codeFields = {}) {
|
|
128
|
+
const prefix = i18n.prefix ?? "form.errors";
|
|
129
|
+
const fields2 = {};
|
|
130
|
+
let unplaced = false;
|
|
131
|
+
const owner = problem.code ? codeFields[problem.code] : void 0;
|
|
132
|
+
if (owner && (!known || known(owner)) && !problem.fields?.[owner]) {
|
|
133
|
+
fields2[owner] = { key: `${prefix}.${problem.code}`, ...problem.detail ? { message: problem.detail } : {}, source: "server" };
|
|
134
|
+
if (!problem.fields || Object.keys(problem.fields).length === 0) return { fields: fields2, form: null };
|
|
135
|
+
}
|
|
136
|
+
for (const [name, refusal] of Object.entries(problem.fields ?? {})) {
|
|
137
|
+
if (known && !known(name)) {
|
|
138
|
+
unplaced = true;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const error = typeof refusal === "string" ? { key: refusal } : "key" in refusal ? refusal : { key: refusal.code ? KEY.test(refusal.code) ? refusal.code : `${prefix}.${refusal.code}` : `${prefix}.invalid`, message: refusal.message };
|
|
142
|
+
fields2[name] = { ...error, source: "server" };
|
|
143
|
+
}
|
|
144
|
+
const placed = Object.keys(fields2).length > 0;
|
|
145
|
+
if (placed && !unplaced) return { fields: fields2, form: null };
|
|
146
|
+
const code = problem.code ?? (problem.status === 0 ? "network" : problem.status && problem.status >= 500 ? "server" : "invalid");
|
|
147
|
+
const retryIn = problem.retryAt ? Math.max(1, Math.ceil((problem.retryAt - Date.now()) / 1e3)) : void 0;
|
|
148
|
+
return {
|
|
149
|
+
fields: fields2,
|
|
150
|
+
form: { key: KEY.test(code) ? code : `${i18n.apiPrefix ?? "apiErrors"}.${code}`, ...problem.detail ? { message: problem.detail } : {}, ...retryIn ? { values: { seconds: retryIn } } : {}, source: "server" }
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/form/core.tsx
|
|
155
|
+
import { jsx } from "react/jsx-runtime";
|
|
156
|
+
var FormContext = React.createContext(null);
|
|
157
|
+
function useFormContext() {
|
|
158
|
+
const ctx = React.useContext(FormContext);
|
|
159
|
+
if (!ctx) throw new Error("A Krizaka form field must be inside its form's `form.Form`.");
|
|
160
|
+
return ctx;
|
|
161
|
+
}
|
|
162
|
+
var run = (schema, value) => schema["~standard"].validate(value);
|
|
163
|
+
function useKrizakaFormCore(definition, options = {}) {
|
|
164
|
+
const i18n = { ...definition.i18n, ...options.i18n };
|
|
165
|
+
const latest = React.useRef({ options, i18n });
|
|
166
|
+
React.useLayoutEffect(() => {
|
|
167
|
+
latest.current = { options, i18n };
|
|
168
|
+
});
|
|
169
|
+
const [error, setError] = React.useState(null);
|
|
170
|
+
const refused = React.useRef(false);
|
|
171
|
+
const api = useForm({
|
|
172
|
+
defaultValues: options.defaultValues ?? definition.defaultValues,
|
|
173
|
+
validators: {
|
|
174
|
+
// Every change: the schema, synchronously (an async refinement waits for the submit). Errors are computed for
|
|
175
|
+
// every field; each field shows its own once the person left it or tried to send.
|
|
176
|
+
onChange: ({ value }) => {
|
|
177
|
+
const result = run(definition.schema, value);
|
|
178
|
+
if (result instanceof Promise || !result.issues?.length) return void 0;
|
|
179
|
+
const fields2 = issuesToFields(result.issues, latest.current.i18n.prefix);
|
|
180
|
+
delete fields2[""];
|
|
181
|
+
return { fields: fields2 };
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
onSubmit: async ({ value }) => {
|
|
185
|
+
const parsed = await run(definition.schema, value);
|
|
186
|
+
if (parsed.issues?.length) {
|
|
187
|
+
apply({ fields: issuesToFields(parsed.issues, latest.current.i18n.prefix) });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
let result;
|
|
191
|
+
try {
|
|
192
|
+
result = await latest.current.options.onSubmit?.(parsed.value);
|
|
193
|
+
} catch (thrown) {
|
|
194
|
+
apply(isFormProblem(thrown) ? thrown : { code: "server" });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (result && typeof result === "object" && "ok" in result && result.ok === false) {
|
|
198
|
+
apply(result.problem);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const data = result && typeof result === "object" && "ok" in result ? result.data : result;
|
|
202
|
+
latest.current.options.onSuccess?.(data);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
const known = (name) => name in api.fieldInfo;
|
|
206
|
+
function apply(problem) {
|
|
207
|
+
refused.current = true;
|
|
208
|
+
const { fields: fields2, form } = problemToErrors(problem, latest.current.i18n, known, latest.current.options.codeFields);
|
|
209
|
+
for (const [name, fieldError] of Object.entries(fields2)) {
|
|
210
|
+
api.setFieldMeta(name, (meta) => ({ ...meta, isBlurred: true, errorMap: { ...meta.errorMap, onServer: fieldError } }));
|
|
211
|
+
}
|
|
212
|
+
setError(form);
|
|
213
|
+
}
|
|
214
|
+
const submit = React.useCallback(async () => {
|
|
215
|
+
setError(null);
|
|
216
|
+
refused.current = false;
|
|
217
|
+
for (const name of Object.keys(api.fieldInfo)) {
|
|
218
|
+
if (api.getFieldMeta(name)?.errorMap?.onServer) api.setFieldMeta(name, (meta) => ({ ...meta, errorMap: { ...meta.errorMap, onServer: void 0 } }));
|
|
219
|
+
}
|
|
220
|
+
await api.handleSubmit();
|
|
221
|
+
const state = api.state;
|
|
222
|
+
return !refused.current && state.isSubmitSuccessful && state.isValid && !Object.values(state.fieldMeta).some((meta) => meta?.errorMap?.onServer);
|
|
223
|
+
}, [api]);
|
|
224
|
+
return React.useMemo(() => ({ api, i18n, error, submit }), [api, error, submit, i18n.t, i18n.prefix, i18n.apiPrefix, i18n.locale]);
|
|
225
|
+
}
|
|
226
|
+
var UNCHECKED = /* @__PURE__ */ Symbol("unchecked");
|
|
227
|
+
function useKrizakaField(name, options = {}) {
|
|
228
|
+
const ctx = useFormContext();
|
|
229
|
+
const id = React.useId();
|
|
230
|
+
const [checked, setChecked] = React.useState(UNCHECKED);
|
|
231
|
+
const check = options.validateAsync;
|
|
232
|
+
const field = useField({
|
|
233
|
+
form: ctx.api,
|
|
234
|
+
name,
|
|
235
|
+
...check ? {
|
|
236
|
+
asyncDebounceMs: options.asyncDebounceMs ?? 350,
|
|
237
|
+
validators: {
|
|
238
|
+
onChangeAsyncDebounceMs: options.asyncDebounceMs ?? 350,
|
|
239
|
+
onChangeAsync: async ({ value: value2, signal }) => {
|
|
240
|
+
const answer = await check(value2, signal);
|
|
241
|
+
if (signal.aborted) return void 0;
|
|
242
|
+
if (!answer) {
|
|
243
|
+
setChecked(value2);
|
|
244
|
+
return void 0;
|
|
245
|
+
}
|
|
246
|
+
setChecked(UNCHECKED);
|
|
247
|
+
const error = typeof answer === "string" ? { key: answer } : answer;
|
|
248
|
+
return { ...error, immediate: true, source: "check" };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} : {}
|
|
252
|
+
});
|
|
253
|
+
const attempts = useStore(ctx.api.store, (state) => state.submissionAttempts);
|
|
254
|
+
const meta = field.state.meta;
|
|
255
|
+
const map = meta.errorMap;
|
|
256
|
+
const all = [map.onServer, map.onChange, map.onBlur, map.onSubmit, map.onMount].filter(Boolean);
|
|
257
|
+
const first = all[0];
|
|
258
|
+
const detail = typeof first === "string" ? { key: first } : first;
|
|
259
|
+
const visible = detail && (detail.immediate || detail.source === "server" || meta.isBlurred || attempts > 0) ? detail : void 0;
|
|
260
|
+
const value = field.state.value;
|
|
261
|
+
return {
|
|
262
|
+
id,
|
|
263
|
+
name,
|
|
264
|
+
value,
|
|
265
|
+
setValue: (next) => {
|
|
266
|
+
if (map.onServer) ctx.api.setFieldMeta(name, (m) => ({ ...m, errorMap: { ...m.errorMap, onServer: void 0 } }));
|
|
267
|
+
field.handleChange(next);
|
|
268
|
+
},
|
|
269
|
+
onBlur: () => field.handleBlur(),
|
|
270
|
+
error: visible ? formMessage(visible, ctx.i18n) : void 0,
|
|
271
|
+
errorDetail: visible,
|
|
272
|
+
validating: Boolean(meta.isValidating),
|
|
273
|
+
available: Boolean(check) && checked !== UNCHECKED && Object.is(checked, value) && !meta.isValidating && all.length === 0,
|
|
274
|
+
hintId: `${id}-hint`,
|
|
275
|
+
errorId: `${id}-error`,
|
|
276
|
+
locale: ctx.i18n.locale ?? "en",
|
|
277
|
+
api: field
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function useKrizakaFieldArray(name) {
|
|
281
|
+
const ctx = useFormContext();
|
|
282
|
+
const field = useField({ form: ctx.api, name, mode: "array" });
|
|
283
|
+
const attempts = useStore(ctx.api.store, (state) => state.submissionAttempts);
|
|
284
|
+
const map = field.state.meta.errorMap;
|
|
285
|
+
const first = map.onServer ?? map.onChange ?? map.onSubmit;
|
|
286
|
+
return {
|
|
287
|
+
name,
|
|
288
|
+
items: field.state.value ?? [],
|
|
289
|
+
push: (item) => field.pushValue(item),
|
|
290
|
+
remove: (index) => void field.removeValue(index),
|
|
291
|
+
move: (from, to) => field.moveValue(from, to),
|
|
292
|
+
fieldName: (index, key) => `${name}[${index}]${key ? `.${key}` : ""}`,
|
|
293
|
+
error: first && (attempts > 0 || first.source === "server" || field.state.meta.isDirty) ? formMessage(first, ctx.i18n) : void 0
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function useFormValue(name) {
|
|
297
|
+
const ctx = useFormContext();
|
|
298
|
+
return useStore(ctx.api.store, (state) => getPath(state.values, name));
|
|
299
|
+
}
|
|
300
|
+
function useFormStatus() {
|
|
301
|
+
const ctx = useFormContext();
|
|
302
|
+
const submitting = useStore(ctx.api.store, (s) => s.isSubmitting);
|
|
303
|
+
const succeeded = useStore(ctx.api.store, (s) => s.isSubmitSuccessful);
|
|
304
|
+
const attempts = useStore(ctx.api.store, (s) => s.submissionAttempts);
|
|
305
|
+
return { submitting, succeeded: succeeded && !ctx.error, attempts };
|
|
306
|
+
}
|
|
307
|
+
function getPath(source, path) {
|
|
308
|
+
let current = source;
|
|
309
|
+
for (const part of path.replace(/\[(\d+)\]/g, ".$1").split(".")) {
|
|
310
|
+
if (current === null || typeof current !== "object") return void 0;
|
|
311
|
+
current = current[part];
|
|
312
|
+
}
|
|
313
|
+
return current;
|
|
314
|
+
}
|
|
315
|
+
function makeCreateKrizakaForm(components, Root) {
|
|
316
|
+
return function createKrizakaForm2(definition) {
|
|
317
|
+
function useForm2(options) {
|
|
318
|
+
const ctx = useKrizakaFormCore(definition, options);
|
|
319
|
+
const [store] = React.useState(() => createStore(ctx));
|
|
320
|
+
React.useLayoutEffect(() => store.set(ctx), [store, ctx]);
|
|
321
|
+
const Form = React.useCallback((props) => /* @__PURE__ */ jsx(Bound, { store, Root, props }), [store]);
|
|
322
|
+
return React.useMemo(
|
|
323
|
+
() => ({
|
|
324
|
+
...components,
|
|
325
|
+
Form,
|
|
326
|
+
submit: ctx.submit,
|
|
327
|
+
reset: (values) => ctx.api.reset(values),
|
|
328
|
+
setValue: (name, value) => ctx.api.setFieldValue(name, value),
|
|
329
|
+
getValues: () => ctx.api.state.values,
|
|
330
|
+
error: ctx.error,
|
|
331
|
+
api: ctx.api
|
|
332
|
+
}),
|
|
333
|
+
[ctx, Form]
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
schema: definition.schema,
|
|
338
|
+
defaultValues: definition.defaultValues,
|
|
339
|
+
useForm: useForm2,
|
|
340
|
+
useFormWithAction: (action, options) => useForm2({ ...options, onSubmit: action })
|
|
341
|
+
};
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function createStore(initial) {
|
|
345
|
+
let value = initial;
|
|
346
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
347
|
+
return {
|
|
348
|
+
get: () => value,
|
|
349
|
+
set: (next) => {
|
|
350
|
+
if (Object.is(next, value)) return;
|
|
351
|
+
value = next;
|
|
352
|
+
listeners.forEach((listener) => listener());
|
|
353
|
+
},
|
|
354
|
+
subscribe: (listener) => {
|
|
355
|
+
listeners.add(listener);
|
|
356
|
+
return () => listeners.delete(listener);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
function Bound({ store, Root, props }) {
|
|
361
|
+
const value = React.useSyncExternalStore(store.subscribe, store.get, store.get);
|
|
362
|
+
return /* @__PURE__ */ jsx(Root, { ...props, value });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/form/money.ts
|
|
366
|
+
import { currencyDigits } from "@krizaka/intl";
|
|
367
|
+
function decimalSeparator(locale) {
|
|
368
|
+
return new Intl.NumberFormat(locale).formatToParts(1.5).find((p) => p.type === "decimal")?.value ?? ".";
|
|
369
|
+
}
|
|
370
|
+
function currencySymbol(currency, locale) {
|
|
371
|
+
return new Intl.NumberFormat(locale, { style: "currency", currency }).formatToParts(0).find((p) => p.type === "currency")?.value ?? currency;
|
|
372
|
+
}
|
|
373
|
+
function parseMoney(text, currency, locale = "en") {
|
|
374
|
+
const digits = currencyDigits(currency);
|
|
375
|
+
const t = text.replace(/[\s\u00a0\u202f']/g, "");
|
|
376
|
+
if (!t) return void 0;
|
|
377
|
+
const negative = t.startsWith("-");
|
|
378
|
+
const body = negative ? t.slice(1) : t;
|
|
379
|
+
if (!/^[\d.,]+$/.test(body)) return null;
|
|
380
|
+
let int = body;
|
|
381
|
+
let frac = "";
|
|
382
|
+
const last = Math.max(body.lastIndexOf("."), body.lastIndexOf(","));
|
|
383
|
+
if (last >= 0) {
|
|
384
|
+
const tail = body.slice(last + 1);
|
|
385
|
+
if (body[last] === decimalSeparator(locale) || tail.length !== 3) {
|
|
386
|
+
if (tail.length > digits || !/^\d*$/.test(tail)) return null;
|
|
387
|
+
int = body.slice(0, last);
|
|
388
|
+
frac = tail;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (!/^(\d*|\d{1,3}([.,]\d{3})+)$/.test(int)) return null;
|
|
392
|
+
int = int.replace(/[.,]/g, "");
|
|
393
|
+
if (!int && !frac) return null;
|
|
394
|
+
const units = BigInt(int || "0") * 10n ** BigInt(digits) + BigInt((frac || "").padEnd(digits, "0") || "0");
|
|
395
|
+
const value = Number(negative ? -units : units);
|
|
396
|
+
return Number.isSafeInteger(value) ? value : null;
|
|
397
|
+
}
|
|
398
|
+
function moneyInputText(minorUnits, currency, locale = "en") {
|
|
399
|
+
if (minorUnits === null || minorUnits === void 0 || !Number.isFinite(minorUnits)) return "";
|
|
400
|
+
const digits = currencyDigits(currency);
|
|
401
|
+
const sign = minorUnits < 0 ? "-" : "";
|
|
402
|
+
const abs = String(Math.abs(Math.trunc(minorUnits))).padStart(digits + 1, "0");
|
|
403
|
+
return digits === 0 ? sign + abs : `${sign}${abs.slice(0, -digits)}${decimalSeparator(locale)}${abs.slice(-digits)}`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/form/web.tsx
|
|
407
|
+
import { Alert } from "@krizaka/ui/alert";
|
|
408
|
+
import { Button } from "@krizaka/ui/button";
|
|
409
|
+
import { Checkbox as CheckboxControl } from "@krizaka/ui/checkbox";
|
|
410
|
+
import { Combobox as ComboboxControl } from "@krizaka/ui/combobox";
|
|
411
|
+
import { DatePicker } from "@krizaka/ui/date-picker";
|
|
412
|
+
import { Field, Input, Select as SelectControl, Textarea } from "@krizaka/ui/field";
|
|
413
|
+
import { Dropzone, formatBytes } from "@krizaka/ui/file-upload";
|
|
414
|
+
import { RadioGroup as RadioGroupControl } from "@krizaka/ui/radio-group";
|
|
415
|
+
import { Slider as SliderControl } from "@krizaka/ui/slider";
|
|
416
|
+
import { Spinner } from "@krizaka/ui/spinner";
|
|
417
|
+
import { Switch as SwitchControl } from "@krizaka/ui/switch";
|
|
418
|
+
import { TagInput as TagInputControl } from "@krizaka/ui/tag-input";
|
|
419
|
+
import * as React2 from "react";
|
|
420
|
+
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
421
|
+
var ids = (f, hint) => [hint && !f.error ? f.hintId : null, f.error ? f.errorId : null].filter(Boolean).join(" ") || void 0;
|
|
422
|
+
function Caption({ field, hint, ok }) {
|
|
423
|
+
const suggestion = field.errorDetail?.suggestion;
|
|
424
|
+
if (field.error)
|
|
425
|
+
return /* @__PURE__ */ jsx2(Field.Error, { id: field.errorId, children: /* @__PURE__ */ jsxs("span", { children: [
|
|
426
|
+
field.error,
|
|
427
|
+
suggestion ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
428
|
+
" ",
|
|
429
|
+
/* @__PURE__ */ jsx2("button", { type: "button", className: "font-semibold text-accent-text underline underline-offset-2", onClick: () => field.setValue(suggestion.value), children: suggestion.label })
|
|
430
|
+
] }) : null
|
|
431
|
+
] }) });
|
|
432
|
+
const text = field.available && ok ? hint ? `${ok} \xB7 ${hint}` : ok : hint;
|
|
433
|
+
return text ? /* @__PURE__ */ jsx2(Field.Hint, { id: field.hintId, children: text }) : null;
|
|
434
|
+
}
|
|
435
|
+
var Check = () => /* @__PURE__ */ jsx2("svg", { viewBox: "0 0 24 24", className: "h-4 w-4 text-fg-success", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ jsx2("path", { d: "M20 6 9 17l-5-5" }) });
|
|
436
|
+
function FormRoot({ value, className, children, ...props }) {
|
|
437
|
+
const ref = React2.useRef(null);
|
|
438
|
+
return /* @__PURE__ */ jsx2(FormContext.Provider, { value, children: /* @__PURE__ */ jsx2(
|
|
439
|
+
"form",
|
|
440
|
+
{
|
|
441
|
+
ref,
|
|
442
|
+
noValidate: true,
|
|
443
|
+
className: className ?? "flex flex-col gap-4",
|
|
444
|
+
onSubmit: async (event) => {
|
|
445
|
+
event.preventDefault();
|
|
446
|
+
if (!await value.submit()) requestAnimationFrame(() => ref.current?.querySelector('[aria-invalid="true"], [data-invalid] input')?.focus());
|
|
447
|
+
},
|
|
448
|
+
...props,
|
|
449
|
+
children
|
|
450
|
+
}
|
|
451
|
+
) });
|
|
452
|
+
}
|
|
453
|
+
function Submit({ variant = "primary", children, ...props }) {
|
|
454
|
+
const { submitting } = useFormStatus();
|
|
455
|
+
return /* @__PURE__ */ jsx2(Button, { type: "submit", variant, loading: submitting, ...props, children });
|
|
456
|
+
}
|
|
457
|
+
function FormError({ title, className }) {
|
|
458
|
+
const ctx = useFormContext();
|
|
459
|
+
if (!ctx.error) return null;
|
|
460
|
+
return /* @__PURE__ */ jsx2(Alert, { tone: "danger", title, className, children: formMessage(ctx.error, ctx.i18n) });
|
|
461
|
+
}
|
|
462
|
+
function TextField({ name, label, hint, disabled, className, type = "text", transform, validateAsync, asyncDebounceMs, availableHint, checkingLabel = "Checking", start, ...input }) {
|
|
463
|
+
const f = useKrizakaField(name, { validateAsync, asyncDebounceMs });
|
|
464
|
+
const end = f.validating ? /* @__PURE__ */ jsx2(Spinner, { size: "sm", label: checkingLabel }) : f.available ? /* @__PURE__ */ jsx2(Check, {}) : null;
|
|
465
|
+
return /* @__PURE__ */ jsxs(Field.Root, { className, children: [
|
|
466
|
+
/* @__PURE__ */ jsx2(Field.Label, { htmlFor: f.id, children: label }),
|
|
467
|
+
/* @__PURE__ */ jsxs("span", { className: "relative flex items-center", children: [
|
|
468
|
+
start ? /* @__PURE__ */ jsx2("span", { className: "pointer-events-none absolute left-3 text-fg-muted", children: start }) : null,
|
|
469
|
+
/* @__PURE__ */ jsx2(
|
|
470
|
+
Input,
|
|
471
|
+
{
|
|
472
|
+
id: f.id,
|
|
473
|
+
name,
|
|
474
|
+
type,
|
|
475
|
+
value: f.value ?? "",
|
|
476
|
+
disabled,
|
|
477
|
+
invalid: Boolean(f.error),
|
|
478
|
+
"aria-describedby": ids(f, hint ?? availableHint),
|
|
479
|
+
onChange: (event) => f.setValue(transform ? transform(event.target.value) : event.target.value),
|
|
480
|
+
onBlur: f.onBlur,
|
|
481
|
+
className: [start ? "pl-9" : "", end ? "pr-10" : ""].join(" ").trim() || void 0,
|
|
482
|
+
...input
|
|
483
|
+
}
|
|
484
|
+
),
|
|
485
|
+
end ? /* @__PURE__ */ jsx2("span", { className: "absolute right-3 flex", children: end }) : null
|
|
486
|
+
] }),
|
|
487
|
+
/* @__PURE__ */ jsx2(Caption, { field: f, hint, ok: availableHint })
|
|
488
|
+
] });
|
|
489
|
+
}
|
|
490
|
+
function TextArea({ name, label, hint, disabled, className, rows = 4, ...input }) {
|
|
491
|
+
const f = useKrizakaField(name);
|
|
492
|
+
return /* @__PURE__ */ jsxs(Field.Root, { className, children: [
|
|
493
|
+
/* @__PURE__ */ jsx2(Field.Label, { htmlFor: f.id, children: label }),
|
|
494
|
+
/* @__PURE__ */ jsx2(Textarea, { id: f.id, name, rows, value: f.value ?? "", disabled, invalid: Boolean(f.error), "aria-describedby": ids(f, hint), onChange: (e) => f.setValue(e.target.value), onBlur: f.onBlur, ...input }),
|
|
495
|
+
/* @__PURE__ */ jsx2(Caption, { field: f, hint })
|
|
496
|
+
] });
|
|
497
|
+
}
|
|
498
|
+
function Checkbox({ name, label, hint, disabled, className }) {
|
|
499
|
+
const f = useKrizakaField(name);
|
|
500
|
+
return /* @__PURE__ */ jsxs("div", { className: className ?? "flex flex-col gap-1.5", children: [
|
|
501
|
+
/* @__PURE__ */ jsx2(CheckboxControl, { id: f.id, name, checked: f.value === true, disabled, invalid: Boolean(f.error), "aria-describedby": ids(f, hint), onCheckedChange: (c) => f.setValue(c === true), onBlur: f.onBlur, children: label }),
|
|
502
|
+
/* @__PURE__ */ jsx2(Caption, { field: f, hint })
|
|
503
|
+
] });
|
|
504
|
+
}
|
|
505
|
+
function Switch({ name, label, hint, disabled, className }) {
|
|
506
|
+
const f = useKrizakaField(name);
|
|
507
|
+
return /* @__PURE__ */ jsxs("div", { className: className ?? "flex items-start justify-between gap-4", children: [
|
|
508
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-0.5", children: [
|
|
509
|
+
/* @__PURE__ */ jsx2(Field.Label, { htmlFor: f.id, className: "text-sm font-medium text-fg", children: label }),
|
|
510
|
+
/* @__PURE__ */ jsx2(Caption, { field: f, hint })
|
|
511
|
+
] }),
|
|
512
|
+
/* @__PURE__ */ jsx2(SwitchControl, { id: f.id, name, checked: f.value === true, disabled, invalid: Boolean(f.error), "aria-describedby": ids(f, hint), onCheckedChange: (c) => f.setValue(c), onBlur: f.onBlur })
|
|
513
|
+
] });
|
|
514
|
+
}
|
|
515
|
+
function RadioGroup({ name, label, hint, disabled, className, options, variant = "list", orientation = "vertical" }) {
|
|
516
|
+
const f = useKrizakaField(name);
|
|
517
|
+
return /* @__PURE__ */ jsxs(Field.Root, { className, role: "group", "aria-labelledby": `${f.id}-label`, children: [
|
|
518
|
+
/* @__PURE__ */ jsx2("span", { id: `${f.id}-label`, className: "text-xs font-semibold text-fg-secondary", children: label }),
|
|
519
|
+
/* @__PURE__ */ jsx2(
|
|
520
|
+
RadioGroupControl.Root,
|
|
521
|
+
{
|
|
522
|
+
"aria-labelledby": `${f.id}-label`,
|
|
523
|
+
value: f.value ?? "",
|
|
524
|
+
onValueChange: (v) => f.setValue(v),
|
|
525
|
+
onBlur: f.onBlur,
|
|
526
|
+
orientation,
|
|
527
|
+
disabled,
|
|
528
|
+
invalid: Boolean(f.error),
|
|
529
|
+
"aria-describedby": ids(f, hint),
|
|
530
|
+
name,
|
|
531
|
+
className: variant === "cards" ? "grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-2" : void 0,
|
|
532
|
+
children: options.map(
|
|
533
|
+
(o) => variant === "cards" ? /* @__PURE__ */ jsxs(RadioGroupControl.Card, { value: o.value, disabled: o.disabled, children: [
|
|
534
|
+
/* @__PURE__ */ jsx2("span", { className: "font-semibold", children: o.label }),
|
|
535
|
+
o.description ? /* @__PURE__ */ jsx2("span", { className: "text-xs text-fg-secondary", children: o.description }) : null
|
|
536
|
+
] }, o.value) : /* @__PURE__ */ jsxs(RadioGroupControl.Item, { value: o.value, disabled: o.disabled, children: [
|
|
537
|
+
o.label,
|
|
538
|
+
o.description ? /* @__PURE__ */ jsx2("span", { className: "block text-xs text-fg-secondary", children: o.description }) : null
|
|
539
|
+
] }, o.value)
|
|
540
|
+
)
|
|
541
|
+
}
|
|
542
|
+
),
|
|
543
|
+
/* @__PURE__ */ jsx2(Caption, { field: f, hint })
|
|
544
|
+
] });
|
|
545
|
+
}
|
|
546
|
+
function Select({ name, label, hint, disabled, className, options, placeholder }) {
|
|
547
|
+
const f = useKrizakaField(name);
|
|
548
|
+
return /* @__PURE__ */ jsxs(Field.Root, { className, children: [
|
|
549
|
+
/* @__PURE__ */ jsx2(Field.Label, { htmlFor: f.id, children: label }),
|
|
550
|
+
/* @__PURE__ */ jsxs(SelectControl, { id: f.id, name, value: f.value ?? "", disabled, invalid: Boolean(f.error), "aria-describedby": ids(f, hint), onChange: (e) => f.setValue(e.target.value), onBlur: f.onBlur, children: [
|
|
551
|
+
placeholder !== void 0 ? /* @__PURE__ */ jsx2("option", { value: "", disabled: true, children: placeholder }) : null,
|
|
552
|
+
options.map((o) => /* @__PURE__ */ jsx2("option", { value: o.value, disabled: o.disabled, children: o.label }, o.value))
|
|
553
|
+
] }),
|
|
554
|
+
/* @__PURE__ */ jsx2(Caption, { field: f, hint })
|
|
555
|
+
] });
|
|
556
|
+
}
|
|
557
|
+
function Combobox({ name, label, hint, disabled, className, options = [], onSearch, multiple, creatable, placeholder, labels }) {
|
|
558
|
+
const f = useKrizakaField(name);
|
|
559
|
+
const [seen, setSeen] = React2.useState({});
|
|
560
|
+
const option = (value) => options.find((o) => o.value === value) ?? seen[value] ?? { value, label: value };
|
|
561
|
+
const remember = (o) => setSeen((known) => known[o.value] === o ? known : { ...known, [o.value]: o });
|
|
562
|
+
const shared = { id: f.id, label, hint, error: f.error, disabled, placeholder, labels, options, onSearch, creatable, name };
|
|
563
|
+
return /* @__PURE__ */ jsx2("div", { className, onBlur: (e) => !e.currentTarget.contains(e.relatedTarget) && f.onBlur(), children: multiple ? /* @__PURE__ */ jsx2(
|
|
564
|
+
ComboboxControl,
|
|
565
|
+
{
|
|
566
|
+
...shared,
|
|
567
|
+
multiple: true,
|
|
568
|
+
value: (f.value ?? []).map(option),
|
|
569
|
+
onValueChange: (next) => {
|
|
570
|
+
next.forEach(remember);
|
|
571
|
+
f.setValue(next.map((o) => o.value));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
) : /* @__PURE__ */ jsx2(
|
|
575
|
+
ComboboxControl,
|
|
576
|
+
{
|
|
577
|
+
...shared,
|
|
578
|
+
value: f.value ? option(f.value) : null,
|
|
579
|
+
onValueChange: (next) => {
|
|
580
|
+
if (next) remember(next);
|
|
581
|
+
f.setValue(next?.value ?? "");
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
) });
|
|
585
|
+
}
|
|
586
|
+
var PART_NAMES = { day: "Day", month: "Month", year: "Year" };
|
|
587
|
+
function DateField({ name, label, hint, disabled, className, entry = "calendar", min, max, labels, autoComplete }) {
|
|
588
|
+
const f = useKrizakaField(name);
|
|
589
|
+
if (entry === "calendar")
|
|
590
|
+
return /* @__PURE__ */ jsx2("div", { className, onBlur: (e) => !e.currentTarget.contains(e.relatedTarget) && f.onBlur(), children: /* @__PURE__ */ jsx2(DatePicker, { label, hint, error: f.error, disabled, name, locale: f.locale, min, max, labels, value: f.value || null, onValueChange: (v) => f.setValue(v ?? "") }) });
|
|
591
|
+
return /* @__PURE__ */ jsx2(DateParts, { field: f, label, hint, disabled, className, names: { ...PART_NAMES, ...labels }, autoComplete });
|
|
592
|
+
}
|
|
593
|
+
function DateParts({ field: f, label, hint, disabled, className, names, autoComplete }) {
|
|
594
|
+
const order = React2.useMemo(() => datePartsOrder(f.locale), [f.locale]);
|
|
595
|
+
const [parts, setParts] = React2.useState(() => splitDate(f.value));
|
|
596
|
+
if ((f.value ?? "") !== composeDate(parts) && composeDate(splitDate(f.value)) === (f.value ?? "")) setParts(splitDate(f.value));
|
|
597
|
+
const refs = { day: React2.useRef(null), month: React2.useRef(null), year: React2.useRef(null) };
|
|
598
|
+
return /* @__PURE__ */ jsxs("fieldset", { className: className ?? "flex flex-col gap-1.5", "aria-describedby": ids(f, hint), onBlur: (e) => !e.currentTarget.contains(e.relatedTarget) && f.onBlur(), children: [
|
|
599
|
+
/* @__PURE__ */ jsx2("legend", { className: "mb-1.5 text-xs font-semibold text-fg-secondary", children: label }),
|
|
600
|
+
/* @__PURE__ */ jsx2("div", { className: "flex gap-2", children: order.map((part) => /* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1 text-xs text-fg-secondary", children: [
|
|
601
|
+
names[part],
|
|
602
|
+
/* @__PURE__ */ jsx2(
|
|
603
|
+
Input,
|
|
604
|
+
{
|
|
605
|
+
ref: refs[part],
|
|
606
|
+
inputMode: "numeric",
|
|
607
|
+
autoComplete: autoComplete === "bday" ? `bday-${part}` : "off",
|
|
608
|
+
maxLength: DATE_PART_LENGTH[part],
|
|
609
|
+
value: parts[part],
|
|
610
|
+
disabled,
|
|
611
|
+
invalid: Boolean(f.error),
|
|
612
|
+
className: part === "year" ? "w-20 text-center tabular-nums" : "w-14 text-center tabular-nums",
|
|
613
|
+
onChange: (e) => {
|
|
614
|
+
const digits = e.target.value.replace(/\D/g, "").slice(0, DATE_PART_LENGTH[part]);
|
|
615
|
+
const next = { ...parts, [part]: digits };
|
|
616
|
+
setParts(next);
|
|
617
|
+
f.setValue(composeDate(next));
|
|
618
|
+
const following = order[order.indexOf(part) + 1];
|
|
619
|
+
if (digits.length === DATE_PART_LENGTH[part] && following) refs[following].current?.focus();
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
)
|
|
623
|
+
] }, part)) }),
|
|
624
|
+
/* @__PURE__ */ jsx2(Caption, { field: f, hint })
|
|
625
|
+
] });
|
|
626
|
+
}
|
|
627
|
+
function TagInput({ name, label, hint, disabled, className, ...props }) {
|
|
628
|
+
const f = useKrizakaField(name);
|
|
629
|
+
return /* @__PURE__ */ jsx2("div", { className, onBlur: (e) => !e.currentTarget.contains(e.relatedTarget) && f.onBlur(), children: /* @__PURE__ */ jsx2(TagInputControl, { label, hint, error: f.error, disabled, name, value: f.value ?? [], onValueChange: (tags) => f.setValue(tags), ...props }) });
|
|
630
|
+
}
|
|
631
|
+
function FileUpload({ name, label, hint, disabled, className, title, browseLabel, accept, maxSize, multiple, maxFiles, removeLabel, refusalLabel }) {
|
|
632
|
+
const f = useKrizakaField(name);
|
|
633
|
+
const files = f.value ?? [];
|
|
634
|
+
return /* @__PURE__ */ jsxs("div", { className: className ?? "flex flex-col gap-2", role: "group", "aria-label": label, onBlur: (e) => !e.currentTarget.contains(e.relatedTarget) && f.onBlur(), children: [
|
|
635
|
+
/* @__PURE__ */ jsx2("span", { className: "text-xs font-semibold text-fg-secondary", children: label }),
|
|
636
|
+
/* @__PURE__ */ jsx2(
|
|
637
|
+
Dropzone,
|
|
638
|
+
{
|
|
639
|
+
title,
|
|
640
|
+
browseLabel,
|
|
641
|
+
hint,
|
|
642
|
+
accept,
|
|
643
|
+
maxSize,
|
|
644
|
+
multiple,
|
|
645
|
+
maxFiles,
|
|
646
|
+
disabled,
|
|
647
|
+
refusalLabel,
|
|
648
|
+
onFiles: (chosen) => f.setValue(multiple ? [...files, ...chosen].slice(0, maxFiles ?? Infinity) : chosen.slice(0, 1))
|
|
649
|
+
}
|
|
650
|
+
),
|
|
651
|
+
files.length > 0 ? /* @__PURE__ */ jsx2("ul", { className: "flex flex-col gap-1.5", children: files.map((file, i) => /* @__PURE__ */ jsxs("li", { className: "flex items-center justify-between gap-3 rounded-lg border border-border-default bg-surface-1 px-3 py-2 text-sm", children: [
|
|
652
|
+
/* @__PURE__ */ jsx2("span", { className: "min-w-0 truncate text-fg", children: file.name }),
|
|
653
|
+
/* @__PURE__ */ jsxs("span", { className: "flex shrink-0 items-center gap-2", children: [
|
|
654
|
+
/* @__PURE__ */ jsx2("span", { className: "font-mono text-xs text-fg-secondary", children: formatBytes(file.size, f.locale) }),
|
|
655
|
+
/* @__PURE__ */ jsx2(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => f.setValue(files.filter((_, j) => j !== i)), children: removeLabel(file.name) })
|
|
656
|
+
] })
|
|
657
|
+
] }, `${file.name}-${i}`)) }) : null,
|
|
658
|
+
f.error ? /* @__PURE__ */ jsx2(Field.Error, { id: f.errorId, children: f.error }) : null
|
|
659
|
+
] });
|
|
660
|
+
}
|
|
661
|
+
function Money({ name, label, hint, disabled, className, currency, placeholder }) {
|
|
662
|
+
const f = useKrizakaField(name);
|
|
663
|
+
const [text, setText] = React2.useState(() => moneyInputText(f.value, currency, f.locale));
|
|
664
|
+
const [editing, setEditing] = React2.useState(false);
|
|
665
|
+
const shown = editing ? text : moneyInputText(f.value, currency, f.locale);
|
|
666
|
+
return /* @__PURE__ */ jsxs(Field.Root, { className, children: [
|
|
667
|
+
/* @__PURE__ */ jsx2(Field.Label, { htmlFor: f.id, children: label }),
|
|
668
|
+
/* @__PURE__ */ jsxs("span", { className: "relative flex items-center", children: [
|
|
669
|
+
/* @__PURE__ */ jsx2("span", { className: "pointer-events-none absolute left-3 text-sm text-fg-secondary", "aria-hidden": true, children: currencySymbol(currency, f.locale) }),
|
|
670
|
+
/* @__PURE__ */ jsx2(
|
|
671
|
+
Input,
|
|
672
|
+
{
|
|
673
|
+
id: f.id,
|
|
674
|
+
name,
|
|
675
|
+
inputMode: "decimal",
|
|
676
|
+
value: shown,
|
|
677
|
+
placeholder,
|
|
678
|
+
disabled,
|
|
679
|
+
invalid: Boolean(f.error),
|
|
680
|
+
"aria-describedby": ids(f, hint),
|
|
681
|
+
className: "pl-9 tabular-nums",
|
|
682
|
+
onFocus: () => {
|
|
683
|
+
setText(moneyInputText(f.value, currency, f.locale));
|
|
684
|
+
setEditing(true);
|
|
685
|
+
},
|
|
686
|
+
onChange: (e) => {
|
|
687
|
+
setText(e.target.value);
|
|
688
|
+
const units = parseMoney(e.target.value, currency, f.locale);
|
|
689
|
+
f.setValue(units === void 0 ? null : units === null ? Number.NaN : units);
|
|
690
|
+
},
|
|
691
|
+
onBlur: () => {
|
|
692
|
+
setEditing(false);
|
|
693
|
+
f.onBlur();
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
)
|
|
697
|
+
] }),
|
|
698
|
+
/* @__PURE__ */ jsx2(Caption, { field: f, hint })
|
|
699
|
+
] });
|
|
700
|
+
}
|
|
701
|
+
function Slider({ name, label, hint, disabled, className, min, max, step, formatValue, thumbLabels }) {
|
|
702
|
+
const f = useKrizakaField(name);
|
|
703
|
+
return /* @__PURE__ */ jsxs("div", { className: className ?? "flex flex-col gap-1.5", children: [
|
|
704
|
+
/* @__PURE__ */ jsx2(SliderControl, { label, showLabel: true, value: f.value, min, max, step, disabled, formatValue, thumbLabels, "aria-describedby": ids(f, hint), onValueChange: (v) => f.setValue(v), onValueCommit: () => f.onBlur() }),
|
|
705
|
+
/* @__PURE__ */ jsx2(Caption, { field: f, hint })
|
|
706
|
+
] });
|
|
707
|
+
}
|
|
708
|
+
function FormField({ name, validateAsync, asyncDebounceMs, children }) {
|
|
709
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: children(useKrizakaField(name, { validateAsync, asyncDebounceMs })) });
|
|
710
|
+
}
|
|
711
|
+
function FieldArray({ name, children }) {
|
|
712
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: children(useKrizakaFieldArray(name)) });
|
|
713
|
+
}
|
|
714
|
+
function Watch({ name, children }) {
|
|
715
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: children(useFormValue(name)) });
|
|
716
|
+
}
|
|
717
|
+
var fields = { TextField, TextArea, Checkbox, Switch, RadioGroup, Select, Combobox, DateField, TagInput, FileUpload, Money, Slider, Field: FormField, FieldArray, Watch, Submit, FormError };
|
|
718
|
+
var createKrizakaForm = makeCreateKrizakaForm(fields, FormRoot);
|
|
719
|
+
function useFormWithAction(form, ...args) {
|
|
720
|
+
return form.useFormWithAction(...args);
|
|
721
|
+
}
|
|
722
|
+
export {
|
|
723
|
+
Checkbox,
|
|
724
|
+
Combobox,
|
|
725
|
+
DateField,
|
|
726
|
+
FieldArray,
|
|
727
|
+
FileUpload,
|
|
728
|
+
FormError,
|
|
729
|
+
FormField,
|
|
730
|
+
Money,
|
|
731
|
+
RadioGroup,
|
|
732
|
+
Select,
|
|
733
|
+
Slider,
|
|
734
|
+
Submit,
|
|
735
|
+
Switch,
|
|
736
|
+
TagInput,
|
|
737
|
+
TextArea,
|
|
738
|
+
TextField,
|
|
739
|
+
Watch,
|
|
740
|
+
composeDate,
|
|
741
|
+
createKrizakaForm,
|
|
742
|
+
currencySymbol,
|
|
743
|
+
datePartsOrder,
|
|
744
|
+
formMessage,
|
|
745
|
+
formMessages,
|
|
746
|
+
isRealDate,
|
|
747
|
+
issueError,
|
|
748
|
+
issuesToFields,
|
|
749
|
+
latestBirthDate,
|
|
750
|
+
moneyInputText,
|
|
751
|
+
parseMoney,
|
|
752
|
+
problemToErrors,
|
|
753
|
+
useFormContext,
|
|
754
|
+
useFormStatus,
|
|
755
|
+
useFormValue,
|
|
756
|
+
useFormWithAction,
|
|
757
|
+
useKrizakaField,
|
|
758
|
+
useKrizakaFieldArray
|
|
759
|
+
};
|
|
760
|
+
//# sourceMappingURL=form.js.map
|