orynn 0.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/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/index.cjs +823 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +400 -0
- package/dist/index.d.ts +400 -0
- package/dist/index.js +809 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +201 -0
- package/package.json +82 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
var __typeError = (msg) => {
|
|
7
|
+
throw TypeError(msg);
|
|
8
|
+
};
|
|
9
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
10
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
11
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
12
|
+
|
|
13
|
+
// src/internal/cx.ts
|
|
14
|
+
function cx(...names) {
|
|
15
|
+
return names.filter(Boolean).join(" ");
|
|
16
|
+
}
|
|
17
|
+
function toMessages(error) {
|
|
18
|
+
if (error == null) return [];
|
|
19
|
+
return (Array.isArray(error) ? error : [error]).filter((m) => Boolean(m));
|
|
20
|
+
}
|
|
21
|
+
function joinIds(...ids) {
|
|
22
|
+
const list = ids.filter((id) => Boolean(id));
|
|
23
|
+
return list.length > 0 ? list.join(" ") : void 0;
|
|
24
|
+
}
|
|
25
|
+
function Field(props) {
|
|
26
|
+
const { label, description, error, required, disabled, className, children } = props;
|
|
27
|
+
const reactId = react.useId();
|
|
28
|
+
const id = props.id ?? reactId;
|
|
29
|
+
const messages = toMessages(error);
|
|
30
|
+
const invalid = messages.length > 0;
|
|
31
|
+
const descriptionId = description != null ? `${id}-description` : void 0;
|
|
32
|
+
const errorId = invalid ? `${id}-error` : void 0;
|
|
33
|
+
const describedById = joinIds(descriptionId, errorId);
|
|
34
|
+
const ctx = { id, describedById, invalid, required, disabled };
|
|
35
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
36
|
+
"div",
|
|
37
|
+
{
|
|
38
|
+
className: cx("orynn-field", className),
|
|
39
|
+
"data-invalid": invalid || void 0,
|
|
40
|
+
"data-disabled": disabled || void 0,
|
|
41
|
+
"data-required": required || void 0,
|
|
42
|
+
children: [
|
|
43
|
+
label != null && /* @__PURE__ */ jsxRuntime.jsx("label", { className: "orynn-field__label", htmlFor: id, children: label }),
|
|
44
|
+
description != null && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "orynn-field__description", id: descriptionId, children: description }),
|
|
45
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "orynn-field__control", children: typeof children === "function" ? children(ctx) : children }),
|
|
46
|
+
invalid && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "orynn-field__error", id: errorId, role: "alert", children: messages.length === 1 ? messages[0] : messages.map((m) => /* @__PURE__ */ jsxRuntime.jsx("span", { children: m }, m)) })
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
var Dropdown = /* @__PURE__ */ react.forwardRef(
|
|
52
|
+
function Dropdown2(props, ref) {
|
|
53
|
+
const {
|
|
54
|
+
label,
|
|
55
|
+
description,
|
|
56
|
+
error,
|
|
57
|
+
className,
|
|
58
|
+
options,
|
|
59
|
+
value,
|
|
60
|
+
onChange,
|
|
61
|
+
placeholder,
|
|
62
|
+
id,
|
|
63
|
+
required,
|
|
64
|
+
disabled,
|
|
65
|
+
onBlur,
|
|
66
|
+
onFocus,
|
|
67
|
+
...rest
|
|
68
|
+
} = props;
|
|
69
|
+
const handleChange = react.useCallback(
|
|
70
|
+
(event) => onChange?.(event.target.value, event),
|
|
71
|
+
[onChange]
|
|
72
|
+
);
|
|
73
|
+
const controlled = value !== void 0;
|
|
74
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
75
|
+
Field,
|
|
76
|
+
{
|
|
77
|
+
label,
|
|
78
|
+
description,
|
|
79
|
+
error,
|
|
80
|
+
required,
|
|
81
|
+
disabled,
|
|
82
|
+
id,
|
|
83
|
+
className,
|
|
84
|
+
children: ({ id: fieldId, describedById, invalid }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
85
|
+
"select",
|
|
86
|
+
{
|
|
87
|
+
...rest,
|
|
88
|
+
ref,
|
|
89
|
+
id: fieldId,
|
|
90
|
+
className: "orynn-dropdown",
|
|
91
|
+
required,
|
|
92
|
+
disabled,
|
|
93
|
+
"aria-invalid": invalid || void 0,
|
|
94
|
+
"aria-describedby": describedById,
|
|
95
|
+
...controlled ? { value } : {},
|
|
96
|
+
onChange: handleChange,
|
|
97
|
+
onBlur,
|
|
98
|
+
onFocus,
|
|
99
|
+
children: [
|
|
100
|
+
placeholder != null && /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", disabled: required, children: placeholder }),
|
|
101
|
+
options.map((option) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value))
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
var Input = /* @__PURE__ */ react.forwardRef(
|
|
110
|
+
function Input2(props, ref) {
|
|
111
|
+
const {
|
|
112
|
+
label,
|
|
113
|
+
description,
|
|
114
|
+
error,
|
|
115
|
+
className,
|
|
116
|
+
value,
|
|
117
|
+
onChange,
|
|
118
|
+
type = "text",
|
|
119
|
+
id,
|
|
120
|
+
required,
|
|
121
|
+
disabled,
|
|
122
|
+
readOnly,
|
|
123
|
+
onBlur,
|
|
124
|
+
onFocus,
|
|
125
|
+
...rest
|
|
126
|
+
} = props;
|
|
127
|
+
const handleChange = react.useCallback(
|
|
128
|
+
(event) => onChange?.(event.target.value, event),
|
|
129
|
+
[onChange]
|
|
130
|
+
);
|
|
131
|
+
const controlled = value !== void 0;
|
|
132
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
133
|
+
Field,
|
|
134
|
+
{
|
|
135
|
+
label,
|
|
136
|
+
description,
|
|
137
|
+
error,
|
|
138
|
+
required,
|
|
139
|
+
disabled,
|
|
140
|
+
id,
|
|
141
|
+
className,
|
|
142
|
+
children: ({ id: fieldId, describedById, invalid }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
143
|
+
"input",
|
|
144
|
+
{
|
|
145
|
+
...rest,
|
|
146
|
+
ref,
|
|
147
|
+
id: fieldId,
|
|
148
|
+
className: "orynn-input",
|
|
149
|
+
type,
|
|
150
|
+
required,
|
|
151
|
+
disabled,
|
|
152
|
+
readOnly,
|
|
153
|
+
"aria-invalid": invalid || void 0,
|
|
154
|
+
"aria-describedby": describedById,
|
|
155
|
+
...controlled ? { value } : {},
|
|
156
|
+
onChange: handleChange,
|
|
157
|
+
onBlur,
|
|
158
|
+
onFocus
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// src/internal/dev.ts
|
|
167
|
+
var __DEV__ = typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
168
|
+
function invariant(condition, message) {
|
|
169
|
+
if (condition) return;
|
|
170
|
+
if (__DEV__) {
|
|
171
|
+
throw new Error(`[orynn] ${message}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
var warned = /* @__PURE__ */ new Set();
|
|
175
|
+
function warnOnce(message) {
|
|
176
|
+
if (!__DEV__ || warned.has(message)) return;
|
|
177
|
+
warned.add(message);
|
|
178
|
+
console.warn(`[orynn] ${message}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/core/registry/registry.ts
|
|
182
|
+
var _entries;
|
|
183
|
+
var Registry = class {
|
|
184
|
+
constructor() {
|
|
185
|
+
__privateAdd(this, _entries, /* @__PURE__ */ new Map());
|
|
186
|
+
}
|
|
187
|
+
register(type, registration) {
|
|
188
|
+
if (__DEV__ && __privateGet(this, _entries).has(type)) {
|
|
189
|
+
warnOnce(`Field type "${type}" is being re-registered; the previous definition is replaced.`);
|
|
190
|
+
}
|
|
191
|
+
__privateGet(this, _entries).set(type, registration);
|
|
192
|
+
}
|
|
193
|
+
get(type) {
|
|
194
|
+
return __privateGet(this, _entries).get(type);
|
|
195
|
+
}
|
|
196
|
+
has(type) {
|
|
197
|
+
return __privateGet(this, _entries).has(type);
|
|
198
|
+
}
|
|
199
|
+
types() {
|
|
200
|
+
return [...__privateGet(this, _entries).keys()];
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
_entries = new WeakMap();
|
|
204
|
+
function createRegistry() {
|
|
205
|
+
return new Registry();
|
|
206
|
+
}
|
|
207
|
+
var defaultRegistry = /* @__PURE__ */ createRegistry();
|
|
208
|
+
function registerField(type, registration) {
|
|
209
|
+
defaultRegistry.register(type, registration);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/layout/resolve-columns.ts
|
|
213
|
+
var DEFAULT_COLUMNS = { desktop: 3, tablet: 2, mobile: 1 };
|
|
214
|
+
var clampCount = (n, fallback) => typeof n === "number" && Number.isFinite(n) && n >= 1 ? Math.floor(n) : fallback;
|
|
215
|
+
function resolveColumns(partial) {
|
|
216
|
+
return {
|
|
217
|
+
desktop: clampCount(partial?.desktop, DEFAULT_COLUMNS.desktop),
|
|
218
|
+
tablet: clampCount(partial?.tablet, DEFAULT_COLUMNS.tablet),
|
|
219
|
+
mobile: clampCount(partial?.mobile, DEFAULT_COLUMNS.mobile)
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function clampSpan(span, columnCount) {
|
|
223
|
+
const n = clampCount(span, 1);
|
|
224
|
+
return Math.min(n, Math.max(columnCount, 1));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/core/config/normalize.ts
|
|
228
|
+
function normalizeGap(gap) {
|
|
229
|
+
if (gap == null) return void 0;
|
|
230
|
+
return typeof gap === "number" ? `${gap}px` : gap;
|
|
231
|
+
}
|
|
232
|
+
function normalizeConfig(config, registry2) {
|
|
233
|
+
invariant(
|
|
234
|
+
config != null && typeof config === "object" && Array.isArray(config.fields),
|
|
235
|
+
"`config` must be an object with a `fields` array."
|
|
236
|
+
);
|
|
237
|
+
const seen = /* @__PURE__ */ new Set();
|
|
238
|
+
for (const field of config.fields) {
|
|
239
|
+
invariant(
|
|
240
|
+
field != null && typeof field === "object",
|
|
241
|
+
"Every entry in `config.fields` must be an object."
|
|
242
|
+
);
|
|
243
|
+
invariant(
|
|
244
|
+
typeof field.name === "string" && field.name.length > 0,
|
|
245
|
+
"Every field needs a non-empty string `name`."
|
|
246
|
+
);
|
|
247
|
+
invariant(!seen.has(field.name), `Duplicate field name "${field.name}" in config.fields.`);
|
|
248
|
+
seen.add(field.name);
|
|
249
|
+
invariant(
|
|
250
|
+
typeof field.type === "string" && field.type.length > 0,
|
|
251
|
+
`Field "${field.name}" is missing a "type".`
|
|
252
|
+
);
|
|
253
|
+
if (registry2) {
|
|
254
|
+
invariant(
|
|
255
|
+
registry2.has(field.type),
|
|
256
|
+
`Unknown field type "${field.type}" on field "${field.name}". Registered types: ${registry2.types().join(", ") || "(none)"}.`
|
|
257
|
+
);
|
|
258
|
+
const typeError = registry2.get(field.type)?.validateConfig?.(field);
|
|
259
|
+
invariant(typeError == null, `Field "${field.name}" (${field.type}): ${typeError}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
fields: config.fields,
|
|
264
|
+
columns: resolveColumns(config.columns),
|
|
265
|
+
gap: normalizeGap(config.gap),
|
|
266
|
+
legend: config.legend,
|
|
267
|
+
id: config.id
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/core/validation/messages.ts
|
|
272
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
273
|
+
var defaultMessages = {
|
|
274
|
+
required: ({ label }) => `${label} is required`,
|
|
275
|
+
minLength: ({ param, label }) => `${label} must be at least ${plural(param, "character")}`,
|
|
276
|
+
maxLength: ({ param, label }) => `${label} must be at most ${plural(param, "character")}`
|
|
277
|
+
};
|
|
278
|
+
var FALLBACK_LABEL = "This field";
|
|
279
|
+
function resolveMessage(token, param, label, overrides) {
|
|
280
|
+
const override = overrides?.[token];
|
|
281
|
+
if (override != null) return override;
|
|
282
|
+
const formatter = defaultMessages[token];
|
|
283
|
+
if (formatter) return formatter({ param, label: label ?? FALLBACK_LABEL });
|
|
284
|
+
return token;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/core/validation/rules.ts
|
|
288
|
+
var isEmpty = (value) => value == null || value === "" || Array.isArray(value) && value.length === 0;
|
|
289
|
+
var asLength = (value) => typeof value === "string" || Array.isArray(value) ? value.length : String(value ?? "").length;
|
|
290
|
+
var builtInRules = {
|
|
291
|
+
required: (value, param) => {
|
|
292
|
+
if (param === false) return null;
|
|
293
|
+
return isEmpty(value) ? "required" : null;
|
|
294
|
+
},
|
|
295
|
+
minLength: (value, param) => {
|
|
296
|
+
if (typeof param !== "number" || isEmpty(value)) return null;
|
|
297
|
+
return asLength(value) < param ? "minLength" : null;
|
|
298
|
+
},
|
|
299
|
+
maxLength: (value, param) => {
|
|
300
|
+
if (typeof param !== "number" || isEmpty(value)) return null;
|
|
301
|
+
return asLength(value) > param ? "maxLength" : null;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
var registry = new Map(Object.entries(builtInRules));
|
|
305
|
+
function registerRule(name, fn) {
|
|
306
|
+
registry.set(name, fn);
|
|
307
|
+
}
|
|
308
|
+
function getRule(name) {
|
|
309
|
+
return registry.get(name);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/core/validation/resolver.ts
|
|
313
|
+
function createRuleResolver() {
|
|
314
|
+
return (values, config) => {
|
|
315
|
+
const result = {};
|
|
316
|
+
for (const field of config.fields) {
|
|
317
|
+
const validation = field.validation;
|
|
318
|
+
if (!validation) continue;
|
|
319
|
+
const value = values[field.name];
|
|
320
|
+
const errors = [];
|
|
321
|
+
for (const key of Object.keys(validation)) {
|
|
322
|
+
if (key === "messages") continue;
|
|
323
|
+
const param = validation[key];
|
|
324
|
+
if (param === void 0 || param === false) continue;
|
|
325
|
+
const rule = getRule(key);
|
|
326
|
+
if (!rule) {
|
|
327
|
+
if (__DEV__) {
|
|
328
|
+
warnOnce(`No validator registered for rule "${key}" (field "${field.name}").`);
|
|
329
|
+
}
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const token = rule(value, param, values);
|
|
333
|
+
if (token == null) continue;
|
|
334
|
+
errors.push({
|
|
335
|
+
rule: key,
|
|
336
|
+
message: resolveMessage(token, param, field.label, validation.messages)
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (errors.length > 0) result[field.name] = errors;
|
|
340
|
+
}
|
|
341
|
+
return result;
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
var defaultResolver = /* @__PURE__ */ createRuleResolver();
|
|
345
|
+
|
|
346
|
+
// src/core/state/reducer.ts
|
|
347
|
+
var initialMeta = {
|
|
348
|
+
touched: {},
|
|
349
|
+
errors: {},
|
|
350
|
+
validating: false,
|
|
351
|
+
everValidated: false
|
|
352
|
+
};
|
|
353
|
+
function metaReducer(state, action) {
|
|
354
|
+
switch (action.type) {
|
|
355
|
+
case "touch": {
|
|
356
|
+
if (state.touched[action.name]) return state;
|
|
357
|
+
return { ...state, touched: { ...state.touched, [action.name]: true } };
|
|
358
|
+
}
|
|
359
|
+
case "touchAll": {
|
|
360
|
+
const touched = { ...state.touched };
|
|
361
|
+
for (const name of action.names) touched[name] = true;
|
|
362
|
+
return { ...state, touched };
|
|
363
|
+
}
|
|
364
|
+
case "validation:start":
|
|
365
|
+
return { ...state, validating: true };
|
|
366
|
+
case "validation:field": {
|
|
367
|
+
const errors = { ...state.errors };
|
|
368
|
+
if (action.errors.length > 0) errors[action.name] = action.errors;
|
|
369
|
+
else delete errors[action.name];
|
|
370
|
+
return { ...state, errors, validating: false, everValidated: true };
|
|
371
|
+
}
|
|
372
|
+
case "validation:all":
|
|
373
|
+
return { ...state, errors: action.result, validating: false, everValidated: true };
|
|
374
|
+
case "reset":
|
|
375
|
+
return { ...initialMeta };
|
|
376
|
+
default:
|
|
377
|
+
return state;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function deriveStatus(meta) {
|
|
381
|
+
if (meta.validating) return "validating";
|
|
382
|
+
if (!meta.everValidated) return "idle";
|
|
383
|
+
return Object.keys(meta.errors).length > 0 ? "invalid" : "valid";
|
|
384
|
+
}
|
|
385
|
+
function computeDirty(values, initial) {
|
|
386
|
+
const dirty = {};
|
|
387
|
+
for (const key of Object.keys(values)) {
|
|
388
|
+
if (!Object.is(values[key], initial[key])) dirty[key] = true;
|
|
389
|
+
}
|
|
390
|
+
return dirty;
|
|
391
|
+
}
|
|
392
|
+
function useLatestRef(value) {
|
|
393
|
+
const ref = react.useRef(value);
|
|
394
|
+
ref.current = value;
|
|
395
|
+
return ref;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/core/state/use-controllable-state.ts
|
|
399
|
+
function useControllableState({
|
|
400
|
+
value,
|
|
401
|
+
defaultValue,
|
|
402
|
+
onChange
|
|
403
|
+
}) {
|
|
404
|
+
const isControlled = value !== void 0;
|
|
405
|
+
const [uncontrolled, setUncontrolled] = react.useState(defaultValue);
|
|
406
|
+
const current = isControlled ? value : uncontrolled;
|
|
407
|
+
const currentRef = useLatestRef(current);
|
|
408
|
+
const onChangeRef = useLatestRef(onChange);
|
|
409
|
+
const isControlledRef = useLatestRef(isControlled);
|
|
410
|
+
const setValue = react.useCallback((next) => {
|
|
411
|
+
const resolved = typeof next === "function" ? next(currentRef.current) : next;
|
|
412
|
+
if (Object.is(resolved, currentRef.current)) return;
|
|
413
|
+
if (!isControlledRef.current) setUncontrolled(resolved);
|
|
414
|
+
onChangeRef.current?.(resolved);
|
|
415
|
+
}, []);
|
|
416
|
+
return [current, setValue];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/core/state/use-fieldset-state.ts
|
|
420
|
+
var NO_ERRORS = Object.freeze([]);
|
|
421
|
+
function computeInitialValues(config, registry2, provided) {
|
|
422
|
+
const out = {};
|
|
423
|
+
for (const field of config.fields) {
|
|
424
|
+
if (provided && Object.prototype.hasOwnProperty.call(provided, field.name)) {
|
|
425
|
+
out[field.name] = provided[field.name];
|
|
426
|
+
} else if (field.defaultValue !== void 0) {
|
|
427
|
+
out[field.name] = field.defaultValue;
|
|
428
|
+
} else {
|
|
429
|
+
out[field.name] = registry2?.get(field.type)?.defaultValue ?? "";
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
function useFieldsetState(config, options = {}) {
|
|
435
|
+
const {
|
|
436
|
+
value,
|
|
437
|
+
defaultValue,
|
|
438
|
+
onChange,
|
|
439
|
+
onValidationChange,
|
|
440
|
+
resolver = defaultResolver,
|
|
441
|
+
registry: registry2,
|
|
442
|
+
validateOn = "blur"
|
|
443
|
+
} = options;
|
|
444
|
+
const normalized = react.useMemo(() => normalizeConfig(config, registry2), [config, registry2]);
|
|
445
|
+
const initialRef = react.useRef(null);
|
|
446
|
+
if (initialRef.current === null) {
|
|
447
|
+
initialRef.current = computeInitialValues(
|
|
448
|
+
normalized,
|
|
449
|
+
registry2,
|
|
450
|
+
value ?? defaultValue
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
const [rawValues, setRawValues] = useControllableState({
|
|
454
|
+
value,
|
|
455
|
+
defaultValue: initialRef.current,
|
|
456
|
+
onChange
|
|
457
|
+
});
|
|
458
|
+
const values = react.useMemo(() => ({ ...initialRef.current, ...rawValues }), [rawValues]);
|
|
459
|
+
const [meta, dispatch] = react.useReducer(metaReducer, initialMeta);
|
|
460
|
+
const valuesRef = useLatestRef(values);
|
|
461
|
+
const resolverRef = useLatestRef(resolver);
|
|
462
|
+
const configRef = useLatestRef(config);
|
|
463
|
+
const normalizedRef = useLatestRef(normalized);
|
|
464
|
+
const registryRef = useLatestRef(registry2);
|
|
465
|
+
const validateOnRef = useLatestRef(validateOn);
|
|
466
|
+
const onValidationChangeRef = useLatestRef(onValidationChange);
|
|
467
|
+
const runIdRef = react.useRef(0);
|
|
468
|
+
const runValidation = react.useCallback(async (scope) => {
|
|
469
|
+
const runId = ++runIdRef.current;
|
|
470
|
+
const input = scope.values ?? valuesRef.current;
|
|
471
|
+
dispatch({ type: "validation:start" });
|
|
472
|
+
const result = await resolverRef.current(input, configRef.current, {
|
|
473
|
+
trigger: scope.trigger,
|
|
474
|
+
...scope.field ? { changedField: scope.field } : {}
|
|
475
|
+
});
|
|
476
|
+
if (runId !== runIdRef.current) return result;
|
|
477
|
+
if (scope.field) {
|
|
478
|
+
dispatch({ type: "validation:field", name: scope.field, errors: result[scope.field] ?? [] });
|
|
479
|
+
} else {
|
|
480
|
+
dispatch({ type: "validation:all", result });
|
|
481
|
+
onValidationChangeRef.current?.(result);
|
|
482
|
+
}
|
|
483
|
+
return result;
|
|
484
|
+
}, []);
|
|
485
|
+
const setFieldValue = react.useCallback(
|
|
486
|
+
(name, next) => {
|
|
487
|
+
setRawValues((prev) => ({ ...prev, [name]: next }));
|
|
488
|
+
if (validateOnRef.current === "change") {
|
|
489
|
+
void runValidation({
|
|
490
|
+
field: name,
|
|
491
|
+
trigger: "change",
|
|
492
|
+
values: { ...valuesRef.current, [name]: next }
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
[setRawValues, runValidation]
|
|
497
|
+
);
|
|
498
|
+
const setValues = react.useCallback((next) => setRawValues(next), [setRawValues]);
|
|
499
|
+
const markTouched = react.useCallback(
|
|
500
|
+
(name) => {
|
|
501
|
+
dispatch({ type: "touch", name });
|
|
502
|
+
if (validateOnRef.current === "blur") {
|
|
503
|
+
void runValidation({ field: name, trigger: "blur" });
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
[runValidation]
|
|
507
|
+
);
|
|
508
|
+
const validate = react.useCallback(() => runValidation({ trigger: "submit" }), [runValidation]);
|
|
509
|
+
const handleSubmit = react.useCallback(
|
|
510
|
+
(onValid, onInvalid) => async (event) => {
|
|
511
|
+
event?.preventDefault?.();
|
|
512
|
+
dispatch({
|
|
513
|
+
type: "touchAll",
|
|
514
|
+
names: normalizedRef.current.fields.map((f) => f.name)
|
|
515
|
+
});
|
|
516
|
+
const result = await runValidation({ trigger: "submit" });
|
|
517
|
+
if (Object.keys(result).length === 0) onValid(valuesRef.current);
|
|
518
|
+
else onInvalid?.(result);
|
|
519
|
+
},
|
|
520
|
+
[runValidation]
|
|
521
|
+
);
|
|
522
|
+
const reset = react.useCallback(
|
|
523
|
+
(next) => {
|
|
524
|
+
const fresh = computeInitialValues(
|
|
525
|
+
normalizedRef.current,
|
|
526
|
+
registryRef.current,
|
|
527
|
+
next
|
|
528
|
+
);
|
|
529
|
+
initialRef.current = fresh;
|
|
530
|
+
setRawValues(fresh);
|
|
531
|
+
dispatch({ type: "reset" });
|
|
532
|
+
runIdRef.current++;
|
|
533
|
+
},
|
|
534
|
+
[setRawValues]
|
|
535
|
+
);
|
|
536
|
+
const handlerCacheRef = react.useRef(
|
|
537
|
+
/* @__PURE__ */ new Map()
|
|
538
|
+
);
|
|
539
|
+
const getHandlers = react.useCallback(
|
|
540
|
+
(name) => {
|
|
541
|
+
const cache = handlerCacheRef.current;
|
|
542
|
+
let entry = cache.get(name);
|
|
543
|
+
if (!entry) {
|
|
544
|
+
entry = {
|
|
545
|
+
setValue: (v) => setFieldValue(name, v),
|
|
546
|
+
markTouched: () => markTouched(name)
|
|
547
|
+
};
|
|
548
|
+
cache.set(name, entry);
|
|
549
|
+
}
|
|
550
|
+
return entry;
|
|
551
|
+
},
|
|
552
|
+
[setFieldValue, markTouched]
|
|
553
|
+
);
|
|
554
|
+
const dirty = react.useMemo(() => computeDirty(values, initialRef.current ?? {}), [values]);
|
|
555
|
+
const status = deriveStatus(meta);
|
|
556
|
+
const isValid = Object.keys(meta.errors).length === 0;
|
|
557
|
+
const getFieldProps = react.useCallback(
|
|
558
|
+
(name) => {
|
|
559
|
+
const handlers = getHandlers(name);
|
|
560
|
+
return {
|
|
561
|
+
name,
|
|
562
|
+
value: values[name],
|
|
563
|
+
errors: meta.errors[name] ?? NO_ERRORS,
|
|
564
|
+
touched: meta.touched[name] ?? false,
|
|
565
|
+
dirty: dirty[name] ?? false,
|
|
566
|
+
setValue: handlers.setValue,
|
|
567
|
+
markTouched: handlers.markTouched
|
|
568
|
+
};
|
|
569
|
+
},
|
|
570
|
+
[values, meta.errors, meta.touched, dirty, getHandlers]
|
|
571
|
+
);
|
|
572
|
+
const state = react.useMemo(
|
|
573
|
+
() => ({
|
|
574
|
+
values,
|
|
575
|
+
touched: meta.touched,
|
|
576
|
+
dirty,
|
|
577
|
+
errors: meta.errors,
|
|
578
|
+
status
|
|
579
|
+
}),
|
|
580
|
+
[values, meta.touched, dirty, meta.errors, status]
|
|
581
|
+
);
|
|
582
|
+
return {
|
|
583
|
+
config: normalized,
|
|
584
|
+
state,
|
|
585
|
+
values,
|
|
586
|
+
errors: meta.errors,
|
|
587
|
+
touched: meta.touched,
|
|
588
|
+
dirty,
|
|
589
|
+
status,
|
|
590
|
+
isValid,
|
|
591
|
+
setFieldValue,
|
|
592
|
+
setValues,
|
|
593
|
+
reset,
|
|
594
|
+
validate,
|
|
595
|
+
handleSubmit,
|
|
596
|
+
getFieldProps
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
function FieldGrid({ columns, gap, className, children }) {
|
|
600
|
+
const style = {
|
|
601
|
+
"--orynn-cols-desktop": columns.desktop,
|
|
602
|
+
"--orynn-cols-tablet": columns.tablet,
|
|
603
|
+
"--orynn-cols-mobile": columns.mobile,
|
|
604
|
+
...gap ? { "--orynn-grid-gap": gap } : {}
|
|
605
|
+
};
|
|
606
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cx("orynn-grid", className), style, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "orynn-grid__track", children }) });
|
|
607
|
+
}
|
|
608
|
+
function FieldGridItem({ span, columns, children }) {
|
|
609
|
+
if (!span) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
|
|
610
|
+
const style = {
|
|
611
|
+
"--orynn-span-desktop": clampSpan(span.desktop, columns.desktop),
|
|
612
|
+
"--orynn-span-tablet": clampSpan(span.tablet, columns.tablet),
|
|
613
|
+
"--orynn-span-mobile": clampSpan(span.mobile, columns.mobile)
|
|
614
|
+
};
|
|
615
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "orynn-grid__item", style, children });
|
|
616
|
+
}
|
|
617
|
+
var noop = () => {
|
|
618
|
+
};
|
|
619
|
+
function FieldRendererImpl({ field, registry: registry2, slice, disabled }) {
|
|
620
|
+
const registration = registry2.get(field.type);
|
|
621
|
+
invariant(
|
|
622
|
+
registration,
|
|
623
|
+
`No control registered for field type "${field.type}" (field "${field.name}").`
|
|
624
|
+
);
|
|
625
|
+
if (!registration) return null;
|
|
626
|
+
const Control = registration.component;
|
|
627
|
+
const required = field.validation?.required === true;
|
|
628
|
+
const isDisabled = disabled === true || field.disabled === true;
|
|
629
|
+
const value = registration.parseValue ? registration.parseValue(slice.value) : slice.value;
|
|
630
|
+
const messages = slice.errors.map((e) => e.message);
|
|
631
|
+
const handleChange = (next) => {
|
|
632
|
+
slice.setValue(registration.formatValue ? registration.formatValue(next) : next);
|
|
633
|
+
};
|
|
634
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
635
|
+
Field,
|
|
636
|
+
{
|
|
637
|
+
label: field.label,
|
|
638
|
+
description: field.description,
|
|
639
|
+
error: messages,
|
|
640
|
+
required,
|
|
641
|
+
disabled: isDisabled,
|
|
642
|
+
children: ({ id, describedById, invalid }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
643
|
+
Control,
|
|
644
|
+
{
|
|
645
|
+
id,
|
|
646
|
+
name: field.name,
|
|
647
|
+
value,
|
|
648
|
+
onChange: handleChange,
|
|
649
|
+
onBlur: slice.markTouched,
|
|
650
|
+
onFocus: noop,
|
|
651
|
+
disabled: isDisabled,
|
|
652
|
+
readOnly: field.readOnly === true,
|
|
653
|
+
required,
|
|
654
|
+
invalid,
|
|
655
|
+
describedById,
|
|
656
|
+
config: field
|
|
657
|
+
}
|
|
658
|
+
)
|
|
659
|
+
}
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
var FieldRenderer = /* @__PURE__ */ react.memo(
|
|
663
|
+
FieldRendererImpl,
|
|
664
|
+
(prev, next) => prev.field === next.field && prev.registry === next.registry && prev.disabled === next.disabled && prev.slice.value === next.slice.value && prev.slice.touched === next.slice.touched && prev.slice.errors === next.slice.errors && prev.slice.setValue === next.slice.setValue && prev.slice.markTouched === next.slice.markTouched
|
|
665
|
+
);
|
|
666
|
+
var toStringValue = (value) => typeof value === "string" ? value : value == null ? "" : String(value);
|
|
667
|
+
var InputControl = ({
|
|
668
|
+
id,
|
|
669
|
+
name,
|
|
670
|
+
value,
|
|
671
|
+
onChange,
|
|
672
|
+
onBlur,
|
|
673
|
+
onFocus,
|
|
674
|
+
disabled,
|
|
675
|
+
readOnly,
|
|
676
|
+
required,
|
|
677
|
+
invalid,
|
|
678
|
+
describedById,
|
|
679
|
+
config
|
|
680
|
+
}) => {
|
|
681
|
+
const field = config;
|
|
682
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
683
|
+
"input",
|
|
684
|
+
{
|
|
685
|
+
id,
|
|
686
|
+
name,
|
|
687
|
+
className: "orynn-input",
|
|
688
|
+
type: field.inputType ?? "text",
|
|
689
|
+
value: toStringValue(value),
|
|
690
|
+
placeholder: field.placeholder,
|
|
691
|
+
required,
|
|
692
|
+
disabled,
|
|
693
|
+
readOnly,
|
|
694
|
+
"aria-invalid": invalid || void 0,
|
|
695
|
+
"aria-describedby": describedById,
|
|
696
|
+
onChange: (event) => onChange(event.target.value, event),
|
|
697
|
+
onBlur,
|
|
698
|
+
onFocus,
|
|
699
|
+
...field.props
|
|
700
|
+
}
|
|
701
|
+
);
|
|
702
|
+
};
|
|
703
|
+
var DropdownControl = ({
|
|
704
|
+
id,
|
|
705
|
+
name,
|
|
706
|
+
value,
|
|
707
|
+
onChange,
|
|
708
|
+
onBlur,
|
|
709
|
+
onFocus,
|
|
710
|
+
disabled,
|
|
711
|
+
required,
|
|
712
|
+
invalid,
|
|
713
|
+
describedById,
|
|
714
|
+
config
|
|
715
|
+
}) => {
|
|
716
|
+
const field = config;
|
|
717
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
718
|
+
"select",
|
|
719
|
+
{
|
|
720
|
+
id,
|
|
721
|
+
name,
|
|
722
|
+
className: "orynn-dropdown",
|
|
723
|
+
value: toStringValue(value),
|
|
724
|
+
required,
|
|
725
|
+
disabled,
|
|
726
|
+
"aria-invalid": invalid || void 0,
|
|
727
|
+
"aria-describedby": describedById,
|
|
728
|
+
onChange: (event) => onChange(event.target.value, event),
|
|
729
|
+
onBlur,
|
|
730
|
+
onFocus,
|
|
731
|
+
...field.props,
|
|
732
|
+
children: [
|
|
733
|
+
field.placeholder != null && /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", disabled: required, children: field.placeholder }),
|
|
734
|
+
field.options.map((option) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value))
|
|
735
|
+
]
|
|
736
|
+
}
|
|
737
|
+
);
|
|
738
|
+
};
|
|
739
|
+
function registerBuiltins(registry2) {
|
|
740
|
+
if (!registry2.has("input")) {
|
|
741
|
+
registry2.register("input", { component: InputControl, defaultValue: "" });
|
|
742
|
+
}
|
|
743
|
+
if (!registry2.has("dropdown")) {
|
|
744
|
+
registry2.register("dropdown", {
|
|
745
|
+
component: DropdownControl,
|
|
746
|
+
defaultValue: "",
|
|
747
|
+
validateConfig: (config) => Array.isArray(config.options) ? null : "a dropdown field requires an `options` array"
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
var seeded = false;
|
|
752
|
+
function ensureBuiltins(defaultRegistry2) {
|
|
753
|
+
if (seeded) return;
|
|
754
|
+
seeded = true;
|
|
755
|
+
registerBuiltins(defaultRegistry2);
|
|
756
|
+
}
|
|
757
|
+
function Fieldset(props) {
|
|
758
|
+
const {
|
|
759
|
+
config,
|
|
760
|
+
value,
|
|
761
|
+
defaultValue,
|
|
762
|
+
onChange,
|
|
763
|
+
onValidationChange,
|
|
764
|
+
resolver,
|
|
765
|
+
registry: registry2,
|
|
766
|
+
validateOn,
|
|
767
|
+
disabled,
|
|
768
|
+
className,
|
|
769
|
+
id
|
|
770
|
+
} = props;
|
|
771
|
+
ensureBuiltins(defaultRegistry);
|
|
772
|
+
const activeRegistry = registry2 ?? defaultRegistry;
|
|
773
|
+
const options = {
|
|
774
|
+
registry: activeRegistry,
|
|
775
|
+
...value !== void 0 ? { value } : {},
|
|
776
|
+
...defaultValue !== void 0 ? { defaultValue } : {},
|
|
777
|
+
...onChange ? { onChange } : {},
|
|
778
|
+
...onValidationChange ? { onValidationChange } : {},
|
|
779
|
+
...resolver ? { resolver } : {},
|
|
780
|
+
...validateOn ? { validateOn } : {}
|
|
781
|
+
};
|
|
782
|
+
const fs = useFieldsetState(config, options);
|
|
783
|
+
const { columns, gap, legend, fields, id: configId } = fs.config;
|
|
784
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
785
|
+
"fieldset",
|
|
786
|
+
{
|
|
787
|
+
className: cx("orynn-fieldset", className),
|
|
788
|
+
id: id ?? configId,
|
|
789
|
+
disabled: disabled || void 0,
|
|
790
|
+
children: [
|
|
791
|
+
legend != null && /* @__PURE__ */ jsxRuntime.jsx("legend", { className: "orynn-fieldset__legend", children: legend }),
|
|
792
|
+
/* @__PURE__ */ jsxRuntime.jsx(FieldGrid, { columns, gap, children: fields.map((field) => /* @__PURE__ */ jsxRuntime.jsx(FieldGridItem, { span: field.layout, columns, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
793
|
+
FieldRenderer,
|
|
794
|
+
{
|
|
795
|
+
field,
|
|
796
|
+
registry: activeRegistry,
|
|
797
|
+
slice: fs.getFieldProps(field.name),
|
|
798
|
+
disabled: disabled === true
|
|
799
|
+
}
|
|
800
|
+
) }, field.name)) })
|
|
801
|
+
]
|
|
802
|
+
}
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/types/validation.ts
|
|
807
|
+
var FORM_ERROR_KEY = "$form";
|
|
808
|
+
|
|
809
|
+
exports.Dropdown = Dropdown;
|
|
810
|
+
exports.FORM_ERROR_KEY = FORM_ERROR_KEY;
|
|
811
|
+
exports.Field = Field;
|
|
812
|
+
exports.Fieldset = Fieldset;
|
|
813
|
+
exports.Input = Input;
|
|
814
|
+
exports.createRegistry = createRegistry;
|
|
815
|
+
exports.createRuleResolver = createRuleResolver;
|
|
816
|
+
exports.defaultRegistry = defaultRegistry;
|
|
817
|
+
exports.defaultResolver = defaultResolver;
|
|
818
|
+
exports.normalizeConfig = normalizeConfig;
|
|
819
|
+
exports.registerField = registerField;
|
|
820
|
+
exports.registerRule = registerRule;
|
|
821
|
+
exports.useFieldsetState = useFieldsetState;
|
|
822
|
+
//# sourceMappingURL=index.cjs.map
|
|
823
|
+
//# sourceMappingURL=index.cjs.map
|