@atscript/ui 0.1.58

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/dist/index.mjs ADDED
@@ -0,0 +1,1498 @@
1
+ import { Validator, createDataFromAnnotatedType, deserializeAnnotatedType, detectDiscriminator, flattenAnnotatedType } from "@atscript/typescript/utils";
2
+ import { Client } from "@atscript/db-client";
3
+ //#region src/shared/annotation-keys.ts
4
+ const UI_TYPE = "ui.type";
5
+ const UI_FORM_PLACEHOLDER = "ui.form.placeholder";
6
+ const UI_FORM_HINT = "ui.form.hint";
7
+ const UI_FORM_CLASSES = "ui.form.classes";
8
+ const UI_FORM_STYLES = "ui.form.styles";
9
+ const UI_FORM_AUTOCOMPLETE = "ui.form.autocomplete";
10
+ const UI_FORM_DISABLED = "ui.form.disabled";
11
+ const UI_FORM_OPTIONS = "ui.form.options";
12
+ const UI_FORM_ORDER = "ui.form.order";
13
+ const UI_FORM_TYPE = "ui.form.type";
14
+ const UI_FORM_COMPONENT = "ui.form.component";
15
+ const UI_FORM_HIDDEN = "ui.form.hidden";
16
+ const UI_FORM_ATTR = "ui.form.attr";
17
+ const UI_FORM_GRID_COL_SPAN = "ui.form.grid.colSpan";
18
+ const UI_FORM_GRID_ROW_SPAN = "ui.form.grid.rowSpan";
19
+ const UI_FORM_SUBMIT_TEXT = "ui.form.submit.text";
20
+ const UI_FORM_LABEL_SINGULAR = "ui.form.label.singular";
21
+ const UI_FORM_ACTION = "ui.form.action";
22
+ const UI_FORM_PREFIX = "ui.form.prefix";
23
+ const UI_FORM_PREFIX_REF = "ui.form.prefix.ref";
24
+ const UI_FORM_PREFIX_ICON = "ui.form.prefix.icon";
25
+ const UI_FORM_SUFFIX = "ui.form.suffix";
26
+ const UI_FORM_SUFFIX_REF = "ui.form.suffix.ref";
27
+ const UI_FORM_SUFFIX_ICON = "ui.form.suffix.icon";
28
+ const UI_TABLE_WIDTH = "ui.table.width";
29
+ const UI_TABLE_COMPONENT = "ui.table.component";
30
+ const UI_TABLE_HIDDEN = "ui.table.hidden";
31
+ const UI_TABLE_ATTR = "ui.table.attr";
32
+ const UI_TABLE_CLASSES = "ui.table.classes";
33
+ const UI_TABLE_STYLES = "ui.table.styles";
34
+ const UI_TABLE_TYPE = "ui.table.type";
35
+ const UI_TABLE_ORDER = "ui.table.order";
36
+ const UI_DICT_LABEL = "ui.dict.label";
37
+ const UI_DICT_DESCR = "ui.dict.descr";
38
+ const UI_DICT_ATTR = "ui.dict.attr";
39
+ const UI_DICT_FILTERABLE = "ui.dict.filterable";
40
+ const UI_DICT_SORTABLE = "ui.dict.sortable";
41
+ const UI_DICT_SEARCHABLE = "ui.dict.searchable";
42
+ const DB_REL_FK = "db.rel.FK";
43
+ const DB_HTTP_PATH = "db.http.path";
44
+ const DB_AMOUNT_CURRENCY = "db.amount.currency";
45
+ const DB_AMOUNT_CURRENCY_REF = "db.amount.currency.ref";
46
+ const DB_UNIT = "db.unit";
47
+ const DB_UNIT_REF = "db.unit.ref";
48
+ const DB_COLUMN_PRECISION = "db.column.precision";
49
+ const WF_ACTION_WITH_DATA = "wf.action.withData";
50
+ const META_LABEL = "meta.label";
51
+ const META_ID = "meta.id";
52
+ const META_DESCRIPTION = "meta.description";
53
+ const META_READONLY = "meta.readonly";
54
+ const META_REQUIRED = "meta.required";
55
+ const META_DEFAULT = "meta.default";
56
+ const META_SENSITIVE = "meta.sensitive";
57
+ const EXPECT_MAX_LENGTH = "expect.maxLength";
58
+ const UI_FORM_FN_PREFIX = "ui.form.fn.";
59
+ const UI_FORM_FN_LABEL = "ui.form.fn.label";
60
+ const UI_FORM_FN_PLACEHOLDER = "ui.form.fn.placeholder";
61
+ const UI_FORM_FN_DESCRIPTION = "ui.form.fn.description";
62
+ const UI_FORM_FN_HINT = "ui.form.fn.hint";
63
+ const UI_FORM_FN_HIDDEN = "ui.form.fn.hidden";
64
+ const UI_FORM_FN_DISABLED = "ui.form.fn.disabled";
65
+ const UI_FORM_FN_READONLY = "ui.form.fn.readonly";
66
+ const UI_FORM_FN_OPTIONS = "ui.form.fn.options";
67
+ const UI_FORM_FN_ATTR = "ui.form.fn.attr";
68
+ const UI_FORM_FN_VALUE = "ui.form.fn.value";
69
+ const UI_FORM_FN_CLASSES = "ui.form.fn.classes";
70
+ const UI_FORM_FN_STYLES = "ui.form.fn.styles";
71
+ const UI_FORM_FN_TITLE = "ui.form.fn.title";
72
+ const UI_FORM_FN_SUBMIT_TEXT = "ui.form.fn.submit.text";
73
+ const UI_FORM_FN_SUBMIT_DISABLED = "ui.form.fn.submit.disabled";
74
+ const UI_TABLE_FN_PREFIX = "ui.table.fn.";
75
+ const UI_TABLE_FN_ATTR = "ui.table.fn.attr";
76
+ const UI_TABLE_FN_CLASSES = "ui.table.fn.classes";
77
+ const UI_TABLE_FN_STYLES = "ui.table.fn.styles";
78
+ const UI_FORM_VALIDATE = "ui.form.validate";
79
+ //#endregion
80
+ //#region src/form/types.ts
81
+ /** Type guard: checks if a field def is an array field. */
82
+ function isArrayField(field) {
83
+ return field.type === "array";
84
+ }
85
+ /** Type guard: checks if a field def is an object field. */
86
+ function isObjectField(field) {
87
+ return field.type === "object";
88
+ }
89
+ /** Type guard: checks if a field def is a union field. */
90
+ function isUnionField(field) {
91
+ return field.type === "union";
92
+ }
93
+ /** Type guard: checks if a field def is a tuple field. */
94
+ function isTupleField(field) {
95
+ return field.type === "tuple";
96
+ }
97
+ //#endregion
98
+ //#region src/shared/field-resolver.ts
99
+ /** Static resolver — ignores fn keys, reads only static metadata. */
100
+ var StaticFieldResolver = class {
101
+ resolveFieldProp(prop, _fnKey, staticKey, _scope, opts) {
102
+ return resolveStatic(prop.metadata, staticKey, opts);
103
+ }
104
+ resolveFormProp(type, _fnKey, staticKey, _scope, opts) {
105
+ return resolveStatic(type.metadata, staticKey, opts);
106
+ }
107
+ hasComputedAnnotations(_prop) {
108
+ return false;
109
+ }
110
+ };
111
+ /** Resolves a static metadata value. Exported for reuse by dynamic resolvers. */
112
+ function resolveStatic(metadata, staticKey, opts) {
113
+ if (staticKey === void 0) return void 0;
114
+ const staticVal = metadata.get(staticKey);
115
+ if (staticVal !== void 0) {
116
+ if (opts?.staticAsBoolean) return true;
117
+ if (opts?.transform) return opts.transform(staticVal);
118
+ return staticVal;
119
+ }
120
+ }
121
+ /** Default static resolver instance. */
122
+ const defaultResolver = new StaticFieldResolver();
123
+ let activeResolver = defaultResolver;
124
+ /** Replace the active resolver (called by ui-fns to install dynamic resolution). */
125
+ function setResolver(resolver) {
126
+ activeResolver = resolver;
127
+ }
128
+ /** Get the current active resolver. */
129
+ function getResolver() {
130
+ return activeResolver;
131
+ }
132
+ /** Resolve a field-level metadata property via the active resolver. */
133
+ function resolveFieldProp(prop, fnKey, staticKey, scope, opts) {
134
+ return activeResolver.resolveFieldProp(prop, fnKey, staticKey, scope, opts);
135
+ }
136
+ /** Resolve a form-level metadata property via the active resolver. */
137
+ function resolveFormProp(type, fnKey, staticKey, scope, opts) {
138
+ return activeResolver.resolveFormProp(type, fnKey, staticKey, scope, opts);
139
+ }
140
+ /** Check if a prop has dynamic annotations via the active resolver. */
141
+ function hasComputedAnnotations(prop) {
142
+ return activeResolver.hasComputedAnnotations(prop);
143
+ }
144
+ function getFieldMeta(prop, key) {
145
+ return prop.metadata.get(key);
146
+ }
147
+ /** Ensures a value is an array — returns as-is if already one, wraps in `[x]` otherwise. */
148
+ function asArray(x) {
149
+ return Array.isArray(x) ? x : [x];
150
+ }
151
+ /**
152
+ * Parses static `ui.attr` metadata into a key-value record.
153
+ * Exported so ui-fns can reuse this without duplicating the parsing logic.
154
+ */
155
+ function parseStaticAttrs(staticAttrs) {
156
+ if (!staticAttrs) return void 0;
157
+ const result = {};
158
+ let hasAttrs = false;
159
+ for (const item of asArray(staticAttrs)) if (typeof item === "object" && item !== null && "name" in item && "value" in item) {
160
+ const { name, value } = item;
161
+ result[name] = value;
162
+ hasAttrs = true;
163
+ }
164
+ return hasAttrs ? result : void 0;
165
+ }
166
+ /**
167
+ * Resolves `<staticKey>` + `<fnKey>` attr metadata on demand.
168
+ * Defaults read `ui.form.attr` + `ui.form.fn.attr`; pass `{ staticKey, fnKey }` to read
169
+ * the table-side pair (`ui.table.attr` + `ui.table.fn.attr`) or any other surface.
170
+ */
171
+ function resolveAttrs(prop, scope, keys = {}) {
172
+ const staticKey = keys.staticKey ?? "ui.form.attr";
173
+ const fnKey = keys.fnKey ?? "ui.form.fn.attr";
174
+ const staticAttrs = getFieldMeta(prop, staticKey);
175
+ const fnAttrs = getFieldMeta(prop, fnKey);
176
+ if (!staticAttrs && !fnAttrs) return void 0;
177
+ const result = {};
178
+ let hasAttrs = false;
179
+ const parsed = parseStaticAttrs(staticAttrs);
180
+ if (parsed) {
181
+ Object.assign(result, parsed);
182
+ hasAttrs = true;
183
+ }
184
+ if (fnAttrs) {
185
+ const resolved = resolveFieldProp(prop, fnKey, void 0, scope);
186
+ if (resolved) {
187
+ Object.assign(result, resolved);
188
+ hasAttrs = true;
189
+ }
190
+ }
191
+ return hasAttrs ? result : void 0;
192
+ }
193
+ //#endregion
194
+ //#region src/value-help/extract-literals.ts
195
+ /**
196
+ * Extracts options from a union of literal types (e.g. 'a' | 'b' | 'c').
197
+ * Returns undefined if the type is not a pure union of literals.
198
+ *
199
+ * Handles nested unions created by flattenAnnotatedType, which recurses
200
+ * into union items and produces synthetic unions containing both individual
201
+ * literals and the original union type as nested items.
202
+ */
203
+ function extractLiteralOptions(prop) {
204
+ if (prop.type.kind !== "union") return void 0;
205
+ const result = collectLiterals(prop.type.items, /* @__PURE__ */ new Set());
206
+ return result && result.length > 0 ? result : void 0;
207
+ }
208
+ /** Returns true when the annotated type is a union composed entirely of literal values. */
209
+ function isPureLiteralUnion(prop) {
210
+ if (prop.type.kind !== "union") return false;
211
+ return checkAllLiterals(prop.type.items);
212
+ }
213
+ /** Walks union items returning true only if every leaf is a literal value. */
214
+ function checkAllLiterals(items) {
215
+ for (const item of items) {
216
+ if (item.type.kind === "" && item.type.value !== void 0) continue;
217
+ if (item.type.kind === "union") {
218
+ if (!checkAllLiterals(item.type.items)) return false;
219
+ continue;
220
+ }
221
+ return false;
222
+ }
223
+ return items.length > 0;
224
+ }
225
+ /**
226
+ * Recursively collects literal values from union items.
227
+ * Returns null if any non-literal, non-union item is found (invalid union).
228
+ * Returns empty array when all items are valid but already seen (deduped).
229
+ */
230
+ function collectLiterals(items, seen) {
231
+ const result = [];
232
+ for (const item of items) if (item.type.kind === "" && item.type.value !== void 0) {
233
+ const val = String(item.type.value);
234
+ if (!seen.has(val)) {
235
+ seen.add(val);
236
+ result.push({
237
+ key: val,
238
+ label: val
239
+ });
240
+ }
241
+ } else if (item.type.kind === "union") {
242
+ const nested = collectLiterals(item.type.items, seen);
243
+ if (nested === null) return null;
244
+ result.push(...nested);
245
+ } else return null;
246
+ return result;
247
+ }
248
+ //#endregion
249
+ //#region src/value-help/extract-ref.ts
250
+ /**
251
+ * Synchronous probe. Returns `{ url, targetField }` iff:
252
+ * 1. the prop carries `@db.rel.FK`,
253
+ * 2. the prop has a `.ref`,
254
+ * 3. the ref's target metadata carries `@db.http.path`.
255
+ */
256
+ function extractValueHelp(prop) {
257
+ if (!prop.metadata.has("db.rel.FK")) return void 0;
258
+ if (!prop.ref) return void 0;
259
+ const target = prop.ref.type();
260
+ if (!target) return void 0;
261
+ const url = target.metadata.get(DB_HTTP_PATH);
262
+ if (!url) return void 0;
263
+ return {
264
+ url,
265
+ targetField: prop.ref.field
266
+ };
267
+ }
268
+ //#endregion
269
+ //#region src/form/create-form-def.ts
270
+ /** Known atscript primitive extension tags that map directly to field types. */
271
+ const UI_TAGS = new Set([
272
+ "action",
273
+ "paragraph",
274
+ "select",
275
+ "radio",
276
+ "checkbox"
277
+ ]);
278
+ /**
279
+ * Converts an ATScript annotated type into a FormDef.
280
+ *
281
+ * - **Object types** (`kind === 'object'`): produces an object root with nested fields.
282
+ * - **Non-object types** (primitive, array, union, etc.): produces a single leaf root field
283
+ * with `path: ''`.
284
+ */
285
+ function createFormDef(type) {
286
+ if (type.type.kind !== "object") {
287
+ const rootField = createFieldDef("", type);
288
+ return {
289
+ type,
290
+ rootField,
291
+ fields: [rootField],
292
+ flatMap: /* @__PURE__ */ new Map()
293
+ };
294
+ }
295
+ const objectType = type;
296
+ const flatMap = flattenAnnotatedType(objectType, { excludePhantomTypes: false });
297
+ const fields = [];
298
+ const structuredPrefixes = /* @__PURE__ */ new Set();
299
+ for (const [path, prop] of flatMap.entries()) {
300
+ if (path === "") continue;
301
+ if (isChildOfStructured(path, structuredPrefixes)) continue;
302
+ const originalProp = resolveOriginalProp(objectType, path) ?? prop;
303
+ const kind = originalProp.type.kind;
304
+ if (kind === "object") {
305
+ const hasLabel = getFieldMeta(originalProp, META_LABEL) !== void 0;
306
+ const hasComponent = getFieldMeta(originalProp, UI_FORM_COMPONENT) !== void 0;
307
+ if (!hasLabel && !hasComponent) continue;
308
+ }
309
+ if (kind === "array") {
310
+ if (originalProp.type.of.type.kind === "array" && !getFieldMeta(originalProp, "ui.form.component")) continue;
311
+ }
312
+ if (kind === "array" || kind === "object" || kind === "union" || kind === "tuple") structuredPrefixes.add(path + ".");
313
+ fields.push(createFieldDef(path, originalProp));
314
+ }
315
+ const orderMap = new Map(fields.map((f) => [f, getFieldMeta(f.prop, "ui.form.order") ?? Infinity]));
316
+ fields.sort((a, b) => orderMap.get(a) - orderMap.get(b));
317
+ const rootField = {
318
+ path: "",
319
+ prop: type,
320
+ type: "object",
321
+ phantom: false,
322
+ name: "",
323
+ allStatic: false
324
+ };
325
+ const def = {
326
+ type,
327
+ rootField,
328
+ fields,
329
+ flatMap
330
+ };
331
+ rootField.objectDef = def;
332
+ return def;
333
+ }
334
+ /** Creates a FormFieldDef from any ATScript annotated type. */
335
+ function createFieldDef(path, prop) {
336
+ const kind = prop.type.kind;
337
+ const name = path.slice(path.lastIndexOf(".") + 1);
338
+ const allStatic = !hasComputedAnnotations(prop);
339
+ const uiType = getFieldMeta(prop, "ui.form.type") ?? getFieldMeta(prop, "ui.type");
340
+ const base = {
341
+ path,
342
+ prop,
343
+ phantom: false,
344
+ name,
345
+ allStatic
346
+ };
347
+ const customType = uiType;
348
+ if (kind === "array") {
349
+ const arrayType = prop.type;
350
+ return {
351
+ ...base,
352
+ type: "array",
353
+ customType,
354
+ itemType: arrayType.of,
355
+ itemField: createFieldDef("", arrayType.of)
356
+ };
357
+ }
358
+ if (kind === "object") return {
359
+ ...base,
360
+ type: "object",
361
+ customType,
362
+ objectDef: createFormDef(prop)
363
+ };
364
+ if (kind === "union") {
365
+ if (isPureLiteralUnion(prop)) return {
366
+ ...base,
367
+ type: uiType ?? "select"
368
+ };
369
+ const unionVariants = buildUnionVariants(prop);
370
+ if (unionVariants.length > 1) return {
371
+ ...base,
372
+ type: "union",
373
+ customType,
374
+ unionVariants
375
+ };
376
+ const v = unionVariants[0];
377
+ if (v?.itemField) return {
378
+ ...v.itemField,
379
+ path,
380
+ name,
381
+ allStatic
382
+ };
383
+ if (v?.def) return {
384
+ ...base,
385
+ type: "object",
386
+ customType,
387
+ objectDef: v.def
388
+ };
389
+ }
390
+ if (kind === "tuple") {
391
+ const tupleType = prop.type;
392
+ return {
393
+ ...base,
394
+ type: "tuple",
395
+ customType,
396
+ itemFields: tupleType.items.map((item, i) => {
397
+ const field = createFieldDef(String(i), item);
398
+ field.name = "";
399
+ return field;
400
+ })
401
+ };
402
+ }
403
+ if (extractValueHelp(prop)) return {
404
+ ...base,
405
+ type: uiType ?? "ref"
406
+ };
407
+ const tags = kind === "" ? prop.type.tags : void 0;
408
+ let uiTag;
409
+ let isTimestamp = false;
410
+ if (tags) for (const t of tags) {
411
+ if (uiTag === void 0 && UI_TAGS.has(t)) uiTag = t;
412
+ if (t === "timestamp") isTimestamp = true;
413
+ }
414
+ const dt = kind === "" ? prop.type.designType : void 0;
415
+ let numericType;
416
+ if (kind === "") {
417
+ const hasCurrency = getFieldMeta(prop, "db.amount.currency") !== void 0 || getFieldMeta(prop, "db.amount.currency.ref") !== void 0;
418
+ const isDecimalDesign = dt === "decimal";
419
+ if (hasCurrency) numericType = "decimal";
420
+ else if (isDecimalDesign) numericType = "decimal";
421
+ else if (dt === "number") {
422
+ const hasUnit = getFieldMeta(prop, "db.unit") !== void 0 || getFieldMeta(prop, "db.unit.ref") !== void 0;
423
+ const hasPrefix = getFieldMeta(prop, "ui.form.prefix") !== void 0 || getFieldMeta(prop, "ui.form.prefix.ref") !== void 0 || getFieldMeta(prop, "ui.form.prefix.icon") !== void 0;
424
+ const hasSuffix = getFieldMeta(prop, "ui.form.suffix") !== void 0 || getFieldMeta(prop, "ui.form.suffix.ref") !== void 0 || getFieldMeta(prop, "ui.form.suffix.icon") !== void 0;
425
+ if (hasUnit || hasPrefix || hasSuffix) numericType = "number";
426
+ }
427
+ }
428
+ const tagType = isTimestamp ? "datetime" : void 0;
429
+ return {
430
+ ...base,
431
+ type: uiType ?? uiTag ?? numericType ?? tagType ?? (dt === "number" ? "number" : dt === "boolean" ? "checkbox" : "text"),
432
+ phantom: kind === "" && dt === "phantom"
433
+ };
434
+ }
435
+ /** Resolves the original annotated type from the type hierarchy by path. */
436
+ function resolveOriginalProp(type, path) {
437
+ const parts = path.split(".");
438
+ let current = type.type;
439
+ for (let i = 0; i < parts.length - 1; i++) {
440
+ const prop = current.props.get(parts[i]);
441
+ if (!prop || prop.type.kind !== "object") return void 0;
442
+ current = prop.type;
443
+ }
444
+ return current.props.get(parts[parts.length - 1]);
445
+ }
446
+ /** Check if a path is a child of any structured prefix (prefixes are pre-suffixed with "."). */
447
+ function isChildOfStructured(path, prefixes) {
448
+ for (const prefix of prefixes) if (path.startsWith(prefix)) return true;
449
+ return false;
450
+ }
451
+ /**
452
+ * Builds union variant definitions from a union annotated type.
453
+ * Iterates top-level items directly — one variant per item.
454
+ */
455
+ function buildUnionVariants(typeDef) {
456
+ const items = typeDef.type.items ?? [typeDef];
457
+ const variants = [];
458
+ for (const item of items) {
459
+ const v = createVariant(item);
460
+ if (items.length > 1) v.label = `${String(variants.length + 1)}. ${v.label}`;
461
+ variants.push(v);
462
+ }
463
+ return variants;
464
+ }
465
+ /** Creates a single union variant from an annotated type item. */
466
+ function createVariant(def) {
467
+ const kind = def.type.kind;
468
+ if (kind === "object") {
469
+ const label = getFieldMeta(def, "meta.label") ?? "Object";
470
+ const hasComponent = getFieldMeta(def, UI_FORM_COMPONENT) !== void 0;
471
+ return {
472
+ label,
473
+ type: def,
474
+ def: createFormDef(def),
475
+ itemField: hasComponent ? createFieldDef("", def) : void 0
476
+ };
477
+ }
478
+ if (kind === "") {
479
+ const dt = def.type.designType;
480
+ return {
481
+ label: capitalize(dt === "phantom" ? "item" : dt),
482
+ type: def,
483
+ itemField: createFieldDef("", def),
484
+ designType: dt
485
+ };
486
+ }
487
+ return {
488
+ label: capitalize(kind),
489
+ type: def,
490
+ itemField: createFieldDef("", def)
491
+ };
492
+ }
493
+ function capitalize(s) {
494
+ return s.charAt(0).toUpperCase() + s.slice(1);
495
+ }
496
+ //#endregion
497
+ //#region src/value-help/dict-paths.ts
498
+ /**
499
+ * Paths that make up the "dict view" of a value-help target:
500
+ * PKs + label + descr + attr fields. Used by filter dialogs to clamp
501
+ * visible columns to the dictionary subset.
502
+ */
503
+ function valueHelpDictPaths(resolved) {
504
+ const paths = [
505
+ ...resolved.primaryKeys,
506
+ resolved.labelField,
507
+ ...resolved.attrFields
508
+ ];
509
+ if (resolved.descrField) paths.push(resolved.descrField);
510
+ return new Set(paths);
511
+ }
512
+ //#endregion
513
+ //#region src/value-help/resolve-options.ts
514
+ /** Extracts the key from an option entry. */
515
+ function optKey(opt) {
516
+ return typeof opt === "string" ? opt : opt.key;
517
+ }
518
+ /** Extracts the display label from an option entry. */
519
+ function optLabel(opt) {
520
+ return typeof opt === "string" ? opt : opt.label;
521
+ }
522
+ /**
523
+ * Converts raw option annotation value to a normalized array.
524
+ */
525
+ function parseStaticOptions(raw) {
526
+ return asArray(raw).map((item) => {
527
+ if (typeof item === "object" && item !== null && "label" in item) {
528
+ const { label, value } = item;
529
+ return value !== void 0 ? {
530
+ key: value,
531
+ label
532
+ } : label;
533
+ }
534
+ return String(item);
535
+ });
536
+ }
537
+ /**
538
+ * Resolves options from metadata with a fallback chain:
539
+ * 1. `@ui.form.fn.options` (dynamic, compiled by ui-fns)
540
+ * 2. `@ui.form.options` (static annotation)
541
+ * 3. Literal union type extraction (auto-derived from type)
542
+ * 4. Future: dictionary / value-help lookup
543
+ */
544
+ function resolveOptions(prop, scope) {
545
+ const resolved = resolveFieldProp(prop, UI_FORM_FN_OPTIONS, UI_FORM_OPTIONS, scope, { transform: parseStaticOptions });
546
+ if (resolved !== void 0) return resolved;
547
+ return extractLiteralOptions(prop);
548
+ }
549
+ //#endregion
550
+ //#region src/value-help/value-help-client.ts
551
+ /**
552
+ * Value-help query client. Wraps a `Client` from `@atscript/db-client`
553
+ * with FK-specific search logic (regex fallback for non-searchable tables,
554
+ * $select scoping).
555
+ *
556
+ * Consumers resolve the target's metadata once via `resolveValueHelp(url)`
557
+ * and pass the resulting `ResolvedValueHelp` to `search()`. Label resolution
558
+ * for cells is deliberately unsupported — cells always display raw ids.
559
+ */
560
+ var ValueHelpClient = class {
561
+ _client;
562
+ constructor(client) {
563
+ this._client = client;
564
+ }
565
+ /**
566
+ * Search the target with value-help semantics.
567
+ *
568
+ * - If target is searchable → sends `$search` (server full-text)
569
+ * - If not searchable → sends `$or` regex across select fields + exact PK match
570
+ */
571
+ async search(resolved, opts) {
572
+ const mode = opts?.mode ?? "form";
573
+ const limit = opts?.limit ?? 20;
574
+ const selectFields = opts?.select ?? computeSelectFields(resolved, mode);
575
+ const text = opts?.text;
576
+ if (!text) return { items: await this._client.query({ controls: {
577
+ $select: selectFields,
578
+ $limit: limit
579
+ } }) };
580
+ if (resolved.searchable) return { items: await this._client.query({ controls: {
581
+ $select: selectFields,
582
+ $limit: limit,
583
+ $search: text
584
+ } }) };
585
+ const filter = buildOrFilter(text, selectFields, resolved.primaryKeys[0] ?? resolved.labelField);
586
+ return { items: await this._client.query({
587
+ filter,
588
+ controls: {
589
+ $select: selectFields,
590
+ $limit: limit
591
+ }
592
+ }) };
593
+ }
594
+ };
595
+ /**
596
+ * Compute the $select fields for a value-help query.
597
+ */
598
+ function computeSelectFields(resolved, mode) {
599
+ const fields = [...resolved.primaryKeys, resolved.labelField];
600
+ if (resolved.descrField) fields.push(resolved.descrField);
601
+ if (mode === "filter") fields.push(...resolved.attrFields);
602
+ return [...new Set(fields)];
603
+ }
604
+ /**
605
+ * Build a Uniquery `$or` filter for value-help search across multiple fields.
606
+ * Uses regex startsWith (case-insensitive) on text fields + exact match on PK.
607
+ */
608
+ function buildOrFilter(text, fields, pkField) {
609
+ const escaped = text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
610
+ const conditions = [];
611
+ for (const field of fields) if (field !== pkField) conditions.push({ [field]: { $regex: `/^${escaped}/i` } });
612
+ const asNum = Number(text);
613
+ if (!Number.isNaN(asNum)) conditions.push({ [pkField]: asNum });
614
+ else conditions.push({ [pkField]: text });
615
+ return { $or: conditions };
616
+ }
617
+ //#endregion
618
+ //#region src/client-factory.ts
619
+ const builtin = (url) => new Client(url);
620
+ let _default = builtin;
621
+ /**
622
+ * Override the app-wide default factory. Call once at startup (e.g. in
623
+ * `entry-client.ts`) to wire shared fetch, credentials, error handling, etc.
624
+ * Every table, value-help picker, and other client consumer will pick it up.
625
+ */
626
+ function setDefaultClientFactory(factory) {
627
+ _default = factory;
628
+ }
629
+ /** Current app-wide default factory. Falls back to `new Client(url)`. */
630
+ function getDefaultClientFactory() {
631
+ return _default;
632
+ }
633
+ /** Reset the default factory to the built-in one (primarily for tests). */
634
+ function resetDefaultClientFactory() {
635
+ _default = builtin;
636
+ }
637
+ //#endregion
638
+ //#region src/shared/meta-cache.ts
639
+ const cache = /* @__PURE__ */ new Map();
640
+ /**
641
+ * Get or create the cache entry for `url`. First caller's `factory` seeds the
642
+ * `Client`; subsequent callers reuse it. On `meta` rejection, the entry is
643
+ * evicted so the next call retries.
644
+ */
645
+ function getMetaEntry(url, factory) {
646
+ const existing = cache.get(url);
647
+ if (existing) return existing;
648
+ const client = (factory ?? getDefaultClientFactory())(url);
649
+ const meta = client.meta().catch((err) => {
650
+ cache.delete(url);
651
+ throw err;
652
+ });
653
+ const entry = {
654
+ client,
655
+ meta,
656
+ type: meta.then((m) => deserializeAnnotatedType(m.type))
657
+ };
658
+ cache.set(url, entry);
659
+ return entry;
660
+ }
661
+ function resetMetaCache() {
662
+ cache.clear();
663
+ }
664
+ //#endregion
665
+ //#region src/value-help/resolve.ts
666
+ /**
667
+ * Lazily fetch and extract value-help metadata for the given URL.
668
+ *
669
+ * - Issues exactly one `GET {url}/meta` per URL across the session (shared via meta-cache).
670
+ * - Concurrent callers with the same URL share the in-flight promise.
671
+ * - If the underlying fetch rejects, the cache entry is evicted so a later
672
+ * retry performs a fresh fetch.
673
+ */
674
+ function resolveValueHelp(url) {
675
+ const entry = getMetaEntry(url);
676
+ if (entry.resolved) return entry.resolved;
677
+ entry.resolved = Promise.all([entry.meta, entry.type]).then(([meta, type]) => buildResolved(url, meta, type));
678
+ return entry.resolved;
679
+ }
680
+ /** Thin alias over `resetMetaCache` — retained so existing test code keeps working. */
681
+ function resetValueHelpCache() {
682
+ resetMetaCache();
683
+ }
684
+ function buildResolved(url, meta, targetType) {
685
+ const objectType = targetType.type.kind === "object" ? targetType.type : void 0;
686
+ const primaryKeys = [];
687
+ let labelField;
688
+ let descrField;
689
+ const attrFields = [];
690
+ let firstStringField;
691
+ if (objectType) for (const [name, fieldProp] of objectType.props) {
692
+ const isPK = fieldProp.metadata.has(META_ID);
693
+ if (isPK) primaryKeys.push(name);
694
+ if (fieldProp.metadata.has("ui.dict.label")) labelField = name;
695
+ if (fieldProp.metadata.has("ui.dict.descr")) descrField = name;
696
+ if (fieldProp.metadata.has("ui.dict.attr")) attrFields.push(name);
697
+ if (!firstStringField && !isPK && fieldProp.type.kind === "" && fieldProp.type.designType === "string") firstStringField = name;
698
+ }
699
+ if (!labelField) labelField = firstStringField;
700
+ if (!labelField) labelField = primaryKeys[0] ?? "id";
701
+ const filterableFields = [];
702
+ const sortableFields = [];
703
+ if (meta.fields) for (const [name, fm] of Object.entries(meta.fields)) {
704
+ if (fm?.filterable) filterableFields.push(name);
705
+ if (fm?.sortable) sortableFields.push(name);
706
+ }
707
+ return {
708
+ url,
709
+ primaryKeys,
710
+ labelField,
711
+ descrField,
712
+ attrFields,
713
+ filterableFields,
714
+ sortableFields,
715
+ searchable: !!meta.searchable,
716
+ targetType
717
+ };
718
+ }
719
+ //#endregion
720
+ //#region src/form/path-utils.ts
721
+ /**
722
+ * Gets a nested value by dot-separated path.
723
+ * Always dereferences `obj.value` first (form data is wrapped in `{ value: domainData }`).
724
+ * When `path` is empty, returns the root domain data (`obj.value`).
725
+ */
726
+ function getByPath(obj, path) {
727
+ const root = obj.value;
728
+ if (!path) return root;
729
+ const keys = path.split(".");
730
+ let current = root;
731
+ for (const key of keys) {
732
+ if (current === null || current === void 0 || typeof current !== "object") return void 0;
733
+ current = current[key];
734
+ }
735
+ return current;
736
+ }
737
+ /**
738
+ * Sets a nested value by dot-separated path.
739
+ * Always dereferences `obj.value` first (form data is wrapped in `{ value: domainData }`).
740
+ * When `path` is empty, sets the root domain data (`obj.value = value`).
741
+ * Creates intermediate objects if they do not exist.
742
+ */
743
+ function setByPath(obj, path, value) {
744
+ if (!path) {
745
+ obj.value = value;
746
+ return;
747
+ }
748
+ const keys = path.split(".");
749
+ const last = keys.pop();
750
+ if (last === void 0) return;
751
+ let current = obj.value;
752
+ for (const key of keys) {
753
+ if (current[key] === null || current[key] === void 0 || typeof current[key] !== "object") current[key] = {};
754
+ current = current[key];
755
+ }
756
+ current[last] = value;
757
+ }
758
+ function parseStaticDefault(raw, prop) {
759
+ if (typeof raw !== "string") return raw;
760
+ if (prop.type.kind === "" && prop.type.designType === "string") return raw;
761
+ try {
762
+ return JSON.parse(raw);
763
+ } catch {
764
+ return;
765
+ }
766
+ }
767
+ /** Cached default resolver — reused when no resolver is provided. */
768
+ const defaultValueResolver = createFormValueResolver();
769
+ function createFormValueResolver(data = {}, context = {}) {
770
+ return (prop, _path) => {
771
+ return resolveFieldProp(prop, UI_FORM_FN_VALUE, META_DEFAULT, {
772
+ v: void 0,
773
+ data,
774
+ context,
775
+ entry: void 0
776
+ }, { transform: (raw) => parseStaticDefault(raw, prop) });
777
+ };
778
+ }
779
+ /**
780
+ * Type-appropriate "defined-but-empty" fallback for primitive design types
781
+ * whose structural default in atscript's `finalDefault` table is
782
+ * `undefined`. Without this, `createFormData` on an optional `decimal`
783
+ * field (or any other primitive missed by atscript's table) returns
784
+ * `undefined` — but `createFormData` is called from "explicit add"
785
+ * contexts (optional-toggle, array-add, tuple-pad, union-pick) where the
786
+ * caller's intent is "give me a value the renderer can edit", not "no
787
+ * value yet". Returning `undefined` leaves the empty-state placeholder
788
+ * stuck in AsFieldShell.
789
+ *
790
+ * `decimal → "0"` mirrors the `number → 0` default — the atscript runtime
791
+ * validator (≥ 0.1.54) rejects `""` for decimal fields, so committing the
792
+ * canonical zero is the only init that survives a submit-without-edit.
793
+ * `useAsDecimal` pads the display to the field's effective scale, so the
794
+ * user sees `0.00` / `0.000` per the `@db.column.precision` annotation.
795
+ */
796
+ function primitiveInitFallback(prop) {
797
+ if (prop.type.kind !== "") return void 0;
798
+ switch (prop.type.designType) {
799
+ case "decimal": return "0";
800
+ default: return;
801
+ }
802
+ }
803
+ function createFormData(type, resolver) {
804
+ let value = createDataFromAnnotatedType(type, { mode: resolver ?? defaultValueResolver });
805
+ if (value === void 0) value = primitiveInitFallback(type);
806
+ return { value };
807
+ }
808
+ const variantValidatorCache = /* @__PURE__ */ new WeakMap();
809
+ function getVariantValidator(variant) {
810
+ let v = variantValidatorCache.get(variant.type);
811
+ if (!v) {
812
+ v = variant.type.validator();
813
+ variantValidatorCache.set(variant.type, v);
814
+ }
815
+ return v;
816
+ }
817
+ const variantsDiscriminatorCache = /* @__PURE__ */ new WeakMap();
818
+ function getVariantsDiscriminator(variants) {
819
+ let cached = variantsDiscriminatorCache.get(variants);
820
+ if (cached === void 0) {
821
+ cached = detectDiscriminator(variants.map((v) => v.type));
822
+ variantsDiscriminatorCache.set(variants, cached);
823
+ }
824
+ return cached;
825
+ }
826
+ function detectUnionVariant(value, variants) {
827
+ if (variants.length <= 1) return 0;
828
+ const disc = getVariantsDiscriminator(variants);
829
+ if (disc && value !== null && typeof value === "object") {
830
+ const tag = value[disc.propertyName];
831
+ const idx = disc.indexMapping[String(tag)];
832
+ if (idx !== void 0) return idx;
833
+ }
834
+ for (let i = 0; i < variants.length; i++) try {
835
+ if (getVariantValidator(variants[i]).validate(value, true)) return i;
836
+ } catch {}
837
+ return 0;
838
+ }
839
+ //#endregion
840
+ //#region src/form/validate.ts
841
+ let defaultValidatorPlugins = [];
842
+ /** Replace the default validator plugins applied to every form/field validator. */
843
+ function setDefaultValidatorPlugins(plugins) {
844
+ defaultValidatorPlugins = plugins;
845
+ }
846
+ /** Get the currently registered default validator plugins. */
847
+ function getDefaultValidatorPlugins() {
848
+ return defaultValidatorPlugins;
849
+ }
850
+ /**
851
+ * Returns a reusable validator function for a whole FormDef.
852
+ *
853
+ * Validator is created once and reused on every call.
854
+ * ATScript's @expect.* validation runs automatically.
855
+ * For custom `ui.fn.*` validators, install ui-fns and pass its plugin via `opts.plugins`.
856
+ */
857
+ function getFormValidator(def, opts) {
858
+ const validator = new Validator(def.type, {
859
+ unknownProps: "ignore",
860
+ ...opts,
861
+ plugins: [...defaultValidatorPlugins, ...opts?.plugins ?? []]
862
+ });
863
+ return (callOpts) => {
864
+ if (validator.validate(callOpts.data, true, {
865
+ data: callOpts.data,
866
+ context: callOpts.context ?? {}
867
+ })) return {};
868
+ const errors = {};
869
+ for (const err of validator.errors) errors[err.path] = err.message;
870
+ return errors;
871
+ };
872
+ }
873
+ /**
874
+ * Creates a cached validator function for a single ATScript prop.
875
+ *
876
+ * The `Validator` instance is created lazily on first call and reused.
877
+ * Returns `true` when valid, or the first error message string when invalid.
878
+ */
879
+ function createFieldValidator(prop, opts) {
880
+ let cached;
881
+ return (value, externalCtx) => {
882
+ cached ??= new Validator(prop, { plugins: defaultValidatorPlugins });
883
+ if (!cached.validate(value, true, externalCtx)) {
884
+ if (opts?.rootOnly) {
885
+ const rootError = cached.errors?.find((e) => e.path === "");
886
+ if (rootError) return rootError.message;
887
+ return true;
888
+ }
889
+ return cached.errors?.[0]?.message || "Invalid value";
890
+ }
891
+ return true;
892
+ };
893
+ }
894
+ //#endregion
895
+ //#region src/form/error-utils.ts
896
+ /**
897
+ * Framework-agnostic helpers for working with form-error maps keyed by
898
+ * dotted path. Used by AsForm to drive error badges and auto-open
899
+ * collapsed sections; safe to share with React (or any other) bindings.
900
+ *
901
+ * Convention:
902
+ * - Keys are dotted paths (`a.b.c`); empty string and `__form` denote
903
+ * the form-level error.
904
+ * - Values may be `string | undefined`; falsy entries are dropped on
905
+ * merge.
906
+ */
907
+ const FORM_ERROR_KEY = "__form";
908
+ /**
909
+ * Merge any number of partial error maps into a single dense
910
+ * `Record<path, message>`. Falsy values are skipped — later sources do
911
+ * NOT overwrite earlier ones with an empty value.
912
+ */
913
+ function mergeErrorMaps(...maps) {
914
+ const merged = {};
915
+ for (const m of maps) {
916
+ if (!m) continue;
917
+ for (const k of Object.keys(m)) {
918
+ const v = m[k];
919
+ if (v) merged[k] = v;
920
+ }
921
+ }
922
+ return merged;
923
+ }
924
+ /**
925
+ * Yield every ancestor prefix of a dotted path, longest-first
926
+ * (`a.b.c` → `a.b.c`, `a.b`, `a`). Returns the path itself first so
927
+ * callers can include it in the iteration without a special case.
928
+ *
929
+ * Empty paths and the form-level key (`__form`) yield nothing.
930
+ */
931
+ function* iteratePathAncestors(path) {
932
+ if (!path || path === FORM_ERROR_KEY) return;
933
+ let pos = path.length;
934
+ while (pos > 0) {
935
+ yield path.slice(0, pos);
936
+ const dot = path.lastIndexOf(".", pos - 1);
937
+ if (dot < 0) return;
938
+ pos = dot;
939
+ }
940
+ }
941
+ /**
942
+ * Build an indexed `Map<absolutePath, descendantErrorCount>` so each
943
+ * struct in the tree can render an error-count badge in O(1).
944
+ *
945
+ * For every error path, the count is incremented on the path itself
946
+ * AND every dotted-path ancestor — so a struct at `a.b` reports the
947
+ * total of all errors at `a.b` or below.
948
+ */
949
+ function buildDescendantErrorCounts(errors) {
950
+ const map = /* @__PURE__ */ new Map();
951
+ for (const errPath of Object.keys(errors)) {
952
+ if (!errors[errPath]) continue;
953
+ for (const prefix of iteratePathAncestors(errPath)) map.set(prefix, (map.get(prefix) ?? 0) + 1);
954
+ }
955
+ return map;
956
+ }
957
+ //#endregion
958
+ //#region src/form/grid.ts
959
+ /** Grid layout parsing for `@ui.form.grid.colSpan` / `@ui.form.grid.rowSpan`. */
960
+ const DEFAULT_COL_SPAN = 12;
961
+ const DEFAULT_ROW_SPAN = 1;
962
+ const COL_SPAN_ALIASES = {
963
+ full: 12,
964
+ half: 6,
965
+ third: 4
966
+ };
967
+ /** Accepts "1"-"12" and the aliases "full" (12), "half" (6), "third" (4). */
968
+ function parseColSpan(raw) {
969
+ if (!raw) return void 0;
970
+ const aliased = COL_SPAN_ALIASES[raw];
971
+ if (aliased !== void 0) return aliased;
972
+ const n = Number.parseInt(raw, 10);
973
+ if (Number.isFinite(n) && n >= 1 && n <= 12 && String(n) === raw) return n;
974
+ }
975
+ /** Accepts numeric strings "1"+; rejects "0", negatives, decimals, aliases. */
976
+ function parseRowSpan(raw) {
977
+ if (!raw) return void 0;
978
+ const n = Number.parseInt(raw, 10);
979
+ if (Number.isFinite(n) && n >= 1 && String(n) === raw) return n;
980
+ }
981
+ /**
982
+ * Resolve a field's grid footprint. Narrow defaults to full-width / single-row
983
+ * regardless of the desktop value, so authors can opt into a narrow override
984
+ * via the second annotation arg without it inheriting an unintended desktop span.
985
+ */
986
+ function resolveGridSpec(colSpan, rowSpan) {
987
+ return {
988
+ col: {
989
+ desktop: parseColSpan(colSpan?.desktop) ?? 12,
990
+ narrow: parseColSpan(colSpan?.narrow) ?? 12
991
+ },
992
+ row: {
993
+ desktop: parseRowSpan(rowSpan?.desktop) ?? 1,
994
+ narrow: parseRowSpan(rowSpan?.narrow) ?? 1
995
+ }
996
+ };
997
+ }
998
+ /**
999
+ * Build the UnoCSS class string for a field's grid footprint.
1000
+ *
1001
+ * - Skips desktop classes that match the default (`as-grid-item` already
1002
+ * covers `col-span-full row-span-1`).
1003
+ * - Skips narrow overrides that match the desktop value (no override needed).
1004
+ * - The narrow variant uses the custom `as-narrow:` prefix, which the
1005
+ * atscript-ui UnoCSS preset rewrites to `@container as-grid (max-width:
1006
+ * 480px) { ... }`. The parent grid is registered as
1007
+ * `container-name: as-grid` via the `as-form-grid` shortcut, so the
1008
+ * rule resolves against the actual grid's inline size — not the viewport.
1009
+ *
1010
+ * Returned string is space-separated, ready to drop into a Vue class binding.
1011
+ */
1012
+ function buildGridClasses(spec) {
1013
+ const out = [];
1014
+ if (spec.col.desktop !== 12) out.push(`col-span-${spec.col.desktop}`);
1015
+ if (spec.row.desktop !== 1) out.push(`row-span-${spec.row.desktop}`);
1016
+ if (spec.col.narrow !== spec.col.desktop) out.push(`as-narrow:col-span-${spec.col.narrow}`);
1017
+ if (spec.row.narrow !== spec.row.desktop) out.push(`as-narrow:row-span-${spec.row.narrow}`);
1018
+ return out.join(" ");
1019
+ }
1020
+ //#endregion
1021
+ //#region src/form/labels.ts
1022
+ /** Singular label for an array field (used by AsArray for "Add <singular>"). */
1023
+ function resolveSingularLabel(meta) {
1024
+ if (!meta) return "item";
1025
+ return getFieldMeta(meta, "ui.form.label.singular") || "item";
1026
+ }
1027
+ //#endregion
1028
+ //#region src/form/measurement.ts
1029
+ /**
1030
+ * Read measurement annotations off a single field prop. Returns `undefined`
1031
+ * for any annotation that's absent — callers can spread the result into a
1032
+ * larger record.
1033
+ */
1034
+ function extractMeasurement(prop) {
1035
+ const precisionMeta = getFieldMeta(prop, DB_COLUMN_PRECISION);
1036
+ return {
1037
+ currencyCode: getFieldMeta(prop, DB_AMOUNT_CURRENCY),
1038
+ currencyRefField: getFieldMeta(prop, DB_AMOUNT_CURRENCY_REF),
1039
+ unitCode: getFieldMeta(prop, DB_UNIT),
1040
+ unitRefField: getFieldMeta(prop, DB_UNIT_REF),
1041
+ precisionScale: precisionMeta?.scale
1042
+ };
1043
+ }
1044
+ //#endregion
1045
+ //#region src/form/decimal-format.ts
1046
+ const WHITESPACE_GROUP_RE = /[   ]/g;
1047
+ const numberFormatCache = /* @__PURE__ */ new Map();
1048
+ function getNumberFormat(locale, opts) {
1049
+ const key = `${locale ?? ""}|${opts.style ?? ""}|${opts.currency ?? ""}|${opts.currencyDisplay ?? ""}|${opts.useGrouping ?? ""}|${opts.minimumFractionDigits ?? ""}|${opts.maximumFractionDigits ?? ""}`;
1050
+ let f = numberFormatCache.get(key);
1051
+ if (!f) {
1052
+ f = new Intl.NumberFormat(locale, opts);
1053
+ numberFormatCache.set(key, f);
1054
+ }
1055
+ return f;
1056
+ }
1057
+ const decimalSeparatorCache = /* @__PURE__ */ new Map();
1058
+ const thousandsSeparatorCache = /* @__PURE__ */ new Map();
1059
+ const currencyDisplayCache = /* @__PURE__ */ new Map();
1060
+ const currencyDecimalsCache = /* @__PURE__ */ new Map();
1061
+ /** "." in en-US, "," in fr-FR. Returns "." when locale is undefined. */
1062
+ function getDecimalSeparator(locale) {
1063
+ const key = locale ?? "";
1064
+ const cached = decimalSeparatorCache.get(key);
1065
+ if (cached !== void 0) return cached;
1066
+ let out = ".";
1067
+ try {
1068
+ out = getNumberFormat(locale, { useGrouping: false }).formatToParts(1.1).find((p) => p.type === "decimal")?.value ?? ".";
1069
+ } catch {}
1070
+ decimalSeparatorCache.set(key, out);
1071
+ return out;
1072
+ }
1073
+ /**
1074
+ * "," in en-US, NNBSP (U+202F) in fr-FR. Returns "" when no grouping is
1075
+ * applied (or when Intl rejects the locale).
1076
+ */
1077
+ function getThousandsSeparator(locale) {
1078
+ const key = locale ?? "";
1079
+ const cached = thousandsSeparatorCache.get(key);
1080
+ if (cached !== void 0) return cached;
1081
+ let out = "";
1082
+ try {
1083
+ out = getNumberFormat(locale, { useGrouping: true }).formatToParts(1e6).find((p) => p.type === "group")?.value ?? "";
1084
+ } catch {}
1085
+ thousandsSeparatorCache.set(key, out);
1086
+ return out;
1087
+ }
1088
+ function getCurrencyDisplayParts(code, locale) {
1089
+ const key = `${locale ?? ""}|${code}`;
1090
+ const cached = currencyDisplayCache.get(key);
1091
+ if (cached) return cached;
1092
+ let out = {
1093
+ symbol: code,
1094
+ position: "prefix"
1095
+ };
1096
+ try {
1097
+ const parts = getNumberFormat(locale, {
1098
+ style: "currency",
1099
+ currency: code,
1100
+ currencyDisplay: "narrowSymbol"
1101
+ }).formatToParts(0);
1102
+ const idxCur = parts.findIndex((p) => p.type === "currency");
1103
+ const idxInt = parts.findIndex((p) => p.type === "integer");
1104
+ out = {
1105
+ symbol: parts[idxCur]?.value ?? code,
1106
+ position: idxInt === -1 || idxCur < idxInt ? "prefix" : "suffix"
1107
+ };
1108
+ } catch {}
1109
+ currencyDisplayCache.set(key, out);
1110
+ return out;
1111
+ }
1112
+ /**
1113
+ * Currency's natural decimal count via Intl. JPY=0, USD/EUR=2, BHD/KWD=3.
1114
+ * Returns `undefined` for codes Intl doesn't know — caller falls back to
1115
+ * `dbPrecisionScale`.
1116
+ */
1117
+ function getCurrencyDecimals(code, locale) {
1118
+ if (!code) return void 0;
1119
+ const key = `${locale ?? ""}|${code}`;
1120
+ if (currencyDecimalsCache.has(key)) return currencyDecimalsCache.get(key);
1121
+ let out;
1122
+ try {
1123
+ const max = getNumberFormat(locale, {
1124
+ style: "currency",
1125
+ currency: code
1126
+ }).resolvedOptions().maximumFractionDigits;
1127
+ out = typeof max === "number" ? max : void 0;
1128
+ } catch {
1129
+ out = void 0;
1130
+ }
1131
+ currencyDecimalsCache.set(key, out);
1132
+ return out;
1133
+ }
1134
+ /**
1135
+ * Parse a user-typed decimal string. Returns the canonical decimal
1136
+ * (no thousands separator, "." as decimal separator) or `null` if invalid.
1137
+ * Accepts both "." and "," as the decimal separator (locale-aware).
1138
+ * Strips the locale thousands separator. Preserves sign. Does NOT
1139
+ * enforce scale.
1140
+ */
1141
+ function parseDecimalInput(raw, locale) {
1142
+ if (typeof raw !== "string") return null;
1143
+ const trimmed = raw.trim();
1144
+ if (trimmed === "") return null;
1145
+ const decSep = getDecimalSeparator(locale);
1146
+ const thouSep = getThousandsSeparator(locale);
1147
+ let sign = "";
1148
+ let body = trimmed;
1149
+ if (body.startsWith("-")) {
1150
+ sign = "-";
1151
+ body = body.slice(1);
1152
+ } else if (body.startsWith("+")) body = body.slice(1);
1153
+ if (body.length === 0) return null;
1154
+ if (body.includes("-") || body.includes("+")) return null;
1155
+ if (thouSep && body.includes(thouSep)) {
1156
+ const decIdx = body.indexOf(decSep);
1157
+ const headRaw = decIdx === -1 ? body : body.slice(0, decIdx);
1158
+ const tail = decIdx === -1 ? "" : body.slice(decIdx);
1159
+ if (tail.includes(thouSep)) return null;
1160
+ if (headRaw.includes(thouSep)) {
1161
+ const groups = headRaw.split(thouSep);
1162
+ const first = groups[0];
1163
+ if (first.length < 1 || first.length > 3 || !/^\d+$/.test(first)) return null;
1164
+ for (let i = 1; i < groups.length; i += 1) if (!/^\d{3}$/.test(groups[i])) return null;
1165
+ }
1166
+ body = `${headRaw.split(thouSep).join("")}${tail}`;
1167
+ }
1168
+ body = body.replace(WHITESPACE_GROUP_RE, "");
1169
+ if (decSep !== ".") {
1170
+ if ((body.match(/,/g)?.length ?? 0) > 1) return null;
1171
+ if (body.includes(".") && body.includes(",")) return null;
1172
+ if (body.includes(",")) body = body.replace(",", ".");
1173
+ } else if (body.includes(",")) {
1174
+ if (body.includes(".")) return null;
1175
+ if ((body.match(/,/g)?.length ?? 0) > 1) return null;
1176
+ body = body.replace(",", ".");
1177
+ }
1178
+ if (!/^\d*\.?\d*$/.test(body)) return null;
1179
+ if (body === "" || body === ".") return null;
1180
+ if (body.endsWith(".")) body = body.slice(0, -1);
1181
+ if (body.startsWith(".")) body = `0${body}`;
1182
+ const dot = body.indexOf(".");
1183
+ if (dot === -1) body = body.replace(/^0+(\d)/, "$1");
1184
+ else {
1185
+ const head = body.slice(0, dot);
1186
+ const tail = body.slice(dot);
1187
+ body = `${head.replace(/^0+(\d)/, "$1")}${tail}`;
1188
+ }
1189
+ if (sign === "-" && /^0+(\.0+)?$/.test(body)) sign = "";
1190
+ return `${sign}${body}`;
1191
+ }
1192
+ /**
1193
+ * Enforce a fractional-digit count by truncating or padding. String-only —
1194
+ * no float arithmetic. Default behaviour truncates (no rounding) so digits
1195
+ * the user typed can't silently shift. Pass `roundHalfUp: true` to round.
1196
+ *
1197
+ * enforceScale("12.345", 2) → "12.34" (truncate)
1198
+ * enforceScale("12.3", 4) → "12.3000" (pad)
1199
+ * enforceScale("12", 0) → "12"
1200
+ * enforceScale("12.99", 0) → "12" (no rounding)
1201
+ */
1202
+ function enforceScale(s, scale, opts) {
1203
+ if (typeof s !== "string" || s === "") return s;
1204
+ if (scale === void 0 || scale < 0) return s;
1205
+ const parts = splitDecimalString(s);
1206
+ if (scale === 0) {
1207
+ if (opts?.roundHalfUp && parts.decimal.length > 0 && parts.decimal[0] >= "5") {
1208
+ const bumped = addOne(parts.integer);
1209
+ return joinDecimalString({
1210
+ sign: parts.sign,
1211
+ integer: bumped,
1212
+ decimal: ""
1213
+ });
1214
+ }
1215
+ return joinDecimalString({
1216
+ sign: parts.sign,
1217
+ integer: parts.integer,
1218
+ decimal: ""
1219
+ });
1220
+ }
1221
+ if (parts.decimal.length === scale) return joinDecimalString(parts);
1222
+ if (parts.decimal.length < scale) return joinDecimalString({
1223
+ sign: parts.sign,
1224
+ integer: parts.integer,
1225
+ decimal: parts.decimal.padEnd(scale, "0")
1226
+ });
1227
+ let head = parts.decimal.slice(0, scale);
1228
+ const dropped = parts.decimal.slice(scale);
1229
+ let integer = parts.integer;
1230
+ if (opts?.roundHalfUp && dropped.length > 0 && dropped[0] >= "5") {
1231
+ const bumpedFrac = addOne(head);
1232
+ if (bumpedFrac.length > head.length) {
1233
+ integer = addOne(integer);
1234
+ head = "0".repeat(scale);
1235
+ } else head = bumpedFrac;
1236
+ }
1237
+ return joinDecimalString({
1238
+ sign: parts.sign,
1239
+ integer,
1240
+ decimal: head
1241
+ });
1242
+ }
1243
+ function addOne(digits) {
1244
+ if (digits === "") return "1";
1245
+ const out = digits.split("");
1246
+ let i = out.length - 1;
1247
+ let carry = 1;
1248
+ while (i >= 0 && carry > 0) {
1249
+ const d = out[i].charCodeAt(0) - 48 + carry | 0;
1250
+ if (d >= 10) {
1251
+ out[i] = "0";
1252
+ carry = 1;
1253
+ } else {
1254
+ out[i] = String.fromCharCode(d + 48);
1255
+ carry = 0;
1256
+ }
1257
+ i -= 1;
1258
+ }
1259
+ if (carry > 0) out.unshift("1");
1260
+ return out.join("");
1261
+ }
1262
+ function splitDecimalString(s) {
1263
+ if (typeof s !== "string" || s === "") return {
1264
+ sign: "",
1265
+ integer: "0",
1266
+ decimal: ""
1267
+ };
1268
+ let sign = "";
1269
+ let body = s;
1270
+ if (body.startsWith("-")) {
1271
+ sign = "-";
1272
+ body = body.slice(1);
1273
+ } else if (body.startsWith("+")) body = body.slice(1);
1274
+ if (body === "") return {
1275
+ sign: "",
1276
+ integer: "0",
1277
+ decimal: ""
1278
+ };
1279
+ const dot = body.indexOf(".");
1280
+ let integer;
1281
+ let decimal;
1282
+ if (dot === -1) {
1283
+ integer = body;
1284
+ decimal = "";
1285
+ } else {
1286
+ integer = body.slice(0, dot);
1287
+ decimal = body.slice(dot + 1);
1288
+ }
1289
+ if (integer === "") integer = "0";
1290
+ integer = integer.replace(/^0+(\d)/, "$1");
1291
+ if (sign === "-" && integer === "0" && !decimal.replace(/0/g, "")) sign = "";
1292
+ return {
1293
+ sign,
1294
+ integer,
1295
+ decimal
1296
+ };
1297
+ }
1298
+ function joinDecimalString(parts) {
1299
+ const integer = parts.integer === "" ? "0" : parts.integer;
1300
+ const decimal = parts.decimal;
1301
+ const body = decimal === "" ? integer : `${integer}.${decimal}`;
1302
+ return `${parts.sign}${body}`;
1303
+ }
1304
+ function formatDecimalForDisplay(opts) {
1305
+ const { value, scale, locale, currency, unit, useGrouping = true } = opts;
1306
+ if (value === null || value === void 0 || value === "") return "";
1307
+ let canonical;
1308
+ if (typeof value === "number") {
1309
+ if (!Number.isFinite(value)) return "";
1310
+ canonical = String(value);
1311
+ } else canonical = value;
1312
+ if (!/^[+-]?\d*(\.\d*)?$/.test(canonical) || canonical === "." || canonical === "-") return canonical;
1313
+ if (scale !== void 0 && scale >= 0) canonical = enforceScale(canonical, scale);
1314
+ const parts = splitDecimalString(canonical);
1315
+ const asNumber = Number(canonical);
1316
+ const intlSafe = Number.isFinite(asNumber);
1317
+ if (currency) {
1318
+ try {
1319
+ const fmt = getNumberFormat(locale, {
1320
+ style: "currency",
1321
+ currency,
1322
+ currencyDisplay: "narrowSymbol",
1323
+ minimumFractionDigits: scale,
1324
+ maximumFractionDigits: scale,
1325
+ useGrouping
1326
+ });
1327
+ if (intlSafe) return fmt.format(asNumber);
1328
+ } catch {}
1329
+ return `${currency} ${formatPlain(parts, scale, locale, useGrouping)}`;
1330
+ }
1331
+ const base = formatPlain(parts, scale, locale, useGrouping);
1332
+ return unit ? `${base} ${unit}` : base;
1333
+ }
1334
+ function formatPlain(parts, scale, locale, useGrouping) {
1335
+ const intStr = useGrouping ? groupInteger(parts.integer, locale) : parts.integer;
1336
+ const decStr = scale !== void 0 && scale >= 0 ? parts.decimal.padEnd(scale, "0").slice(0, scale) : parts.decimal;
1337
+ const decSep = getDecimalSeparator(locale);
1338
+ const body = decStr.length > 0 ? `${intStr}${decSep}${decStr}` : intStr;
1339
+ return `${parts.sign}${body}`;
1340
+ }
1341
+ /**
1342
+ * Insert the locale's thousands separator into a plain integer string.
1343
+ * Pure string-based — no float math, handles arbitrary length.
1344
+ */
1345
+ function groupInteger(integer, locale) {
1346
+ const sep = getThousandsSeparator(locale);
1347
+ if (!sep) return integer;
1348
+ if (integer.length <= 3) return integer;
1349
+ const out = [];
1350
+ let i = integer.length;
1351
+ while (i > 3) {
1352
+ out.unshift(integer.slice(i - 3, i));
1353
+ i -= 3;
1354
+ }
1355
+ out.unshift(integer.slice(0, i));
1356
+ return out.join(sep);
1357
+ }
1358
+ //#endregion
1359
+ //#region src/table/create-table-def.ts
1360
+ /**
1361
+ * Builds a TableDef from a moost-db MetaResponse.
1362
+ *
1363
+ * 1. Deserializes `meta.type` into a live TAtscriptAnnotatedType
1364
+ * 2. Flattens to discover all field paths
1365
+ * 3. Builds ColumnDef per field using annotations + meta.fields capabilities
1366
+ * 4. Sorts by @ui.table.order
1367
+ */
1368
+ function createTableDef(meta, preDeserializedType) {
1369
+ const type = preDeserializedType ?? deserializeAnnotatedType(meta.type);
1370
+ const flatMap = type.type.kind === "object" ? flattenAnnotatedType(type, { excludePhantomTypes: true }) : /* @__PURE__ */ new Map();
1371
+ const columns = [];
1372
+ for (const [path, prop] of flatMap.entries()) {
1373
+ if (path === "") continue;
1374
+ if (!(path in meta.fields)) {
1375
+ const kind = prop.type.kind;
1376
+ if (path.includes(".") || kind === "object" || kind === "array") continue;
1377
+ }
1378
+ const fieldMeta = meta.fields[path];
1379
+ const options = extractLiteralOptions(prop);
1380
+ const valueHelpInfo = extractValueHelp(prop);
1381
+ const maxLengthMeta = getFieldMeta(prop, EXPECT_MAX_LENGTH);
1382
+ const tableType = getFieldMeta(prop, UI_TABLE_TYPE);
1383
+ const sharedType = getFieldMeta(prop, UI_TYPE);
1384
+ const tableComponent = getFieldMeta(prop, UI_TABLE_COMPONENT);
1385
+ columns.push({
1386
+ path,
1387
+ label: getFieldMeta(prop, "meta.label") ?? humanizePath(path),
1388
+ type: tableType ?? sharedType ?? (valueHelpInfo ? "ref" : inferDisplayType(prop, options)),
1389
+ component: tableComponent,
1390
+ sortable: fieldMeta?.sortable ?? false,
1391
+ filterable: fieldMeta?.filterable ?? false,
1392
+ nullable: prop.optional === true,
1393
+ visible: getFieldMeta(prop, UI_TABLE_HIDDEN) === void 0,
1394
+ width: getFieldMeta(prop, UI_TABLE_WIDTH),
1395
+ maxLen: maxLengthMeta?.length,
1396
+ order: getFieldMeta(prop, "ui.table.order") ?? Infinity,
1397
+ options,
1398
+ valueHelpInfo,
1399
+ ...extractMeasurement(prop)
1400
+ });
1401
+ }
1402
+ columns.sort((a, b) => a.order - b.order);
1403
+ const actions = groupActions(meta.actions ?? []);
1404
+ const crud = meta.crud ?? {};
1405
+ return {
1406
+ type,
1407
+ columns,
1408
+ flatMap,
1409
+ primaryKeys: meta.primaryKeys,
1410
+ preferredId: meta.preferredId ?? meta.primaryKeys,
1411
+ crud,
1412
+ canRemove: "remove" in crud,
1413
+ actions,
1414
+ searchable: meta.searchable,
1415
+ vectorSearchable: meta.vectorSearchable,
1416
+ searchIndexes: meta.searchIndexes,
1417
+ relations: meta.relations
1418
+ };
1419
+ }
1420
+ /** Sort by (order ?? 0). `toSorted` is stable per spec, so ties preserve declaration order. */
1421
+ function byOrder(xs) {
1422
+ return xs.toSorted((x, y) => (x.order ?? 0) - (y.order ?? 0));
1423
+ }
1424
+ /**
1425
+ * Partition actions by `level`, sort each group by `(order ?? 0)` then
1426
+ * declaration order, and pick the first `default: true` entry per level.
1427
+ * The synthesised `__remove` UI action lives outside this set and is never
1428
+ * a candidate for `default.row`.
1429
+ */
1430
+ function groupActions(actions) {
1431
+ const table = [];
1432
+ const row = [];
1433
+ const rows = [];
1434
+ for (const a of actions) if (a.level === "table") table.push(a);
1435
+ else if (a.level === "row") row.push(a);
1436
+ else if (a.level === "rows") rows.push(a);
1437
+ const tableSorted = byOrder(table);
1438
+ const rowSorted = byOrder(row);
1439
+ const rowsSorted = byOrder(rows);
1440
+ return {
1441
+ table: tableSorted,
1442
+ row: rowSorted,
1443
+ rows: rowsSorted,
1444
+ default: {
1445
+ table: tableSorted.find((a) => a.default === true),
1446
+ row: rowSorted.find((a) => a.default === true),
1447
+ rows: rowsSorted.find((a) => a.default === true)
1448
+ }
1449
+ };
1450
+ }
1451
+ /** Infers a display type string from the annotated type's kind and designType. */
1452
+ function inferDisplayType(prop, literalOpts) {
1453
+ const kind = prop.type.kind;
1454
+ if (kind === "array") return "array";
1455
+ if (kind === "object") return "object";
1456
+ if (kind === "union") return literalOpts !== void 0 ? "enum" : "union";
1457
+ if (kind === "") {
1458
+ const final = prop.type;
1459
+ const dt = final.designType;
1460
+ if (dt === "number") return final.tags?.has("timestamp") ? "datetime" : "number";
1461
+ if (dt === "decimal") return "number";
1462
+ if (dt === "boolean") return "boolean";
1463
+ return "text";
1464
+ }
1465
+ return "text";
1466
+ }
1467
+ /** Converts a dot-path to a human-readable label (e.g. 'firstName' → 'First Name'). */
1468
+ function humanizePath(path) {
1469
+ return path.slice(path.lastIndexOf(".") + 1).replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (s) => s.toUpperCase());
1470
+ }
1471
+ //#endregion
1472
+ //#region src/shared/str.ts
1473
+ /** Safely convert an unknown value to a string without triggering no-base-to-string lint errors. */
1474
+ function str(value) {
1475
+ if (typeof value === "string") return value;
1476
+ if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") return `${value}`;
1477
+ return JSON.stringify(value) ?? "";
1478
+ }
1479
+ //#endregion
1480
+ //#region src/table/column-resolver.ts
1481
+ /** Get visible columns only, already sorted by order. */
1482
+ function getVisibleColumns(def) {
1483
+ return def.columns.filter((c) => c.visible);
1484
+ }
1485
+ /** Get sortable columns. */
1486
+ function getSortableColumns(def) {
1487
+ return def.columns.filter((c) => c.sortable);
1488
+ }
1489
+ /** Get filterable columns. */
1490
+ function getFilterableColumns(def) {
1491
+ return def.columns.filter((c) => c.filterable);
1492
+ }
1493
+ /** Find a column by path. */
1494
+ function getColumn(def, path) {
1495
+ return def.columns.find((c) => c.path === path);
1496
+ }
1497
+ //#endregion
1498
+ export { DB_AMOUNT_CURRENCY, DB_AMOUNT_CURRENCY_REF, DB_COLUMN_PRECISION, DB_HTTP_PATH, DB_REL_FK, DB_UNIT, DB_UNIT_REF, DEFAULT_COL_SPAN, DEFAULT_ROW_SPAN, EXPECT_MAX_LENGTH, META_DEFAULT, META_DESCRIPTION, META_ID, META_LABEL, META_READONLY, META_REQUIRED, META_SENSITIVE, StaticFieldResolver, UI_DICT_ATTR, UI_DICT_DESCR, UI_DICT_FILTERABLE, UI_DICT_LABEL, UI_DICT_SEARCHABLE, UI_DICT_SORTABLE, UI_FORM_ACTION, UI_FORM_ATTR, UI_FORM_AUTOCOMPLETE, UI_FORM_CLASSES, UI_FORM_COMPONENT, UI_FORM_DISABLED, UI_FORM_FN_ATTR, UI_FORM_FN_CLASSES, UI_FORM_FN_DESCRIPTION, UI_FORM_FN_DISABLED, UI_FORM_FN_HIDDEN, UI_FORM_FN_HINT, UI_FORM_FN_LABEL, UI_FORM_FN_OPTIONS, UI_FORM_FN_PLACEHOLDER, UI_FORM_FN_PREFIX, UI_FORM_FN_READONLY, UI_FORM_FN_STYLES, UI_FORM_FN_SUBMIT_DISABLED, UI_FORM_FN_SUBMIT_TEXT, UI_FORM_FN_TITLE, UI_FORM_FN_VALUE, UI_FORM_GRID_COL_SPAN, UI_FORM_GRID_ROW_SPAN, UI_FORM_HIDDEN, UI_FORM_HINT, UI_FORM_LABEL_SINGULAR, UI_FORM_OPTIONS, UI_FORM_ORDER, UI_FORM_PLACEHOLDER, UI_FORM_PREFIX, UI_FORM_PREFIX_ICON, UI_FORM_PREFIX_REF, UI_FORM_STYLES, UI_FORM_SUBMIT_TEXT, UI_FORM_SUFFIX, UI_FORM_SUFFIX_ICON, UI_FORM_SUFFIX_REF, UI_FORM_TYPE, UI_FORM_VALIDATE, UI_TABLE_ATTR, UI_TABLE_CLASSES, UI_TABLE_COMPONENT, UI_TABLE_FN_ATTR, UI_TABLE_FN_CLASSES, UI_TABLE_FN_PREFIX, UI_TABLE_FN_STYLES, UI_TABLE_HIDDEN, UI_TABLE_ORDER, UI_TABLE_STYLES, UI_TABLE_TYPE, UI_TABLE_WIDTH, UI_TYPE, ValueHelpClient, WF_ACTION_WITH_DATA, asArray, buildDescendantErrorCounts, buildGridClasses, buildUnionVariants, createFieldValidator, createFormData, createFormDef, createFormValueResolver, createTableDef, defaultResolver, detectUnionVariant, enforceScale, extractLiteralOptions, extractMeasurement, extractValueHelp, formatDecimalForDisplay, getByPath, getColumn, getCurrencyDecimals, getCurrencyDisplayParts, getDecimalSeparator, getDefaultClientFactory, getDefaultValidatorPlugins, getFieldMeta, getFilterableColumns, getFormValidator, getMetaEntry, getResolver, getSortableColumns, getThousandsSeparator, getVisibleColumns, groupInteger, hasComputedAnnotations, isArrayField, isObjectField, isPureLiteralUnion, isTupleField, isUnionField, iteratePathAncestors, joinDecimalString, mergeErrorMaps, optKey, optLabel, parseColSpan, parseDecimalInput, parseRowSpan, parseStaticAttrs, parseStaticOptions, resetDefaultClientFactory, resetMetaCache, resetValueHelpCache, resolveAttrs, resolveFieldProp, resolveFormProp, resolveGridSpec, resolveOptions, resolveSingularLabel, resolveStatic, resolveValueHelp, setByPath, setDefaultClientFactory, setDefaultValidatorPlugins, setResolver, splitDecimalString, str, valueHelpDictPaths };