@coffer-org/sdk 1.1.0 → 1.2.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 (69) hide show
  1. package/dist/color-names.d.ts +0 -1
  2. package/dist/color-names.js +0 -1
  3. package/dist/condition.d.ts +3 -19
  4. package/dist/condition.js +50 -69
  5. package/dist/countries.d.ts +0 -2
  6. package/dist/countries.js +0 -1
  7. package/dist/currencies.d.ts +0 -10
  8. package/dist/currencies.js +0 -10
  9. package/dist/extend.d.ts +3 -22
  10. package/dist/extend.js +2 -6
  11. package/dist/field-helpers.d.ts +0 -7
  12. package/dist/field-helpers.js +0 -16
  13. package/dist/field-presets.d.ts +55 -87
  14. package/dist/field-presets.js +116 -108
  15. package/dist/fields/constants.d.ts +36 -0
  16. package/dist/fields/constants.js +18 -0
  17. package/dist/fields/normalize.d.ts +84 -0
  18. package/dist/fields/normalize.js +47 -0
  19. package/dist/fields/url.d.ts +13 -0
  20. package/dist/fields/url.js +49 -0
  21. package/dist/fields/validation.d.ts +30 -0
  22. package/dist/fields/validation.js +57 -0
  23. package/dist/fields.d.ts +222 -332
  24. package/dist/fields.js +435 -351
  25. package/dist/index.d.ts +14 -16
  26. package/dist/index.js +14 -16
  27. package/dist/module.d.ts +13 -103
  28. package/dist/module.js +51 -83
  29. package/dist/package.d.ts +3 -20
  30. package/dist/package.js +14 -19
  31. package/dist/plugin.d.ts +5 -45
  32. package/dist/plugin.js +5 -19
  33. package/dist/settings.d.ts +1 -2
  34. package/dist/settings.js +0 -1
  35. package/dist/units.d.ts +0 -1
  36. package/dist/units.js +0 -5
  37. package/dist/vault.d.ts +3 -8
  38. package/dist/vault.js +4 -1
  39. package/package.json +5 -1
  40. package/dist/color-names.d.ts.map +0 -1
  41. package/dist/color-names.js.map +0 -1
  42. package/dist/condition.d.ts.map +0 -1
  43. package/dist/condition.js.map +0 -1
  44. package/dist/countries.d.ts.map +0 -1
  45. package/dist/countries.js.map +0 -1
  46. package/dist/currencies.d.ts.map +0 -1
  47. package/dist/currencies.js.map +0 -1
  48. package/dist/extend.d.ts.map +0 -1
  49. package/dist/extend.js.map +0 -1
  50. package/dist/field-helpers.d.ts.map +0 -1
  51. package/dist/field-helpers.js.map +0 -1
  52. package/dist/field-presets.d.ts.map +0 -1
  53. package/dist/field-presets.js.map +0 -1
  54. package/dist/fields.d.ts.map +0 -1
  55. package/dist/fields.js.map +0 -1
  56. package/dist/index.d.ts.map +0 -1
  57. package/dist/index.js.map +0 -1
  58. package/dist/module.d.ts.map +0 -1
  59. package/dist/module.js.map +0 -1
  60. package/dist/package.d.ts.map +0 -1
  61. package/dist/package.js.map +0 -1
  62. package/dist/plugin.d.ts.map +0 -1
  63. package/dist/plugin.js.map +0 -1
  64. package/dist/settings.d.ts.map +0 -1
  65. package/dist/settings.js.map +0 -1
  66. package/dist/units.d.ts.map +0 -1
  67. package/dist/units.js.map +0 -1
  68. package/dist/vault.d.ts.map +0 -1
  69. package/dist/vault.js.map +0 -1
package/dist/fields.js CHANGED
@@ -1,42 +1,31 @@
1
- /**
2
- * Система типів полів — серце платформи. Чистий, ізоморфний модуль.
3
- * Один опис поля → три споживачі:
4
- * 1. `zod` — валідація (однакова на сервері й клієнті)
5
- * 2. `column` — тип колонки в БД
6
- * 3. `prim`/`kind` — ключі для резолву рендера (див. web/render/registry)
7
- */
8
1
  import { z } from 'zod';
9
- import ISO6391 from 'iso-639-1';
10
- import { resolveUnits } from './units.js';
11
- import { isCurrencyCode } from './currencies.js';
12
- // Пресети тонкі обгортки над примітивами нижче. Циклічний імпорт безпечний:
13
- // преcети викликають фабрики лише в тілі функцій (фабрики — hoisted-декларації).
14
- import { presets } from './field-presets.js';
2
+ import { resolveUnits } from "./units.js";
3
+ import { isCurrencyCode } from "./currencies.js";
4
+ import { vmsg, reqErr, typeErr, reqTypeErr, jsonRefined, optionalize, jsonValue } from "./fields/validation.js";
5
+ export { vmsg, reqErr, typeErr, reqTypeErr, jsonRefined, optionalize, jsonValue, decodeVmsg, validateField } from "./fields/validation.js";
6
+ export { normalizeOpts } from "./fields/normalize.js";
7
+ import { normalizeOpts } from "./fields/normalize.js";
8
+ import { presets } from "./field-presets.js";
15
9
  export const isField = (x) => 'key' in x && !('el' in x);
16
10
  export const isStatic = (x) => 'el' in x && x.el === 'static';
17
11
  export const isGroup = (x) => 'el' in x && x.el === 'group';
18
12
  export const isDivider = (x) => 'el' in x && x.el === 'divider';
19
13
  export const isInfo = (x) => 'el' in x && x.el === 'info';
20
14
  export const isButton = (x) => 'el' in x && x.el === 'button';
21
- // ─── Layout-фабрики ──────────────────────────────────────────────────────────
22
- /**
23
- * Візуальна або storage-група полів (один рівень вкладеності — груп усередині груп нема).
24
- * Без `key` → layout-група (не зберігається). З `key` → storage:
25
- * - embedded (key, без multiple): зберігається як колонка або вкладений об'єкт.
26
- * - collection (key + multiple): child-таблиця (масив рядків).
27
- * Storage-група вимагає `label` і лише `FieldItem`-нащадків (зі storage key).
28
- */
15
+ export const isBreak = (x) => 'el' in x && x.el === 'break';
29
16
  export function group(o) {
17
+ const r = o.rules ?? {};
18
+ const v = o.view ?? {};
30
19
  if (o.key) {
31
20
  if (!o.label)
32
- throw new Error(`[field.group] storage-група '${o.key}' потребує label`);
21
+ throw new Error(`[field.group] storage group '${o.key}' requires a label`);
33
22
  for (const f of o.fields) {
34
23
  if (!('key' in f))
35
- throw new Error(`[field.group] storage-група '${o.key}': діти мусять бути FieldItem (мати key)`);
24
+ throw new Error(`[field.group] storage group '${o.key}': children must be FieldItem (have a key)`);
36
25
  }
37
- for (const k of o.unique ?? []) {
26
+ for (const k of r.unique ?? []) {
38
27
  if (!o.fields.some((f) => 'key' in f && f.key === k))
39
- throw new Error(`[field.group] '${o.key}': unique-ключ '${k}' не є сабфілдом`);
28
+ throw new Error(`[field.group] '${o.key}': unique key '${k}' is not a subfield`);
40
29
  }
41
30
  }
42
31
  return {
@@ -44,120 +33,143 @@ export function group(o) {
44
33
  key: o.key,
45
34
  multiple: o.multiple,
46
35
  required: o.required,
47
- unique: o.unique,
36
+ unique: r.unique,
48
37
  label: o.label,
49
- icon: o.icon,
38
+ icon: v.icon,
39
+ display: v.display ?? 'wrap',
40
+ kind: v.kind,
41
+ fixed: r.fixed,
50
42
  fields: o.fields,
51
43
  };
52
44
  }
45
+ export function row(o) {
46
+ return group({ label: o.label, fields: o.fields, view: { ...o.view, display: 'scroll' } });
47
+ }
48
+ export function table(o) {
49
+ return group({
50
+ key: o.key,
51
+ label: o.label,
52
+ fields: o.fields,
53
+ multiple: true,
54
+ required: o.required,
55
+ rules: o.rules,
56
+ view: { ...o.view, display: 'table' },
57
+ });
58
+ }
59
+ export function sheet(o) {
60
+ const flat = [];
61
+ o.fields.forEach((rowFields, i) => {
62
+ if (i > 0)
63
+ flat.push(brk());
64
+ flat.push(...rowFields);
65
+ });
66
+ return group({ label: o.label, fields: flat, view: { ...o.view, display: 'sheet' } });
67
+ }
68
+ export function url(o) {
69
+ return group({
70
+ key: o.key,
71
+ label: o.label ?? o.key,
72
+ required: o.required,
73
+ view: { kind: 'url' },
74
+ fields: [
75
+ string({ key: 'scheme' }),
76
+ string({ key: 'username' }),
77
+ string({ key: 'password' }),
78
+ string({ key: 'host' }),
79
+ int({ key: 'port' }),
80
+ string({ key: 'path' }),
81
+ string({ key: 'query' }),
82
+ string({ key: 'hash' }),
83
+ ],
84
+ });
85
+ }
86
+ export function keyed(o) {
87
+ if (o.fixed && o.by.type.prim !== 'select')
88
+ throw new Error(`[field.keyed] fixed requires by=select (enumerated), got '${o.by.type.prim}'`);
89
+ const make = (o.container ?? group);
90
+ const g = make({ label: o.label, fields: [o.by, ...o.fields], view: o.view });
91
+ return {
92
+ ...g,
93
+ key: o.key,
94
+ multiple: true,
95
+ required: o.required,
96
+ unique: o.unique ?? [o.by.key],
97
+ fixed: o.fixed,
98
+ };
99
+ }
53
100
  export const isStorageGroup = (g) => g.key != null;
54
101
  export const isCollectionGroup = (g) => g.key != null && g.multiple === true;
55
102
  export const isEmbeddedGroup = (g) => g.key != null && g.multiple !== true;
56
- /** Горизонтальний розділювач. */
57
103
  export function divider() {
58
104
  return { el: 'divider' };
59
105
  }
60
- /** Довідковий markdown-блок за i18n-ключем. */
106
+ export function brk() {
107
+ return { el: 'break' };
108
+ }
61
109
  export function info(textKey) {
62
110
  return { el: 'info', text: textKey };
63
111
  }
64
- /** Кнопка-дія: викликає обробник, зареєстрований у actionRegistry під ключем `value`. */
65
112
  export function button(o) {
66
113
  return { el: 'button', label: o.label, value: o.value, icon: o.icon, variant: o.variant };
67
114
  }
68
- /** Обгортає FieldMeta у FieldItem/StaticEl залежно від opts.key/opts.value. */
69
115
  export function wrapKey(opts, meta) {
70
- // Бренд (phantom) існує лише в типі — у runtime повертаємо чистий { key, type }.
71
- let m = opts.span != null ? { ...meta, span: opts.span } : meta;
116
+ let m = meta;
72
117
  if (opts.noEditControl)
73
118
  m = { ...m, hints: { ...m.hints, noEditControl: true } };
119
+ if (opts.default !== undefined)
120
+ m = { ...m, default: opts.default };
121
+ if (opts.hidden !== undefined)
122
+ m = { ...m, hidden: opts.hidden };
74
123
  if (opts.key)
75
124
  return { key: opts.key, type: m };
76
125
  if ('value' in opts && opts.value !== undefined)
77
126
  return { el: 'static', type: m, value: opts.value };
78
127
  return m;
79
128
  }
80
- // ─── Константи ────────────────────────────────────────────────────────────────
81
- /** Усі мови ISO 639-1 (native-назви як label). Джерело — пакет `iso-639-1`. */
82
- export const LANGUAGES = ISO6391.getAllCodes()
83
- .map((code) => ({ code, name: ISO6391.getNativeName(code) }))
84
- .sort((a, b) => a.name.localeCompare(b.name));
85
- /** Стать — спільний optionList 'sex' (реєструє plugin-core). i18n: core.sex.*. */
86
- export const SEX_OPTIONS = [
87
- { value: 'M', title: 'core.sex.M' },
88
- { value: 'F', title: 'core.sex.F' },
89
- { value: 'other', title: 'core.sex.other' },
90
- ];
91
- // ─── Helpers ──────────────────────────────────────────────────────────────────
92
- /** Структуроване повідомлення для zod: JSON {code, params}. Декодує mutate.ts. */
93
- export function vmsg(code, params) {
94
- return JSON.stringify(params ? { code, params } : { code });
95
- }
96
- /** v4 error-map: повідомлення для відсутнього значення (колишній required_error). */
97
- export function reqErr(code = 'required') {
98
- return { error: (iss) => (iss.input === undefined ? vmsg(code) : undefined) };
99
- }
100
- /** v4 error-map: повідомлення для невалідного типу (колишній invalid_type_error). */
101
- export function typeErr(code = 'invalid_type') {
102
- return { error: (iss) => (iss.code === 'invalid_type' ? vmsg(code) : undefined) };
103
- }
104
- /** v4 error-map: required + invalid_type разом (колишні required_error + invalid_type_error). */
105
- export function reqTypeErr() {
106
- return {
107
- error: (iss) => iss.code === 'invalid_type' ? (iss.input === undefined ? vmsg('required') : vmsg('invalid_type')) : undefined,
108
- };
109
- }
110
- /**
111
- * Спільна фабрика для полів, що зберігають JSON-рядок і валідуються вкладеною
112
- * zod-схемою: parse → inner.safeParse → при невдачі issue з кодом `code`,
113
- * при невалідному JSON — код 'json'. Уникає дублювання try/parse/superRefine.
114
- */
115
- export function jsonRefined(inner, code) {
116
- return z.string(reqErr()).superRefine((val, ctx) => {
117
- try {
118
- const r = inner.safeParse(JSON.parse(val));
119
- if (!r.success)
120
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg(code) });
121
- }
122
- catch {
123
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
124
- }
125
- });
126
- }
127
- export function optionalize(schema, required) {
128
- return z.preprocess((v) => (v === '' || v === null ? undefined : v), required ? schema : schema.optional());
129
+ export function isJsonStored(field) {
130
+ return field.json === true || field.hints['multiple'] === true;
129
131
  }
132
+ export { LANGUAGES, SEX_OPTIONS, WEEKDAY_OPTIONS } from "./fields/constants.js";
133
+ export { parseUrl, buildUrl, hrefOf } from "./fields/url.js";
130
134
  function resolveDate(v) {
131
135
  return v === 'today' ? new Date().toISOString().slice(0, 10) : v;
132
136
  }
133
- const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
134
- // ─── Модифікатори ───────────────────────────────────────────────────────────
135
- /**
136
- * Обгортає поле у JSON-масив значень (storage: TEXT). Внутрішній zod — `base.zod`.
137
- * Порядок модифікаторів: base → applyMultiple.
138
- *
139
- * Виняток: kind 'image'/'media' обробляє multiple сам у рендері (галерея) —
140
- * MultipleField*-обгортки на вебі їх пропускають.
141
- */
142
- /** Zod «JSON-масив значень inner» — спільне для applyMultiple і bindSelectSourceZod. */
137
+ function safeJsonParse(s) {
138
+ try {
139
+ return JSON.parse(s);
140
+ }
141
+ catch {
142
+ return undefined;
143
+ }
144
+ }
143
145
  function multipleZod(inner) {
144
- return z.string(reqErr()).superRefine((val, ctx) => {
145
- try {
146
- const arr = JSON.parse(val);
147
- if (!Array.isArray(arr)) {
148
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('array') });
149
- return;
146
+ return z.unknown().superRefine((raw, ctx) => {
147
+ let arr;
148
+ if (Array.isArray(raw)) {
149
+ arr = raw;
150
+ }
151
+ else if (typeof raw === 'string') {
152
+ try {
153
+ arr = JSON.parse(raw);
150
154
  }
151
- for (const item of arr) {
152
- const r = inner.safeParse(item);
153
- if (!r.success) {
154
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('array_item') });
155
- return;
156
- }
155
+ catch {
156
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
157
+ return;
157
158
  }
158
159
  }
159
- catch {
160
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
160
+ else {
161
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('array') });
162
+ return;
163
+ }
164
+ if (!Array.isArray(arr)) {
165
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('array') });
166
+ return;
167
+ }
168
+ for (const item of arr) {
169
+ if (!inner.safeParse(item).success) {
170
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('array_item') });
171
+ return;
172
+ }
161
173
  }
162
174
  });
163
175
  }
@@ -165,12 +177,13 @@ export function applyMultiple(base, multiple) {
165
177
  if (!multiple)
166
178
  return base;
167
179
  const s = multipleZod(base.zod);
168
- return { ...base, column: 'text', hints: { ...base.hints, multiple: true }, zod: optionalize(s, base.required) };
180
+ return { ...base, column: 'text', hints: { ...base.hints, multiple: true }, json: true, zod: optionalize(s, base.required) };
169
181
  }
170
- export function string(o) {
182
+ export function string(raw) {
183
+ const o = normalizeOpts(raw);
171
184
  const required = o.required ?? false;
172
- const cfg = o.config ?? {};
173
- const min = cfg.min ?? (required ? 1 : 0);
185
+ const cfg = (o.config ?? {});
186
+ const min = cfg.min ?? (o.required === true ? 1 : 0);
174
187
  let s = z.string(reqTypeErr());
175
188
  if (min > 0)
176
189
  s = s.min(min, { message: vmsg('min_length', { min }) });
@@ -195,20 +208,50 @@ export function string(o) {
195
208
  const meta = applyMultiple(base, o.multiple ?? false);
196
209
  return wrapKey(o, meta);
197
210
  }
198
- export function text(o) {
199
- // Support legacy min/max at top-level (used internally by presets)
200
- const mergedConfig = { ...o.config };
201
- if (o.min != null && mergedConfig.min == null)
202
- mergedConfig.min = o.min;
203
- if (o.max != null && mergedConfig.max == null)
204
- mergedConfig.max = o.max;
205
- const meta = string({ ...o, key: undefined, value: undefined, config: mergedConfig });
211
+ function pathField(o, kind, exts) {
212
+ const required = o.required ?? false;
213
+ let s = z.string(reqTypeErr());
214
+ if (o.required === true)
215
+ s = s.min(1, { message: vmsg('min_length', { min: 1 }) });
216
+ const base = {
217
+ kind,
218
+ label: o.label ?? '',
219
+ required,
220
+ prim: 'text',
221
+ column: 'text',
222
+ hints: exts && exts.length ? { exts } : {},
223
+ zod: optionalize(s, required),
224
+ ...(o.default !== undefined ? { default: o.default } : {}),
225
+ };
226
+ return wrapKey(o, base);
227
+ }
228
+ export function localDir(raw) {
229
+ return pathField(normalizeOpts(raw), 'localDir');
230
+ }
231
+ export function localFile(raw) {
232
+ const o = normalizeOpts(raw);
233
+ return pathField(o, 'localFile', o.exts);
234
+ }
235
+ export function text(raw) {
236
+ const o = normalizeOpts(raw);
237
+ const meta = string({ ...raw, key: undefined, value: undefined });
206
238
  const textMeta = { ...meta, prim: 'text', kind: 'textarea' };
207
239
  return wrapKey(o, textMeta);
208
240
  }
209
- export function real(o) {
241
+ export function i18n(raw) {
242
+ const o = normalizeOpts(raw);
243
+ const meta = text({ ...raw, key: undefined, value: undefined });
244
+ return wrapKey(o, { ...meta, kind: 'i18n' });
245
+ }
246
+ export function smallText(raw) {
247
+ const o = normalizeOpts(raw);
248
+ const meta = text({ ...raw, key: undefined, value: undefined });
249
+ return wrapKey(o, { ...meta, kind: 'smalltext' });
250
+ }
251
+ export function real(raw) {
252
+ const o = normalizeOpts(raw);
210
253
  const required = o.required ?? false;
211
- const cfg = o.config ?? {};
254
+ const cfg = (o.config ?? {});
212
255
  let s = z.coerce.number(typeErr());
213
256
  if (cfg.min != null)
214
257
  s = s.min(cfg.min, { message: vmsg('min', { min: cfg.min }) });
@@ -226,9 +269,10 @@ export function real(o) {
226
269
  const meta = applyMultiple(base, o.multiple ?? false);
227
270
  return wrapKey(o, meta);
228
271
  }
229
- export function int(o) {
272
+ export function int(raw) {
273
+ const o = normalizeOpts(raw);
230
274
  const required = o.required ?? false;
231
- const cfg = o.config ?? {};
275
+ const cfg = (o.config ?? {});
232
276
  let s = z.coerce.number(typeErr()).int({ message: vmsg('int') });
233
277
  if (cfg.min != null)
234
278
  s = s.min(cfg.min, { message: vmsg('min', { min: cfg.min }) });
@@ -246,12 +290,12 @@ export function int(o) {
246
290
  const meta = applyMultiple(base, o.multiple ?? false);
247
291
  return wrapKey(o, meta);
248
292
  }
249
- export function date(o) {
293
+ export function date(raw) {
294
+ const o = normalizeOpts(raw);
250
295
  const required = o.required ?? false;
251
296
  const granularity = o.config?.granularity ?? 'day';
252
297
  let s;
253
298
  if (granularity === 'year') {
254
- // year/month: min/max порівнюються лексикографічно як рядки (без resolveDate, на відміну від day)
255
299
  s = z
256
300
  .string(reqErr())
257
301
  .regex(/^\d{4}$/, { message: vmsg('date_format') })
@@ -296,7 +340,8 @@ export function date(o) {
296
340
  const meta = applyMultiple(base, o.multiple ?? false);
297
341
  return wrapKey(o, meta);
298
342
  }
299
- export function time(o) {
343
+ export function time(raw) {
344
+ const o = normalizeOpts(raw);
300
345
  const required = o.required ?? false;
301
346
  const granularity = o.config?.granularity ?? 'second';
302
347
  const timeRegex = {
@@ -317,7 +362,8 @@ export function time(o) {
317
362
  const meta = applyMultiple(base, o.multiple ?? false);
318
363
  return wrapKey(o, meta);
319
364
  }
320
- export function datetime(o) {
365
+ export function datetime(raw) {
366
+ const o = normalizeOpts(raw);
321
367
  const required = o.required ?? false;
322
368
  const granularity = o.config?.granularity ?? 'minute';
323
369
  const dtRegex = {
@@ -338,8 +384,9 @@ export function datetime(o) {
338
384
  const meta = applyMultiple(base, o.multiple ?? false);
339
385
  return wrapKey(o, meta);
340
386
  }
341
- export function boolean(o) {
342
- const s = z.preprocess((v) => (v === 'true' || v === 1 || v === '1' ? true : v === 'false' || v === 0 || v === '0' ? false : v), z.boolean().default(false));
387
+ export function boolean(raw) {
388
+ const o = normalizeOpts(raw);
389
+ const s = z.preprocess((v) => (v === 'true' || v === 1 || v === '1' ? true : v === 'false' || v === 0 || v === '0' ? false : v), z.boolean().default(false).nullable());
343
390
  const meta = {
344
391
  kind: 'boolean',
345
392
  label: o.label ?? '',
@@ -351,7 +398,8 @@ export function boolean(o) {
351
398
  };
352
399
  return wrapKey(o, meta);
353
400
  }
354
- export function triState(o) {
401
+ export function triState(raw) {
402
+ const o = normalizeOpts(raw);
355
403
  const required = o.required ?? false;
356
404
  const s = z.enum(['yes', 'no', 'unknown'], { error: () => vmsg('enum') });
357
405
  const meta = {
@@ -365,9 +413,9 @@ export function triState(o) {
365
413
  };
366
414
  return wrapKey(o, meta);
367
415
  }
368
- export function select(o) {
416
+ export function select(raw) {
417
+ const o = normalizeOpts(raw);
369
418
  const required = o.required ?? false;
370
- // ── статичне джерело: NamedOptionList (string) або inline OptionItem[] ──
371
419
  const source = typeof o.options === 'string' ? o.options : null;
372
420
  const inlineOpts = Array.isArray(o.options) ? o.options : [];
373
421
  const s = inlineOpts.length > 0
@@ -386,63 +434,54 @@ export function select(o) {
386
434
  const meta = applyMultiple(base, o.multiple ?? false);
387
435
  return wrapKey(o, meta);
388
436
  }
389
- export function relation(o) {
437
+ export function relation(raw) {
438
+ const o = normalizeOpts(raw);
390
439
  const required = o.required ?? false;
391
440
  const multi = o.multiple ?? false;
392
441
  let s;
393
442
  if (multi) {
394
- s = z.string().superRefine((val, ctx) => {
395
- try {
396
- const arr = JSON.parse(val);
397
- if (!Array.isArray(arr) || !arr.every((id) => uuidRe.test(id)))
398
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('uuid_list') });
399
- }
400
- catch {
401
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('uuid_list') });
402
- }
443
+ s = z.unknown().superRefine((raw, ctx) => {
444
+ const arr = jsonValue(raw);
445
+ if (!Array.isArray(arr) || !arr.every((id) => Number.isInteger(id) && id > 0))
446
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('int_list') });
403
447
  });
404
448
  }
405
449
  else {
406
- s = z.string().regex(uuidRe, { message: vmsg('uuid') });
450
+ s = z.coerce.number().int().positive({ message: vmsg('int') });
407
451
  }
408
452
  const meta = {
409
453
  kind: 'relation',
410
454
  label: o.label ?? '',
411
455
  required,
412
456
  prim: 'relation',
413
- column: 'text',
457
+ column: multi ? 'text' : 'integer',
414
458
  hints: { multi, displayKey: o.displayKey ?? 'name' },
415
- relation: { vault: o.vault, type: o.module },
459
+ relation: { vault: raw.options.vault, type: raw.options.module },
460
+ ...(multi && { json: true }),
416
461
  zod: optionalize(s, required),
417
462
  };
418
463
  return wrapKey(o, meta);
419
464
  }
420
- /**
421
- * Compose-time: підв'язати enum-валідацію source-селекту до значень списку.
422
- * Factory не знає вмісту optionList (списки збираються лише в composeRegistry),
423
- * тому zod source-полів спочатку z.string(); реєстр викликає це для кожного
424
- * такого поля, щоб сервер відхиляв значення поза списком.
425
- * Відтворює повний ланцюг модифікаторів: enum → multiple.
426
- */
427
465
  export function bindSelectSourceZod(field, values) {
428
466
  if (!values.length)
429
467
  return;
430
468
  const enumS = z.enum(values, { error: () => vmsg('enum') });
431
- // Точне відтворення ланцюга factory: кожен крок обгортається в optionalize,
432
- // як у select → applyMultiple.
433
469
  let s = optionalize(enumS, field.required);
434
470
  if (field.hints['multiple'] === true)
435
471
  s = optionalize(multipleZod(s), field.required);
436
472
  field.zod = s;
437
473
  }
438
- export function json(o) {
474
+ export function json(raw) {
475
+ const o = normalizeOpts(raw);
439
476
  const required = o.required ?? false;
440
- const s = z.string().superRefine((val, ctx) => {
441
- try {
442
- JSON.parse(val);
443
- }
444
- catch {
445
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
477
+ const s = z.unknown().superRefine((raw, ctx) => {
478
+ if (typeof raw === 'string') {
479
+ try {
480
+ JSON.parse(raw);
481
+ }
482
+ catch {
483
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
484
+ }
446
485
  }
447
486
  });
448
487
  const meta = {
@@ -452,35 +491,58 @@ export function json(o) {
452
491
  prim: 'json',
453
492
  column: 'text',
454
493
  hints: {},
494
+ json: true,
455
495
  zod: optionalize(s, required),
456
496
  };
457
497
  return wrapKey(o, meta);
458
498
  }
459
- export function checklist(o) {
499
+ export function check(raw) {
500
+ const o = normalizeOpts(raw);
460
501
  const required = o.required ?? false;
461
- const itemSchema = z.object({ done: z.boolean(), text: z.string().min(1) });
462
- const s = jsonRefined(z.array(itemSchema), 'checklist_structure');
463
- const base = {
464
- kind: 'checklist',
502
+ const multiple = o.multiple ?? false;
503
+ const slots = o.slots && o.slots.length > 0 ? o.slots : [''];
504
+ if (multiple) {
505
+ const fields = [string({ key: 'text', label: 'core.fields.text' })];
506
+ slots.forEach((label, i) => fields.push(boolean({ key: `check${i}`, label: label || 'core.fields.check' })));
507
+ return group({ key: o.key, label: o.label ?? o.key, multiple: true, required: required, fields });
508
+ }
509
+ const shape = { text: z.string() };
510
+ const columns = { text: 'text' };
511
+ slots.forEach((_, i) => {
512
+ shape[`check${i}`] = z.boolean();
513
+ columns[`check${i}`] = 'boolean';
514
+ });
515
+ const rowSchema = z.object(shape).passthrough();
516
+ const s = z.unknown().superRefine((raw, ctx) => {
517
+ const parsed = typeof raw === 'string' ? safeJsonParse(raw) : raw;
518
+ if (parsed === undefined) {
519
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
520
+ return;
521
+ }
522
+ if (!rowSchema.safeParse(parsed).success)
523
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('check_structure') });
524
+ });
525
+ const meta = {
526
+ kind: 'check',
465
527
  label: o.label ?? '',
466
528
  required,
467
- prim: 'checklist',
529
+ prim: 'check',
468
530
  column: 'text',
469
- hints: {},
531
+ hints: { slots },
532
+ columns,
470
533
  zod: optionalize(s, required),
471
534
  };
472
- const meta = applyMultiple(base, o.multiple ?? false);
473
535
  return wrapKey(o, meta);
474
536
  }
475
- export function measured(o) {
537
+ export function measured(raw) {
538
+ const o = normalizeOpts(raw);
476
539
  const required = o.required ?? false;
477
540
  const cfg = o.config ?? {};
478
- // Resolve units: string preset name or inline array
479
541
  const unitsSpec = (typeof o.options === 'string' ? o.options : o.options);
480
542
  const units = resolveUnits(unitsSpec);
481
543
  let numSchema = z.number().finite();
482
544
  if (cfg.step === 1)
483
- numSchema = numSchema.int({ message: vmsg('int') }); // int via step:1 is not used, kept for compat
545
+ numSchema = numSchema.int({ message: vmsg('int') });
484
546
  if (cfg.min != null)
485
547
  numSchema = numSchema.min(cfg.min);
486
548
  if (cfg.max != null)
@@ -489,46 +551,47 @@ export function measured(o) {
489
551
  value: numSchema,
490
552
  unit: z.string().refine((v) => units.some((u) => u.value === v), { message: vmsg('measured_unit') }),
491
553
  });
492
- const s = z.string().superRefine((val, ctx) => {
493
- try {
494
- const r = rowSchema.safeParse(JSON.parse(val));
495
- if (!r.success) {
496
- const vmsgIssue = r.error.issues.find((iss) => {
497
- try {
498
- return typeof JSON.parse(iss.message).code === 'string';
499
- }
500
- catch {
501
- return false;
502
- }
503
- });
504
- ctx.addIssue({
505
- code: z.ZodIssueCode.custom,
506
- message: vmsgIssue ? vmsgIssue.message : vmsg('measured_structure'),
507
- });
508
- }
509
- }
510
- catch {
554
+ const s = z.unknown().superRefine((raw, ctx) => {
555
+ const parsed = jsonValue(raw);
556
+ if (typeof parsed === 'string') {
511
557
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
558
+ return;
559
+ }
560
+ const r = rowSchema.safeParse(parsed);
561
+ if (!r.success) {
562
+ const vmsgIssue = r.error.issues.find((iss) => {
563
+ try {
564
+ return typeof JSON.parse(iss.message).code === 'string';
565
+ }
566
+ catch {
567
+ return false;
568
+ }
569
+ });
570
+ ctx.addIssue({
571
+ code: z.ZodIssueCode.custom,
572
+ message: vmsgIssue ? vmsgIssue.message : vmsg('measured_structure'),
573
+ });
512
574
  }
513
575
  });
514
- const base = {
576
+ const meta = {
515
577
  kind: 'measured',
516
578
  label: o.label ?? '',
517
579
  required,
518
580
  prim: 'measured',
519
581
  column: 'text',
520
582
  hints: {
521
- units: o.options, // рядок або масив — як є
583
+ units: o.options,
522
584
  ...(cfg.min != null && { min: cfg.min }),
523
585
  ...(cfg.max != null && { max: cfg.max }),
524
586
  ...(cfg.step != null && { step: cfg.step }),
525
587
  },
588
+ columns: { value: 'real', unit: 'text' },
526
589
  zod: optionalize(s, required),
527
590
  };
528
- const meta = applyMultiple(base, o.multiple ?? false);
529
591
  return wrapKey(o, meta);
530
592
  }
531
- export function money(o) {
593
+ export function money(raw) {
594
+ const o = normalizeOpts(raw);
532
595
  const required = o.required ?? false;
533
596
  const cfg = o.config ?? {};
534
597
  let numSchema = z.number().finite();
@@ -540,17 +603,17 @@ export function money(o) {
540
603
  value: numSchema,
541
604
  currency: z.string().refine(isCurrencyCode, { message: vmsg('money_currency') }),
542
605
  });
543
- const s = z.string().superRefine((val, ctx) => {
544
- try {
545
- const r = rowSchema.safeParse(JSON.parse(val));
546
- if (!r.success)
547
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('money_structure') });
548
- }
549
- catch {
606
+ const s = z.unknown().superRefine((raw, ctx) => {
607
+ const parsed = jsonValue(raw);
608
+ if (typeof parsed === 'string') {
550
609
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
610
+ return;
551
611
  }
612
+ const r = rowSchema.safeParse(parsed);
613
+ if (!r.success)
614
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('money_structure') });
552
615
  });
553
- const base = {
616
+ const meta = {
554
617
  kind: 'money',
555
618
  label: o.label ?? '',
556
619
  required,
@@ -561,23 +624,24 @@ export function money(o) {
561
624
  ...(cfg.min != null && { min: cfg.min }),
562
625
  ...(cfg.max != null && { max: cfg.max }),
563
626
  },
627
+ columns: { value: 'real', currency: 'text' },
564
628
  zod: optionalize(s, required),
565
629
  };
566
- const meta = applyMultiple(base, o.multiple ?? false);
567
630
  return wrapKey(o, meta);
568
631
  }
569
- export function code(o) {
632
+ export function code(raw) {
633
+ const o = normalizeOpts(raw);
570
634
  const required = o.required ?? false;
571
635
  const cfg = o.config ?? {};
572
636
  const rowSchema = z.object({ code: z.string(), lang: z.string() });
573
- const s = z.string().superRefine((val, ctx) => {
574
- try {
575
- if (!rowSchema.safeParse(JSON.parse(val)).success)
576
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('code_structure') });
577
- }
578
- catch {
637
+ const s = z.unknown().superRefine((raw, ctx) => {
638
+ const parsed = jsonValue(raw);
639
+ if (typeof parsed === 'string') {
579
640
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
641
+ return;
580
642
  }
643
+ if (!rowSchema.safeParse(parsed).success)
644
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('code_structure') });
581
645
  });
582
646
  const base = {
583
647
  kind: 'code',
@@ -586,25 +650,27 @@ export function code(o) {
586
650
  prim: 'code',
587
651
  column: 'text',
588
652
  hints: { ...(cfg.defaultLang && { defaultLang: cfg.defaultLang }) },
653
+ columns: { code: 'text', lang: 'text' },
589
654
  zod: optionalize(s, required),
590
655
  };
591
656
  return wrapKey(o, base);
592
657
  }
593
- export function geo(o) {
658
+ export function geo(raw) {
659
+ const o = normalizeOpts(raw);
594
660
  const required = o.required ?? false;
595
661
  const rowSchema = z.object({
596
662
  lat: z.number().min(-90).max(90),
597
663
  lng: z.number().min(-180).max(180),
598
664
  label: z.string().optional(),
599
665
  });
600
- const s = z.string().superRefine((val, ctx) => {
601
- try {
602
- if (!rowSchema.safeParse(JSON.parse(val)).success)
603
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('geo_structure') });
604
- }
605
- catch {
666
+ const s = z.unknown().superRefine((raw, ctx) => {
667
+ const parsed = jsonValue(raw);
668
+ if (typeof parsed === 'string') {
606
669
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
670
+ return;
607
671
  }
672
+ if (!rowSchema.safeParse(parsed).success)
673
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('geo_structure') });
608
674
  });
609
675
  const base = {
610
676
  kind: 'geo',
@@ -613,45 +679,25 @@ export function geo(o) {
613
679
  prim: 'geo',
614
680
  column: 'text',
615
681
  hints: {},
682
+ columns: { lat: 'real', lng: 'real', label: 'text' },
616
683
  zod: optionalize(s, required),
617
684
  };
618
685
  return wrapKey(o, base);
619
686
  }
620
687
  export function perWeekday(o) {
621
- const required = o.required ?? false;
622
- const cfg = o.config ?? {};
623
- let num = z.number(typeErr('per_weekday'));
624
- if (cfg.min != null)
625
- num = num.min(cfg.min, { message: vmsg('per_weekday') });
626
- if (cfg.max != null)
627
- num = num.max(cfg.max, { message: vmsg('per_weekday') });
628
- const arrSchema = z.array(num).length(7);
629
- const s = z.string().superRefine((val, ctx) => {
630
- try {
631
- const r = arrSchema.safeParse(JSON.parse(val));
632
- if (!r.success)
633
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('per_weekday') });
634
- }
635
- catch {
636
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
637
- }
638
- });
639
- const base = {
640
- kind: 'perWeekday',
641
- label: o.label ?? '',
642
- required,
643
- prim: 'per-weekday',
644
- column: 'text',
645
- hints: {
646
- ...(cfg.min != null && { min: cfg.min }),
647
- ...(cfg.max != null && { max: cfg.max }),
648
- ...(cfg.step != null && { step: cfg.step }),
649
- },
650
- zod: optionalize(s, required),
688
+ return {
689
+ ...keyed({
690
+ key: o.key,
691
+ label: o.label,
692
+ by: select({ key: 'day', label: 'core.fields.weekday', options: 'weekdays' }),
693
+ fixed: true,
694
+ fields: [real({ key: 'value', label: 'core.fields.value', rules: o.rules })],
695
+ }),
696
+ kind: 'per-weekday',
651
697
  };
652
- return wrapKey(o, base);
653
698
  }
654
- export function lookup(o) {
699
+ export function lookup(raw) {
700
+ const o = normalizeOpts(raw);
655
701
  const meta = {
656
702
  kind: 'lookup',
657
703
  label: o.label ?? '',
@@ -664,62 +710,71 @@ export function lookup(o) {
664
710
  };
665
711
  return wrapKey(o, meta);
666
712
  }
667
- /**
668
- * Відновлює FieldMeta з клієнтського FieldClient (для рекурсивного рендеру
669
- * сабфілдів групових полів через FieldInput/FieldValue). zod тут не потрібен —
670
- * валідація відбувається на рівні схеми батьківського поля.
671
- */
672
713
  export function clientToMeta(fc) {
673
714
  return { ...fc, column: 'text', zod: z.any() };
674
715
  }
675
- export function period(o) {
676
- const granularity = o.granularity ?? 'day';
677
- const fromField = granularity === 'datetime'
678
- ? datetime({ label: 'core.period.from' })
679
- : date({ label: 'core.period.from', config: { granularity: granularity === 'month' ? 'month' : 'day' } });
680
- const untilField = granularity === 'datetime'
681
- ? datetime({ label: 'core.period.until' })
682
- : date({ label: 'core.period.until', config: { granularity: granularity === 'month' ? 'month' : 'day' } });
683
- // Самодостатній JSON-композит {from,until} (раніше будувався на field.row).
684
- // Відчеплено від row, щоб row можна було видалити. Storage: JSON {from,until}
685
- // (single) або [{from,until}] (multiple), як measured/keyValue.
716
+ export function period(raw) {
717
+ const o = normalizeOpts(raw);
718
+ const granularity = (o.config?.granularity ?? 'day');
686
719
  const required = o.required ?? false;
687
720
  const multiple = o.multiple ?? false;
688
- const subDefs = { from: fromField, until: untilField };
721
+ const mkSub = (key, label) => granularity === 'datetime'
722
+ ? datetime({ key, label })
723
+ : date({ key, label, rules: { granularity: granularity === 'month' ? 'month' : 'day' } });
724
+ if (multiple) {
725
+ return group({
726
+ key: o.key,
727
+ label: o.label ?? o.key,
728
+ multiple: true,
729
+ required: required,
730
+ fields: [mkSub('from', 'core.period.from'), mkSub('until', 'core.period.until')],
731
+ });
732
+ }
733
+ const fromField = mkSub('from', 'core.period.from');
734
+ const untilField = mkSub('until', 'core.period.until');
735
+ const subDefs = { from: fromField.type, until: untilField.type };
689
736
  const rowShape = {};
690
737
  for (const [key, fm] of Object.entries(subDefs))
691
738
  rowShape[key] = fm.zod.optional();
692
- const rowSchema = z.object(rowShape).passthrough();
693
- let s = jsonRefined(multiple ? z.array(rowSchema) : rowSchema, 'row_structure');
739
+ let s = z.object(rowShape).passthrough();
694
740
  s = s.superRefine((val, ctx) => {
695
- if (val == null || typeof val !== 'string')
741
+ if (val == null)
696
742
  return;
697
- try {
698
- const parsed = JSON.parse(val);
699
- const items = Array.isArray(parsed) ? parsed : [parsed];
700
- for (const item of items) {
701
- const r = item;
702
- if (r?.from && r?.until && String(r.from) > String(r.until))
703
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('period_order') });
743
+ let parsed = val;
744
+ if (typeof val === 'string') {
745
+ try {
746
+ parsed = JSON.parse(val);
747
+ }
748
+ catch {
749
+ return;
704
750
  }
705
751
  }
706
- catch {
707
- /* already handled by jsonRefined */
752
+ const items = Array.isArray(parsed) ? parsed : [parsed];
753
+ for (const item of items) {
754
+ const r = item;
755
+ if (r?.from && r?.until && String(r.from) > String(r.until))
756
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('period_order') });
708
757
  }
709
758
  });
710
- const subFields = Object.entries(subDefs).map(([key, fm]) => ({ key, ...toClient(fm) }));
759
+ const colType = granularity === 'datetime' ? 'datetime' : granularity === 'day' ? 'date' : 'text';
760
+ const subFields = [
761
+ { key: 'from', ...toClient(fromField.type) },
762
+ { key: 'until', ...toClient(untilField.type) },
763
+ ];
711
764
  const meta = {
712
765
  kind: 'period',
713
766
  label: o.label ?? '',
714
767
  required,
715
768
  prim: 'period',
716
769
  column: 'text',
717
- hints: { fields: subFields, rowMultiple: multiple, unique: [], granularity },
770
+ hints: { fields: subFields, rowMultiple: false, unique: [], granularity },
771
+ columns: { from: colType, until: colType },
718
772
  zod: optionalize(s, required),
719
773
  };
720
774
  return wrapKey(o, meta);
721
775
  }
722
- function makeFile(kind, o) {
776
+ function makeFile(kind, raw) {
777
+ const o = normalizeOpts(raw);
723
778
  const required = o.required ?? false;
724
779
  const multiple = o.multiple ?? false;
725
780
  const rowSchema = z.object({
@@ -727,21 +782,23 @@ function makeFile(kind, o) {
727
782
  mime: z.string().optional(),
728
783
  size: z.number().optional(),
729
784
  });
730
- // Schema accepts both single {name,...} and array [{name,...},...] stored as JSON.
731
- // multiple=true expects an array; single accepts a single object or array.
732
- const s = z.string().superRefine((val, ctx) => {
733
- try {
734
- const parsed = JSON.parse(val);
735
- const items = Array.isArray(parsed) ? parsed : [parsed];
736
- for (const it of items) {
737
- if (!rowSchema.safeParse(it).success) {
738
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('file_structure') });
739
- return;
740
- }
785
+ const s = z.unknown().superRefine((raw, ctx) => {
786
+ if (typeof raw === 'string') {
787
+ try {
788
+ JSON.parse(raw);
789
+ }
790
+ catch {
791
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
792
+ return;
741
793
  }
742
794
  }
743
- catch {
744
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
795
+ const parsed = jsonValue(raw);
796
+ const items = Array.isArray(parsed) ? parsed : [parsed];
797
+ for (const it of items) {
798
+ if (!rowSchema.safeParse(it).success) {
799
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('file_structure') });
800
+ return;
801
+ }
745
802
  }
746
803
  });
747
804
  const meta = {
@@ -751,6 +808,7 @@ function makeFile(kind, o) {
751
808
  prim: 'file',
752
809
  column: 'text',
753
810
  hints: multiple ? { multiple: true } : {},
811
+ json: true,
754
812
  zod: optionalize(s, required),
755
813
  };
756
814
  return wrapKey(o, meta);
@@ -773,41 +831,26 @@ export function image(o) {
773
831
  export function media(o) {
774
832
  return makeFile('media', o);
775
833
  }
776
- const kvItemSchema = z.object({ key: z.string().min(1), value: z.string() });
777
- export function keyValue(o) {
778
- const required = o.required ?? false;
779
- const multiple = o.multiple ?? true;
780
- const s = z.string().superRefine((val, ctx) => {
781
- try {
782
- const r = z.array(kvItemSchema).safeParse(JSON.parse(val));
783
- if (!r.success)
784
- return ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('structure') });
785
- if (!multiple && r.data.length > 1)
786
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('kv_single') });
787
- }
788
- catch {
789
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
790
- }
791
- });
792
- const meta = {
793
- kind: 'keyValue',
834
+ export function avatar(o) {
835
+ return makeFile('avatar', o);
836
+ }
837
+ export function cover(o) {
838
+ return makeFile('cover', o);
839
+ }
840
+ export function keyValue(raw) {
841
+ const o = normalizeOpts(raw);
842
+ const valueType = { ...(o.option ?? string({ label: '' })), label: o.valueLabel ?? '' };
843
+ return keyed({
844
+ key: o.key,
794
845
  label: o.label ?? '',
795
- required,
796
- prim: 'key-value',
797
- column: 'text',
798
- // kvMultiple (НЕ multiple): «кілька пар» — внутрішня семантика віджету,
799
- // не applyMultiple-обгортка. Інакше display/form гейти multiple ловлять його помилково.
800
- hints: {
801
- kvMultiple: multiple,
802
- option: o.option ?? string({ label: '' }),
803
- keyLabel: o.keyLabel,
804
- valueLabel: o.valueLabel,
805
- },
806
- zod: optionalize(s, required),
807
- };
808
- return wrapKey(o, meta);
846
+ container: o.container ?? table,
847
+ by: string({ key: 'key', label: o.keyLabel ?? '' }),
848
+ fields: [{ key: 'value', type: valueType }],
849
+ required: o.required,
850
+ });
809
851
  }
810
- function makeRange(kind, int, o) {
852
+ function makeRange(kind, int, raw) {
853
+ const o = normalizeOpts(raw);
811
854
  const required = o.required ?? false;
812
855
  const cfg = o.config ?? {};
813
856
  let n = z.number().finite();
@@ -818,19 +861,23 @@ function makeRange(kind, int, o) {
818
861
  if (cfg.max != null)
819
862
  n = n.max(cfg.max);
820
863
  const rowSchema = z.object({ from: n, to: n });
821
- const s = z.string().superRefine((val, ctx) => {
822
- try {
823
- const r = rowSchema.safeParse(JSON.parse(val));
824
- if (!r.success) {
825
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('range_structure') });
864
+ const s = z.unknown().superRefine((raw, ctx) => {
865
+ if (typeof raw === 'string') {
866
+ try {
867
+ JSON.parse(raw);
868
+ }
869
+ catch {
870
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
826
871
  return;
827
872
  }
828
- if (r.data.from > r.data.to)
829
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('range_order') });
830
873
  }
831
- catch {
832
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
874
+ const r = rowSchema.safeParse(jsonValue(raw));
875
+ if (!r.success) {
876
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('range_structure') });
877
+ return;
833
878
  }
879
+ if (r.data.from > r.data.to)
880
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('range_order') });
834
881
  });
835
882
  const base = {
836
883
  kind,
@@ -843,6 +890,7 @@ function makeRange(kind, int, o) {
843
890
  ...(cfg.max != null && { max: cfg.max }),
844
891
  ...(cfg.step != null && { step: cfg.step }),
845
892
  },
893
+ columns: int ? { from: 'integer', to: 'integer' } : { from: 'real', to: 'real' },
846
894
  zod: optionalize(s, required),
847
895
  };
848
896
  return wrapKey(o, base);
@@ -853,17 +901,41 @@ export function numberRange(o) {
853
901
  export function realRange(o) {
854
902
  return makeRange('realRange', false, o);
855
903
  }
856
- // ─────────────────────────────────────────────────────────────────────────────
857
- // f єдина точка входу для визначення полів у типах
858
- // ─────────────────────────────────────────────────────────────────────────────
859
- /**
860
- * Базові примітиви (storage-aligned) + структурні типи. Пресети (тонкі обгортки
861
- * над цими примітивами) додаються нижче з field-presets.ts і НЕ можуть перебивати
862
- * жоден ключ звідси (guard у composeF).
863
- */
904
+ export function embed(raw) {
905
+ const o = normalizeOpts(raw);
906
+ const required = o.required ?? false;
907
+ const multiple = o.multiple ?? false;
908
+ const recSchema = z.object({
909
+ url: z.string().min(1),
910
+ title: z.string().optional(),
911
+ description: z.string().optional(),
912
+ thumbnail: z.string().optional(),
913
+ provider: z.string().optional(),
914
+ type: z.enum(['video', 'rich', 'photo', 'link']).optional(),
915
+ html: z.string().optional(),
916
+ width: z.number().optional(),
917
+ height: z.number().optional(),
918
+ });
919
+ const s = jsonRefined(multiple ? z.array(recSchema) : z.union([recSchema, z.array(recSchema).max(1)]), 'embed_structure');
920
+ const meta = {
921
+ kind: 'embed',
922
+ label: o.label ?? '',
923
+ required,
924
+ prim: 'embed',
925
+ column: 'text',
926
+ hints: multiple ? { multiple: true } : {},
927
+ json: true,
928
+ zod: optionalize(s, required),
929
+ };
930
+ return wrapKey(o, meta);
931
+ }
864
932
  const PRIMITIVES = {
865
933
  string,
866
934
  text,
935
+ i18n,
936
+ smallText,
937
+ localDir,
938
+ localFile,
867
939
  real,
868
940
  int,
869
941
  date,
@@ -874,7 +946,7 @@ const PRIMITIVES = {
874
946
  select,
875
947
  relation,
876
948
  json,
877
- checklist,
949
+ check,
878
950
  measured,
879
951
  money,
880
952
  code,
@@ -888,24 +960,36 @@ const PRIMITIVES = {
888
960
  video,
889
961
  image,
890
962
  media,
963
+ avatar,
964
+ cover,
965
+ embed,
891
966
  keyValue,
892
967
  numberRange,
893
968
  realRange,
894
969
  group,
970
+ row,
971
+ table,
972
+ sheet,
973
+ keyed,
974
+ url,
895
975
  divider,
976
+ break: brk,
896
977
  info,
897
978
  button,
898
979
  };
899
- /** Збирає `f`, гарантуючи, що жоден пресет не затіняє примітив. */
900
980
  function composeF(presets) {
901
981
  for (const k of Object.keys(presets))
902
982
  if (k in PRIMITIVES)
903
- throw new Error(`[f] пресет '${k}' перебиває примітив`);
983
+ throw new Error(`[f] preset '${k}' overrides a primitive`);
904
984
  return { ...PRIMITIVES, ...presets };
905
985
  }
906
- export const field = composeF(presets);
986
+ let _composed;
987
+ const composedField = () => (_composed ??= composeF(presets));
988
+ export const field = new Proxy({}, {
989
+ get: (_t, k) => composedField()[k],
990
+ has: (_t, k) => k in composedField(),
991
+ });
907
992
  export function toClient(field) {
908
- const { kind, label, required, prim, hints, options, relation, virtual } = field;
909
- return { kind, label, required, prim, hints, options, relation, virtual };
993
+ const { kind, label, required, prim, hints, options, relation, virtual, json, hidden } = field;
994
+ return { kind, label, required, prim, hints, options, relation, virtual, json, hidden };
910
995
  }
911
- //# sourceMappingURL=fields.js.map