@coffer-org/plugin-transit 7.1.1 → 7.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.
- package/dist/schema.js +18 -2200
- package/package.json +3 -3
package/dist/schema.js
CHANGED
|
@@ -21,12 +21,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
21
21
|
enumerable: true
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
|
-
//#region ../sdk/
|
|
24
|
+
//#region ../sdk/dist/library.js
|
|
25
25
|
function defineLibrary(v) {
|
|
26
26
|
return v;
|
|
27
27
|
}
|
|
28
28
|
//#endregion
|
|
29
|
-
//#region ../sdk/
|
|
29
|
+
//#region ../sdk/dist/plugin.js
|
|
30
30
|
function definePlugin(p) {
|
|
31
31
|
if (!p.id) throw new Error("[plugin] missing id");
|
|
32
32
|
if (!p.label) throw new Error(`[plugin] ${p.id}: missing label`);
|
|
@@ -4069,7 +4069,7 @@ function number(params) {
|
|
|
4069
4069
|
return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
|
|
4070
4070
|
}
|
|
4071
4071
|
//#endregion
|
|
4072
|
-
//#region ../sdk/
|
|
4072
|
+
//#region ../sdk/dist/units.js
|
|
4073
4073
|
var UNITS_CURRENCY = [
|
|
4074
4074
|
{
|
|
4075
4075
|
value: "UAH",
|
|
@@ -4373,20 +4373,6 @@ var UNITS_MAP = {
|
|
|
4373
4373
|
label: "core.units.ppm"
|
|
4374
4374
|
}]
|
|
4375
4375
|
};
|
|
4376
|
-
/**
|
|
4377
|
-
* Resolve a unit spec — a named scale or an explicit option list — to its options.
|
|
4378
|
-
*
|
|
4379
|
-
* Throws rather than returning `undefined`, because both ways of reaching `undefined` here
|
|
4380
|
-
* produce a field that CONSTRUCTS and then crashes when someone tries to save through it:
|
|
4381
|
-
* `measured`'s unit role closes over these options and calls `.some(…)` on them inside its
|
|
4382
|
-
* refine, so a missing spec or a typo'd scale name turns a user's save into a
|
|
4383
|
-
* `Cannot read properties of undefined` instead of a validation message. The golden matrix
|
|
4384
|
-
* recorded exactly that for `f.measured({})`.
|
|
4385
|
-
*
|
|
4386
|
-
* A declaration error belongs at declaration time, where the plugin author sees it and the
|
|
4387
|
-
* message can name the scale they meant — the same reason the host-services seam fails at
|
|
4388
|
-
* registration rather than at first render.
|
|
4389
|
-
*/
|
|
4390
4376
|
function resolveUnits(u) {
|
|
4391
4377
|
if (typeof u === "string") {
|
|
4392
4378
|
const known = UNITS_MAP[u];
|
|
@@ -4397,48 +4383,25 @@ function resolveUnits(u) {
|
|
|
4397
4383
|
return u;
|
|
4398
4384
|
}
|
|
4399
4385
|
//#endregion
|
|
4400
|
-
//#region ../sdk/
|
|
4401
|
-
/** Structured message for zod: JSON {code, params}. Decoded by mutate.ts. */
|
|
4386
|
+
//#region ../sdk/dist/fields/validation.js
|
|
4402
4387
|
function vmsg(code, params) {
|
|
4403
4388
|
return JSON.stringify(params ? {
|
|
4404
4389
|
code,
|
|
4405
4390
|
params
|
|
4406
4391
|
} : { code });
|
|
4407
4392
|
}
|
|
4408
|
-
/** v4 error-map: message for a missing value (formerly required_error). */
|
|
4409
4393
|
function reqErr(code = "required") {
|
|
4410
4394
|
return { error: (iss) => iss.input === void 0 ? vmsg(code) : void 0 };
|
|
4411
4395
|
}
|
|
4412
|
-
/** v4 error-map: message for an invalid type (formerly invalid_type_error). */
|
|
4413
4396
|
function typeErr(code = "invalid_type") {
|
|
4414
4397
|
return { error: (iss) => iss.code === "invalid_type" ? vmsg(code) : void 0 };
|
|
4415
4398
|
}
|
|
4416
|
-
/** v4 error-map: required + invalid_type together (formerly required_error + invalid_type_error). */
|
|
4417
4399
|
function reqTypeErr() {
|
|
4418
4400
|
return { error: (iss) => iss.code === "invalid_type" ? iss.input === void 0 ? vmsg("required") : vmsg("invalid_type") : void 0 };
|
|
4419
4401
|
}
|
|
4420
|
-
/**
|
|
4421
|
-
* A string schema whose CONTENT checks only speak once the value is known to be a string.
|
|
4422
|
-
*
|
|
4423
|
-
* zod's length checks are not type-guarded: `min`/`max` read `input.length`, and an ARRAY has
|
|
4424
|
-
* one. So `z.string().min(1)` answers `[]` with `invalid_type` AND `too_small` — two messages
|
|
4425
|
-
* about a single wrongness, both of which reach the user, since `mutate.ts` and `extend-io.ts`
|
|
4426
|
-
* turn every issue into its own `ValidationError`. (A number answers with `invalid_type`
|
|
4427
|
-
* alone, having no `length` to read — so the redundancy was array-shaped and invisible until
|
|
4428
|
-
* the golden matrix started recording the whole issue list instead of `issues[0]`.)
|
|
4429
|
-
*
|
|
4430
|
-
* Piping puts the type question first and alone: `add` never runs on a non-string. Pass the
|
|
4431
|
-
* content checks as a builder rather than chaining them onto the result, because a `ZodPipe`
|
|
4432
|
-
* has no `.min`/`.max`/`.regex` to chain.
|
|
4433
|
-
*/
|
|
4434
4402
|
function stringContent(add) {
|
|
4435
4403
|
return string$1(reqTypeErr()).pipe(add(string$1()));
|
|
4436
4404
|
}
|
|
4437
|
-
/** Parse a JSON string, else return the native value (object/array) as-is.
|
|
4438
|
-
* Tolerant input for native form-state AND legacy JSON-string payloads.
|
|
4439
|
-
* A string that fails JSON.parse is returned unchanged — callers detect
|
|
4440
|
-
* parse failure via `typeof result === 'string'` (inners here are object/array,
|
|
4441
|
-
* never a bare string). */
|
|
4442
4405
|
function jsonValue(raw) {
|
|
4443
4406
|
if (typeof raw === "string") try {
|
|
4444
4407
|
return JSON.parse(raw);
|
|
@@ -4447,12 +4410,6 @@ function jsonValue(raw) {
|
|
|
4447
4410
|
}
|
|
4448
4411
|
return raw;
|
|
4449
4412
|
}
|
|
4450
|
-
/**
|
|
4451
|
-
* Factory for fields that store JSON and validate it with a nested zod schema.
|
|
4452
|
-
* Accepts a native object/array (native form state) OR a JSON string (legacy).
|
|
4453
|
-
* Single parse through jsonValue → inner.safeParse → issue `code`; an unparseable
|
|
4454
|
-
* string remains a string → code 'json'.
|
|
4455
|
-
*/
|
|
4456
4413
|
function jsonRefined(inner, code) {
|
|
4457
4414
|
return unknown().superRefine((raw, ctx) => {
|
|
4458
4415
|
const parsed = jsonValue(raw);
|
|
@@ -4469,37 +4426,11 @@ function jsonRefined(inner, code) {
|
|
|
4469
4426
|
});
|
|
4470
4427
|
});
|
|
4471
4428
|
}
|
|
4472
|
-
/**
|
|
4473
|
-
* An empty value is no value: `''` and `null` become `undefined`, and the field's own schema
|
|
4474
|
-
* never sees them. Every field is optional, so this is the ONLY shape — a field cannot demand
|
|
4475
|
-
* a value, and the rules it carries judge what was entered rather than whether anything was.
|
|
4476
|
-
*
|
|
4477
|
-
* This used to have a second branch, for `required` fields, which turned the same emptiness
|
|
4478
|
-
* into a `required` issue through a `z.unknown()` guard piped into the schema. That branch is
|
|
4479
|
-
* why a `.min(1)` on a required string was unreachable: the empty string had already been
|
|
4480
|
-
* converted to the issue before the string schema ran.
|
|
4481
|
-
*/
|
|
4482
4429
|
function optionalize(schema) {
|
|
4483
4430
|
return preprocess((v) => v === "" || v === null ? void 0 : v, schema.optional());
|
|
4484
4431
|
}
|
|
4485
4432
|
//#endregion
|
|
4486
|
-
//#region ../sdk/
|
|
4487
|
-
/**
|
|
4488
|
-
* Field META modifiers — the shared machinery a built field (FieldMeta) passes through
|
|
4489
|
-
* regardless of its type: value options (`applyOptions`/`bindOptionsZod`), the `multiple`
|
|
4490
|
-
* array wrap (`applyMultiple`), and the final node-or-bare-FieldMeta shape
|
|
4491
|
-
* (`wrapKey`, EVERY factory's last step).
|
|
4492
|
-
*
|
|
4493
|
-
* A LEAF module, the same shape as `fields/validation.ts`/`fields/normalize.ts`: it
|
|
4494
|
-
* borrows the FieldMeta/LayoutNode/FieldClient/ColumnType/OptionItem SHAPES from
|
|
4495
|
-
* `../fields.ts` as TYPE-ONLY imports (erased at compile time — no runtime edge), but has
|
|
4496
|
-
* NO runtime dependency on `../fields.ts` itself. This is what lets
|
|
4497
|
-
* `materialize/pipeline.ts` import `applyMultiple`/`wrapKey` from here instead of from
|
|
4498
|
-
* `fields.ts`, which in turn is what lets `fields.ts` import `materialize` FROM
|
|
4499
|
-
* `materialize/pipeline.ts` without closing a cycle (`fields.ts` → `pipeline.ts` →
|
|
4500
|
-
* `fields/meta.ts`, nothing pointing back). `fields.ts` re-exports every symbol here, so
|
|
4501
|
-
* no external import path (`@coffer-org/sdk/fields`) changes.
|
|
4502
|
-
*/
|
|
4433
|
+
//#region ../sdk/dist/fields/meta.js
|
|
4503
4434
|
function safeJsonParse(s) {
|
|
4504
4435
|
try {
|
|
4505
4436
|
return JSON.parse(s);
|
|
@@ -4507,14 +4438,6 @@ function safeJsonParse(s) {
|
|
|
4507
4438
|
return;
|
|
4508
4439
|
}
|
|
4509
4440
|
}
|
|
4510
|
-
/**
|
|
4511
|
-
* Wraps a field into a JSON array of values (storage: TEXT). The inner zod is `base.zod`.
|
|
4512
|
-
* Modifier order: base → applyMultiple.
|
|
4513
|
-
*
|
|
4514
|
-
* Exception: kind 'image'/'media' handle multiple themselves in the renderer (gallery) —
|
|
4515
|
-
* the MultipleField* wrappers on the web skip them.
|
|
4516
|
-
*/
|
|
4517
|
-
/** Zod "JSON array of inner values" — used by applyMultiple. */
|
|
4518
4441
|
function multipleZod(inner) {
|
|
4519
4442
|
return unknown().superRefine((raw, ctx) => {
|
|
4520
4443
|
let arr;
|
|
@@ -4565,15 +4488,12 @@ function applyMultiple(base, multiple) {
|
|
|
4565
4488
|
zod: optionalize(s)
|
|
4566
4489
|
};
|
|
4567
4490
|
}
|
|
4568
|
-
/** Inline option entry → OptionItem (plain string = value and label at once). */
|
|
4569
4491
|
function toOptionItem(o) {
|
|
4570
4492
|
return typeof o === "string" ? {
|
|
4571
4493
|
value: o,
|
|
4572
4494
|
title: o
|
|
4573
4495
|
} : o;
|
|
4574
4496
|
}
|
|
4575
|
-
/** Canonical comparison form. Both the input and the option value pass through it, so a
|
|
4576
|
-
* numeric field compares `100` against the option spelled `'100'`. */
|
|
4577
4497
|
function optionKey(column, v) {
|
|
4578
4498
|
if (column === "integer" || column === "real") {
|
|
4579
4499
|
const n = Number(v);
|
|
@@ -4581,12 +4501,6 @@ function optionKey(column, v) {
|
|
|
4581
4501
|
}
|
|
4582
4502
|
return String(v ?? "");
|
|
4583
4503
|
}
|
|
4584
|
-
/**
|
|
4585
|
-
* Membership check LAYERED OVER the field's own schema, never replacing it. Replacing it
|
|
4586
|
-
* with `z.enum` — which is what select used to do — works only over a bare string: over an
|
|
4587
|
-
* int it would destroy the int-ness, the coercion and the min/max, and over an ip it would
|
|
4588
|
-
* destroy the structure. Layering keeps every type's own error code and adds one.
|
|
4589
|
-
*/
|
|
4590
4504
|
function gateZod(inner, gate, multiple, column) {
|
|
4591
4505
|
return inner.superRefine((v, ctx) => {
|
|
4592
4506
|
if (gate.values === null || v === void 0 || v === null || v === "") return;
|
|
@@ -4598,11 +4512,6 @@ function gateZod(inner, gate, multiple, column) {
|
|
|
4598
4512
|
});
|
|
4599
4513
|
});
|
|
4600
4514
|
}
|
|
4601
|
-
/**
|
|
4602
|
-
* Attaches `options`/`strict` to any field. Called from `wrapKey`, which EVERY factory
|
|
4603
|
-
* funnels through — core and plugin-defined alike — so a new field type gets value options
|
|
4604
|
-
* without writing a line for them.
|
|
4605
|
-
*/
|
|
4606
4515
|
function applyOptions(base, o) {
|
|
4607
4516
|
if (base.optionGate) return base;
|
|
4608
4517
|
if (base.relation || base.parts || base.options) return base;
|
|
@@ -4631,22 +4540,10 @@ function applyOptions(base, o) {
|
|
|
4631
4540
|
}
|
|
4632
4541
|
return m;
|
|
4633
4542
|
}
|
|
4634
|
-
/**
|
|
4635
|
-
* Fills (or refills) a strict field's closed set. Called by composeRegistry once the full
|
|
4636
|
-
* option set is known — inline options, plugin contributions and the named source merged.
|
|
4637
|
-
* A non-strict field has no gate and is left alone.
|
|
4638
|
-
*/
|
|
4639
4543
|
function bindOptionsZod(field, values) {
|
|
4640
4544
|
if (!field.optionGate || !values.length) return;
|
|
4641
4545
|
field.optionGate.values = new Set(values.map((v) => optionKey(field.column, v)));
|
|
4642
4546
|
}
|
|
4643
|
-
/**
|
|
4644
|
-
* Wraps a FieldMeta into a node. `key` is the name the PARENT gave this child
|
|
4645
|
-
* (materialize/pipeline.ts) — an argument, not an option a factory reads out of its own
|
|
4646
|
-
* opts, which is why it comes first. Absent means the element was never declared under a
|
|
4647
|
-
* name: it becomes a keyless value node when it carries a `value`, and a bare FieldMeta
|
|
4648
|
-
* otherwise.
|
|
4649
|
-
*/
|
|
4650
4547
|
function wrapKey(key, opts, meta) {
|
|
4651
4548
|
let m = applyOptions(meta, opts);
|
|
4652
4549
|
if (opts.noEditControl) m = {
|
|
@@ -4766,7 +4663,7 @@ function wrapKey(key, opts, meta) {
|
|
|
4766
4663
|
return m;
|
|
4767
4664
|
}
|
|
4768
4665
|
//#endregion
|
|
4769
|
-
//#region ../sdk/
|
|
4666
|
+
//#region ../sdk/dist/fields/normalize.js
|
|
4770
4667
|
function normalizeOpts(rawIn) {
|
|
4771
4668
|
const raw = rawIn;
|
|
4772
4669
|
if (raw["view"] !== void 0) throw new Error("[normalizeOpts] `view` is gone — presentational options live in `ui: {}` (see CLAUDE.md § field author options and the field-types skill).");
|
|
@@ -4813,30 +4710,8 @@ function normalizeOpts(rawIn) {
|
|
|
4813
4710
|
return Object.fromEntries(Object.entries(result).filter(([, val]) => val !== void 0));
|
|
4814
4711
|
}
|
|
4815
4712
|
//#endregion
|
|
4816
|
-
//#region ../sdk/
|
|
4817
|
-
/**
|
|
4818
|
-
* PHASE 1 — declaration. A factory returns this and nothing else: plain data, no zod,
|
|
4819
|
-
* no column, no widget. A parent may rewrite ANY property of a child's declaration
|
|
4820
|
-
* before phase 2 builds it (see materialize/pipeline.ts).
|
|
4821
|
-
*/
|
|
4713
|
+
//#region ../sdk/dist/materialize/decl.js
|
|
4822
4714
|
var BRAND = "__fieldDecl";
|
|
4823
|
-
/**
|
|
4824
|
-
* `opts.fields` vs `decl.fields` — the same author input reachable two ways, and only one of
|
|
4825
|
-
* them is ever safe to read as BUILT children:
|
|
4826
|
-
*
|
|
4827
|
-
* - `opts.fields` is the RAW author input, unpacked by `normalizeOpts`'s spread (it does
|
|
4828
|
-
* not know or care that `fields` is special) — never rewritten, never materialized. A
|
|
4829
|
-
* composite that computes its OWN roles from what the author passed (`check`'s content
|
|
4830
|
-
* fields) reads it from here, at declare() time, exactly as the author wrote it — decls
|
|
4831
|
-
* and all.
|
|
4832
|
-
* - `decl.fields` is what `declare()` copies from that same raw input for the tree walk
|
|
4833
|
-
* (`materialize/pipeline.ts`) and for `resolveParts` to find. Once a parent has been
|
|
4834
|
-
* materialized, this is where its BUILT children live.
|
|
4835
|
-
*
|
|
4836
|
-
* A consumer that wants built children — not raw author input — reads the materialized tree
|
|
4837
|
-
* (`materialize()`/`materializeTree()`'s output), never `decl.opts.fields`: that stays
|
|
4838
|
-
* exactly what the author passed, decls included, for as long as the decl is unbuilt.
|
|
4839
|
-
*/
|
|
4840
4715
|
function declare(factory, raw) {
|
|
4841
4716
|
const opts = normalizeOpts(raw);
|
|
4842
4717
|
const spec = raw;
|
|
@@ -4851,9 +4726,7 @@ function isDecl(v) {
|
|
|
4851
4726
|
return typeof v === "object" && v !== null && BRAND in v;
|
|
4852
4727
|
}
|
|
4853
4728
|
//#endregion
|
|
4854
|
-
//#region ../sdk/
|
|
4855
|
-
/** True for a type registered through `roles`/`roleZod`/`structureCode` (materialize/composite.ts)
|
|
4856
|
-
* rather than a plain `build()` — read generically, never by factory name. */
|
|
4729
|
+
//#region ../sdk/dist/materialize/registry.js
|
|
4857
4730
|
function isComposite(type) {
|
|
4858
4731
|
return "roles" in type;
|
|
4859
4732
|
}
|
|
@@ -4867,12 +4740,6 @@ function typeOf(factory) {
|
|
|
4867
4740
|
if (!def) throw new Error(`[types] unknown factory '${factory}'`);
|
|
4868
4741
|
return def;
|
|
4869
4742
|
}
|
|
4870
|
-
/**
|
|
4871
|
-
* `presetOpts` merged as DEFAULTS under the caller's own opts (`{ ...presetOpts, ...o }`) —
|
|
4872
|
-
* an author's own value always wins. `config` is merged the same way one level deep, so a
|
|
4873
|
-
* preset's own default (`weight`'s `config.min: 1`) survives an author setting a DIFFERENT
|
|
4874
|
-
* config key (`rules: { max: 5000 }`) without the preset needing to repeat it.
|
|
4875
|
-
*/
|
|
4876
4743
|
function mergePresetOpts(presetOpts, o) {
|
|
4877
4744
|
const config = presetOpts.config || o.config ? {
|
|
4878
4745
|
...presetOpts.config,
|
|
@@ -4884,37 +4751,6 @@ function mergePresetOpts(presetOpts, o) {
|
|
|
4884
4751
|
...config ? { config } : {}
|
|
4885
4752
|
};
|
|
4886
4753
|
}
|
|
4887
|
-
/**
|
|
4888
|
-
* Registers a PRESET: a type that is its base type's `TypeDef` with fixed/default opts
|
|
4889
|
-
* merged in, and — where it differs — its own `kind`/`widget`/`build`. This is what lets
|
|
4890
|
-
* `field-presets.ts` express "a preset is its base type plus these opts" as DATA instead of
|
|
4891
|
-
* copy-pasting normalizeOpts/optionalize/applyMultiple/wrapKey into a hand-written body:
|
|
4892
|
-
* `kind`/`prim`/`widget`/`selfManages` default to the base type's own — a
|
|
4893
|
-
* preset differs from its base in BEHAVIOUR, not in what column/prim family it belongs to; a
|
|
4894
|
-
* change that DOES need a different prim/column is not a preset, it is a new type — and
|
|
4895
|
-
* `over` replaces exactly the properties a given preset differs in.
|
|
4896
|
-
*
|
|
4897
|
-
* `presetOpts` are DEFAULTS the caller's own opts can still override (see `mergePresetOpts`
|
|
4898
|
-
* above). A preset that must instead FORCE a value regardless of the caller (`country`'s
|
|
4899
|
-
* `options: 'countries'`, `tags`'s `multiple: true`) is not this mechanism — those forward
|
|
4900
|
-
* directly to the base factory with the forced opts spread AFTER the caller's own
|
|
4901
|
-
* (`string({ ...raw, options: 'countries' })`), the pattern `country`/`currency`/`percent`/
|
|
4902
|
-
* `year`/`tags` already use, none of which needs its own registry entry: their `factory` IS
|
|
4903
|
-
* the base's, unchanged.
|
|
4904
|
-
*
|
|
4905
|
-
* `over.build`, when given, replaces the base type's `build()` entirely for THIS preset —
|
|
4906
|
-
* still called with the merged opts, so it may itself delegate to the base's own `build()`
|
|
4907
|
-
* and layer extra hints on top (`snippet` adds `hints.language` over `text`'s own build).
|
|
4908
|
-
*
|
|
4909
|
-
* Resolves `base` LAZILY (on first actual read, memoized), never at the moment
|
|
4910
|
-
* `registerPreset` itself is called: `field-presets.ts` calls it at MODULE TOP LEVEL, inside
|
|
4911
|
-
* the same circular import (`fields.ts` ⇄ `field-presets.ts`) its own header note already
|
|
4912
|
-
* documents — depending on which module the loader entered first, the base's own
|
|
4913
|
-
* `registerType` call (in `fields.ts`) may not have run yet. `text`/`i18n`/`smallText` in
|
|
4914
|
-
* fields.ts sidestep the same trap by calling `typeOf('string')` only from inside their
|
|
4915
|
-
* `build()`, never at their own `registerType()` call site; a preset's `kind`/`prim`/
|
|
4916
|
-
* `widget`/`selfManages` need the identical deferral since they too default from the base.
|
|
4917
|
-
*/
|
|
4918
4754
|
function registerPreset(factory, base, presetOpts, over = {}) {
|
|
4919
4755
|
let resolved;
|
|
4920
4756
|
const baseType = () => {
|
|
@@ -4941,21 +4777,7 @@ function registerPreset(factory, base, presetOpts, over = {}) {
|
|
|
4941
4777
|
});
|
|
4942
4778
|
}
|
|
4943
4779
|
//#endregion
|
|
4944
|
-
//#region ../sdk/
|
|
4945
|
-
/**
|
|
4946
|
-
* ONE composite transform. It was written six times (geo, illustrated, measured, range,
|
|
4947
|
-
* dimensions, check(single)) and differed only in the validation code and the per-role
|
|
4948
|
-
* default zod.
|
|
4949
|
-
*
|
|
4950
|
-
* raw → jsonValue → carries nothing? (passthrough, when `acceptsEmptyRow`)
|
|
4951
|
-
* → carries nothing at all? (pass through, there is nothing to judge)
|
|
4952
|
-
* → rowSchema (partsRowShape) → stripServerOwnedParts → optional per-type refine
|
|
4953
|
-
*
|
|
4954
|
-
* A `CompositeTypeDef` names ONLY what its own type contributes: its roles, the default
|
|
4955
|
-
* zod per role, and how its structure failure reports. Everything else — the empty
|
|
4956
|
-
* check, the JSON tolerance, the row parse, stripping server-owned parts — lives here
|
|
4957
|
-
* exactly once.
|
|
4958
|
-
*/
|
|
4780
|
+
//#region ../sdk/dist/materialize/composite.js
|
|
4959
4781
|
function buildComposite(def, opts, parts) {
|
|
4960
4782
|
const rowSchema = object(partsRowShape(def.roleZod(opts, parts), parts));
|
|
4961
4783
|
const roleKeys = (which) => parts.filter((p) => p.mode === "stored" && (which?.(p) ?? true)).map(partValueKey);
|
|
@@ -4964,7 +4786,6 @@ function buildComposite(def, opts, parts) {
|
|
|
4964
4786
|
const parsed = jsonValue(raw);
|
|
4965
4787
|
const p = parsed;
|
|
4966
4788
|
const isRow = p == null || typeof p === "object" && !Array.isArray(p);
|
|
4967
|
-
/** Every one of `keys` empty in this value — `null`/absent alike. */
|
|
4968
4789
|
const allEmpty = (keys) => isRow && (p == null || keys.length > 0 && keys.every((k) => p[k] == null));
|
|
4969
4790
|
if (clientKeys && allEmpty(clientKeys)) return raw;
|
|
4970
4791
|
if (typeof parsed === "string") {
|
|
@@ -4999,27 +4820,7 @@ function buildComposite(def, opts, parts) {
|
|
|
4999
4820
|
};
|
|
5000
4821
|
}
|
|
5001
4822
|
//#endregion
|
|
5002
|
-
//#region ../sdk/
|
|
5003
|
-
/**
|
|
5004
|
-
* PHASE 2 — materialization. The six steps every field goes through, in one place:
|
|
5005
|
-
* they used to be copied into 40-odd factory bodies (43 normalizeOpts heads,
|
|
5006
|
-
* 38 wrapKey tails, 37 optionalize calls).
|
|
5007
|
-
*
|
|
5008
|
-
* Composites (roles, the shared structure transform) arrive in materialize/composite.ts;
|
|
5009
|
-
* this file resolves a composite's parts and picks the right build path, but the
|
|
5010
|
-
* transform itself lives there, exactly once.
|
|
5011
|
-
*/
|
|
5012
|
-
/**
|
|
5013
|
-
* `name` is the property this declaration was declared under in its parent's map — the ONLY
|
|
5014
|
-
* source of a key a declared field ever has (a factory has no `key` option to read one from)
|
|
5015
|
-
* and it is threaded straight through to `wrapKey`, which keys the result whenever `name` is
|
|
5016
|
-
* defined. Absent (a top-level `materialize()` call on a standalone declaration) the result
|
|
5017
|
-
* is keyless: a value node if it carries a `value`, a bare FieldMeta otherwise.
|
|
5018
|
-
*
|
|
5019
|
-
* This function does not itself stamp the result's `name` (the ADDRESS, `LayoutNode.name` —
|
|
5020
|
-
* see that type's own doc comment) — its caller, `materializeEl`, does that for every branch,
|
|
5021
|
-
* including this one.
|
|
5022
|
-
*/
|
|
4823
|
+
//#region ../sdk/dist/materialize/pipeline.js
|
|
5023
4824
|
function materialize(decl, name) {
|
|
5024
4825
|
const opts = decl.opts;
|
|
5025
4826
|
const type = typeOf(decl.factory);
|
|
@@ -5045,22 +4846,6 @@ function materialize(decl, name) {
|
|
|
5045
4846
|
};
|
|
5046
4847
|
return wrapKey(name, opts, type.selfManages?.has("multiple") ? base : applyMultiple(base, opts.multiple ?? false));
|
|
5047
4848
|
}
|
|
5048
|
-
/**
|
|
5049
|
-
* The shared storage transform behind a composite's `multiple` form: a collection group
|
|
5050
|
-
* (child table) whose row fields ARE the given parts, under their role names — the exact
|
|
5051
|
-
* `ContainerNode` shape `fields.ts`'s own `group({key,label,multiple:true,required,fields,view})`
|
|
5052
|
-
* would build for this call pattern (no icon, no `rules`, no `value` — none of the three
|
|
5053
|
-
* former call sites ever passed them), constructed directly rather than through `group()`
|
|
5054
|
-
* itself to avoid a runtime edge back into fields.ts (see this module's own header note).
|
|
5055
|
-
*
|
|
5056
|
-
* Reached automatically, by the storage rule in `materialize()` above, for every composite
|
|
5057
|
-
* registered through `registerType`/`CompositeTypeDef` — `illustrated`, `check` and
|
|
5058
|
-
* `period` among them. `check`'s `roles(opts)` already returns checkbox-first order for
|
|
5059
|
-
* `multiple` and content-first for `single`, and `period`'s `roles(opts)` returns the same
|
|
5060
|
-
* from/until order regardless of `multiple`, so the generic path needs no special case for
|
|
5061
|
-
* either — the row-field ORDER is each type's own declared data (its `roles`), not
|
|
5062
|
-
* something this helper infers.
|
|
5063
|
-
*/
|
|
5064
4849
|
function collectionGroup(key, opts, parts, collectionView) {
|
|
5065
4850
|
const rowFields = parts.map((p) => ({
|
|
5066
4851
|
...wrapKey(p.role, p.mode === "computedStored" ? { value: p.value } : {}, p.meta),
|
|
@@ -5078,22 +4863,6 @@ function collectionGroup(key, opts, parts, collectionView) {
|
|
|
5078
4863
|
fields: rowFields
|
|
5079
4864
|
};
|
|
5080
4865
|
}
|
|
5081
|
-
/**
|
|
5082
|
-
* One tree node, named by the property it was declared under. Every returned node — every
|
|
5083
|
-
* branch below — carries that name (`name`, the ADDRESS a parent reaches it by) once `name`
|
|
5084
|
-
* is defined; only the STORAGE name (`key`) is conditional.
|
|
5085
|
-
*
|
|
5086
|
-
* A DECLARATION takes that name as both its `name` and its `key` (`materialize` → `wrapKey`
|
|
5087
|
-
* always keys a named declaration). An already-built element takes `key` too, but only if it
|
|
5088
|
-
* OWNS A SCOPE and has not been named yet: a `ContainerNode` that declares `scope: 'nest'`
|
|
5089
|
-
* owns a column prefix or a child table, and that scope has to be named — `group()`/`row()`/
|
|
5090
|
-
* `keyed()` no longer take a key of their own, so the parent's property name is where it
|
|
5091
|
-
* comes from. Everything else — a `'hoist'` group (an editorial block, a sheet row: it
|
|
5092
|
-
* arranges children inside the parent's scope and owns none), a node already carrying its
|
|
5093
|
-
* key, a keyless value node, a pseudo-element — owns no scope, so it keeps whatever `key` it
|
|
5094
|
-
* already had (usually none), but still gets `name` stamped: that is the whole point of this
|
|
5095
|
-
* task — a `'hoist'` child's declared property name used to be discarded here entirely.
|
|
5096
|
-
*/
|
|
5097
4866
|
function materializeEl(el, name) {
|
|
5098
4867
|
if (isDecl(el)) {
|
|
5099
4868
|
const built = materialize(el, name);
|
|
@@ -5119,61 +4888,25 @@ function materializeEl(el, name) {
|
|
|
5119
4888
|
name
|
|
5120
4889
|
} : el;
|
|
5121
4890
|
}
|
|
5122
|
-
/**
|
|
5123
|
-
* A built group that owns a scope but has not been given its storage key yet — i.e. it HAS
|
|
5124
|
-
* children (`fields`, the same question `hasChildren` in `../fields.ts` asks) and a `'nest'`
|
|
5125
|
-
* scope, but no key. Checked structurally rather than by importing `hasChildren`: this file
|
|
5126
|
-
* has no RUNTIME edge back into `fields.ts` (see the header note) — `Array.isArray` is the
|
|
5127
|
-
* same HAS-question, asked locally to keep that boundary.
|
|
5128
|
-
*/
|
|
5129
4891
|
function isNamelessScope(el) {
|
|
5130
4892
|
const g = el;
|
|
5131
4893
|
return typeof g === "object" && g !== null && Array.isArray(g.fields) && g.scope === "nest" && g.key == null;
|
|
5132
4894
|
}
|
|
5133
|
-
/**
|
|
5134
|
-
* A bare `FieldMeta`: it HAS none of a node's own marks yet — no `el` (so not a static value or
|
|
5135
|
-
* a pseudo-element — those are keyless ON PURPOSE and keep only an address) and no `key` (so
|
|
5136
|
-
* not a named field) — but it DOES carry a built field's own marks (`kind`, `zod`). This is a
|
|
5137
|
-
* declaration-stage shape, before it becomes a node at all, so it asks about its own
|
|
5138
|
-
* properties directly rather than through `hasValue`/`hasChildren` (which read `.type`/
|
|
5139
|
-
* `.fields`, not `.kind`/`.zod`).
|
|
5140
|
-
*/
|
|
5141
4895
|
function isNamelessField(el) {
|
|
5142
4896
|
const m = el;
|
|
5143
4897
|
return typeof m === "object" && m !== null && !("el" in m) && !("key" in m) && typeof m.kind === "string" && m.zod !== void 0;
|
|
5144
4898
|
}
|
|
5145
|
-
/** `group()` falls back to the key for a missing label; with the key arriving here instead,
|
|
5146
|
-
* that fallback has to be applied here too, or a nameless scope would keep an empty one. */
|
|
5147
4899
|
function labelOf(el, name) {
|
|
5148
4900
|
const g = el;
|
|
5149
4901
|
return g.label === void 0 || g.label === "" ? name : g.label;
|
|
5150
4902
|
}
|
|
5151
4903
|
var NUMERIC_LIKE$1 = /^\d+$/;
|
|
5152
|
-
/**
|
|
5153
|
-
* A LayoutEl tree: declarations become built elements, everything else passes through.
|
|
5154
|
-
* ONE input shape — a NAMED MAP, whose property name becomes the child's key. A
|
|
5155
|
-
* numeric-like name throws: object iteration would reorder such keys, and declaration
|
|
5156
|
-
* order IS the field order.
|
|
5157
|
-
*/
|
|
5158
4904
|
function materializeTree(els) {
|
|
5159
4905
|
return Object.entries(els).map(([name, el]) => {
|
|
5160
4906
|
if (NUMERIC_LIKE$1.test(name)) throw new Error(`fields: numeric-like child name '${name}' (object iteration would reorder the children)`);
|
|
5161
4907
|
return materializeEl(el, name);
|
|
5162
4908
|
});
|
|
5163
4909
|
}
|
|
5164
|
-
/**
|
|
5165
|
-
* A ShelfDef/ExtendDef/SettingsDef-shaped object: materializes its `fields` tree, every
|
|
5166
|
-
* other property carried through unchanged. `defineShelf`/`defineExtend`/`defineSettings`
|
|
5167
|
-
* (and `composeRegistry`'s defensive pass over a hand-rolled manifest) all call this instead
|
|
5168
|
-
* of repeating `{ ...def, fields: materializeTree(def.fields) as LayoutEl[] }` — the
|
|
5169
|
-
* copy-paste this refactor exists to delete, one level up from the factory bodies.
|
|
5170
|
-
*
|
|
5171
|
-
* `T`'s own `fields` may be the author-facing `LayoutInput` (a `ShelfDefInput`/
|
|
5172
|
-
* `ExtendDefInput`/`SettingsDefInput`) or an already-built `LayoutEl[]` (materializeDef is
|
|
5173
|
-
* a no-op on an already-built tree — see fields.ts's own note on that) — either way, the
|
|
5174
|
-
* RETURNED object's `fields` is always the built `LayoutEl[]`: the input type widens, the
|
|
5175
|
-
* materialized type never does.
|
|
5176
|
-
*/
|
|
5177
4910
|
function materializeDef(def) {
|
|
5178
4911
|
const fields = Array.isArray(def.fields) ? def.fields : materializeTree(def.fields);
|
|
5179
4912
|
return {
|
|
@@ -5182,25 +4915,7 @@ function materializeDef(def) {
|
|
|
5182
4915
|
};
|
|
5183
4916
|
}
|
|
5184
4917
|
//#endregion
|
|
5185
|
-
//#region ../sdk/
|
|
5186
|
-
/**
|
|
5187
|
-
* Composite value parts.
|
|
5188
|
-
*
|
|
5189
|
-
* A composite field type declares its ROLES (what the pieces of its value mean); the
|
|
5190
|
-
* author of a shelf supplies FILLINGS for those roles through `parts`. The storage
|
|
5191
|
-
* columns of the composite are derived from the result instead of being written by
|
|
5192
|
-
* hand, and each piece is an ordinary field — typed, labelled and renderable.
|
|
5193
|
-
*
|
|
5194
|
-
* The role set is closed: consumers (unit conversion, formatting, describe_shelf) rely
|
|
5195
|
-
* on knowing which piece is the magnitude and which is the unit. What FILLS a role is
|
|
5196
|
-
* open: a magnitude may be real/int/rating, a unit may be a select or a relation.
|
|
5197
|
-
*/
|
|
5198
|
-
/**
|
|
5199
|
-
* `NormalizedOpts` (what `FieldDecl.opts` actually is) has no single `ui` property — the
|
|
5200
|
-
* author's `ui` is flattened into individual fields (`hidden`, `display`, `kind`, …)
|
|
5201
|
-
* by `normalizeOpts`. A correction that wants to carry presentation forward has to collect
|
|
5202
|
-
* those flat fields back into the nested shape `declare()`'s own `normalizeOpts` call expects.
|
|
5203
|
-
*/
|
|
4918
|
+
//#region ../sdk/dist/parts.js
|
|
5204
4919
|
var UI_KEYS = [
|
|
5205
4920
|
"hidden",
|
|
5206
4921
|
"noEditControl",
|
|
@@ -5213,40 +4928,18 @@ var UI_KEYS = [
|
|
|
5213
4928
|
"compareWith",
|
|
5214
4929
|
"span"
|
|
5215
4930
|
];
|
|
5216
|
-
/** The author's presentation opts, re-nested from `opts`'s flat fields — `undefined` when
|
|
5217
|
-
* none were set, so a correction never adds an empty `ui: {}` no filling ever had. */
|
|
5218
4931
|
function uiOf(opts) {
|
|
5219
4932
|
if (!opts) return void 0;
|
|
5220
4933
|
const v = {};
|
|
5221
4934
|
for (const k of UI_KEYS) if (opts[k] !== void 0) v[k] = opts[k];
|
|
5222
4935
|
return Object.keys(v).length ? v : void 0;
|
|
5223
4936
|
}
|
|
5224
|
-
/** A filling built from a ported factory arrives as a FieldDecl — build it into the
|
|
5225
|
-
* node or bare FieldMeta it represents; anything else (an unported factory's
|
|
5226
|
-
* result, or an already-built filling) passes through unchanged. `name` (the role) is
|
|
5227
|
-
* the part's own storage key (`ResolvedPart.key` is always the role, see below) — passing
|
|
5228
|
-
* it to `materialize` is what lets `wrapKey` recognize a computed filling as computed
|
|
5229
|
-
* (its `computed` test requires a key) and stamp `compute`/`derived`/`hints.noEditControl`
|
|
5230
|
-
* on the built meta, exactly as it would for any other keyed field. */
|
|
5231
4937
|
function built(input, name) {
|
|
5232
4938
|
return isDecl(input) ? materialize(input, name) : input;
|
|
5233
4939
|
}
|
|
5234
|
-
/**
|
|
5235
|
-
* Whether `def` admits `d` as-is — the diagnostic question `RoleDef.accepts` answers.
|
|
5236
|
-
* `resolveParts` uses this to decide whether a correction happened (and is worth logging);
|
|
5237
|
-
* a role's own `normalize` uses it to decide whether to return the filling unchanged.
|
|
5238
|
-
*/
|
|
5239
4940
|
function accepts(def, d) {
|
|
5240
4941
|
return Array.isArray(def.accepts) ? def.accepts.includes(typeOf(d.factory).prim) : def.accepts.test(d);
|
|
5241
4942
|
}
|
|
5242
|
-
/**
|
|
5243
|
-
* The correction every role gets for free: keep what the author said ABOUT the field
|
|
5244
|
-
* (`label`, `ui`, `value` — a constant or a ComputeFn), replace the type, and
|
|
5245
|
-
* drop what belonged to the OLD type (`rules` — a number's min/max mean nothing to a
|
|
5246
|
-
* picture — and `multiple`, because a composite role owns exactly one column). A role whose
|
|
5247
|
-
* own default needs more than a bare factory (`measured`'s unit, `period`'s endpoints)
|
|
5248
|
-
* writes its own `normalize` instead of using this.
|
|
5249
|
-
*/
|
|
5250
4943
|
function retypeTo(factory) {
|
|
5251
4944
|
return (d) => {
|
|
5252
4945
|
const { label, value } = d?.opts ?? {};
|
|
@@ -5258,12 +4951,6 @@ function retypeTo(factory) {
|
|
|
5258
4951
|
});
|
|
5259
4952
|
};
|
|
5260
4953
|
}
|
|
5261
|
-
/**
|
|
5262
|
-
* Build a RoleDef whose correction is the shared default: accept a filling `accepts`
|
|
5263
|
-
* admits UNCHANGED, otherwise `retypeTo(defaultFactory)`. The common case — every role that
|
|
5264
|
-
* needs no per-instance default (a currency scaled to the field's own `units`, an endpoint
|
|
5265
|
-
* at the composite's own granularity) reads as one line.
|
|
5266
|
-
*/
|
|
5267
4954
|
function role(name, acceptsSpec, defaultFactory) {
|
|
5268
4955
|
const retype = retypeTo(defaultFactory);
|
|
5269
4956
|
const def = {
|
|
@@ -5273,28 +4960,15 @@ function role(name, acceptsSpec, defaultFactory) {
|
|
|
5273
4960
|
};
|
|
5274
4961
|
return def;
|
|
5275
4962
|
}
|
|
5276
|
-
/**
|
|
5277
|
-
* THE NAMING RULE for a composite: every part lives under its ROLE name, always — in the
|
|
5278
|
-
* value object (`{ value: 5, unit: 'kg' }`) and in storage (column `<field key>__<role>`,
|
|
5279
|
-
* `partColumns`). Consumers that reason about MEANING (a widget looking for the unit, unit
|
|
5280
|
-
* conversion, formatting) may hard-code the role name as a property name directly; this
|
|
5281
|
-
* helper exists only so a caller that already has a `ResolvedPart`/`PartSlot` need not know
|
|
5282
|
-
* that.
|
|
5283
|
-
*/
|
|
5284
4963
|
function partValueKey(p) {
|
|
5285
4964
|
return p.role;
|
|
5286
4965
|
}
|
|
5287
|
-
/** role → the key that role occupies in the value object. See `partValueKey`. */
|
|
5288
4966
|
function partValueKeys(parts) {
|
|
5289
4967
|
const out = {};
|
|
5290
4968
|
for (const p of parts) out[p.role] = p.role;
|
|
5291
4969
|
return out;
|
|
5292
4970
|
}
|
|
5293
4971
|
var NUMERIC_LIKE = /^\d+$/;
|
|
5294
|
-
/** The three shapes a factory returns, reduced to (key, meta, value). `name` is the
|
|
5295
|
-
* role this filling occupies — threaded to `built()` so a computed filling is built AS a
|
|
5296
|
-
* keyed field (see `built`'s own note); `ResolvedPart.key` below still always uses the
|
|
5297
|
-
* role directly, never this function's `key`. */
|
|
5298
4972
|
function readInput(input, name) {
|
|
5299
4973
|
input = built(input, name);
|
|
5300
4974
|
if (hasValue(input) && input.key !== void 0) return {
|
|
@@ -5313,18 +4987,6 @@ function readInput(input, name) {
|
|
|
5313
4987
|
value: void 0
|
|
5314
4988
|
};
|
|
5315
4989
|
}
|
|
5316
|
-
/**
|
|
5317
|
-
* Roles in declaration order, each with its filling. `owner` names the composite this
|
|
5318
|
-
* call resolves parts for (its own field key, supplied by the pipeline) — used only to
|
|
5319
|
-
* tag the one diagnostic line below.
|
|
5320
|
-
*
|
|
5321
|
-
* Throws with a message naming the composite's roles for an UNKNOWN role name — a typo
|
|
5322
|
-
* there fails loudly at `defineShelf` time rather than yielding a column that silently
|
|
5323
|
-
* never appears. It has nothing to say about a RENAMED storage key any more: a filling
|
|
5324
|
-
* carries no name of its own, so the role is the only source of one. A filling whose TYPE
|
|
5325
|
-
* the role does not accept is never rejected either: the role is a GUARD, not a filter, and
|
|
5326
|
-
* CORRECTS it instead (`RoleDef.normalize`) — see this module's header note.
|
|
5327
|
-
*/
|
|
5328
4990
|
function resolveParts(roles, spec, owner) {
|
|
5329
4991
|
const known = new Set(roles.map((r) => r.role));
|
|
5330
4992
|
for (const r of roles) if (r.role.includes("__")) throw new Error(`parts: role name '${r.role}' contains '__', the storage path separator`);
|
|
@@ -5373,25 +5035,6 @@ function resolveParts(roles, spec, owner) {
|
|
|
5373
5035
|
for (const p of out) if (p.meta.parts) assertInertNesting(p.meta, `${owner ?? "(unkeyed)"}.${p.key}`);
|
|
5374
5036
|
return out;
|
|
5375
5037
|
}
|
|
5376
|
-
/**
|
|
5377
|
-
* A composite filling a composite's role STORES fine at any depth: `partColumns` expands
|
|
5378
|
-
* it, and the value↔columns walks (`flattenComposite`/`nestComposite`) follow it. The
|
|
5379
|
-
* COMPUTE side does not follow it yet. Every engine that addresses a composite part reaches
|
|
5380
|
-
* exactly ONE level below a field:
|
|
5381
|
-
*
|
|
5382
|
-
* - `completeValue` / `resolveField` (server/compute-unit.ts) — runs a part's `value`
|
|
5383
|
-
* - `applyStoredParts` (server/part-injection.ts) — writes the computed column
|
|
5384
|
-
* - `cacheClockColumns` / `levelClockColumnsByOwner` (server/cache-clock.ts) — the
|
|
5385
|
-
* `__cachedAt` column a `cache` needs to ever refresh
|
|
5386
|
-
* - the file normalizer (server/file-fields.ts) — resolves a part's `{name}` pointer
|
|
5387
|
-
* - `collectMutateSources` / `collectComputeUnits` (sdk/mutate-graph.ts) — the trigger graph
|
|
5388
|
-
*
|
|
5389
|
-
* A part BELOW that level carrying any of those would own a real column that nothing ever
|
|
5390
|
-
* writes, refreshes, validates or triggers — the exact silent failure a storage change must
|
|
5391
|
-
* not ship. So it is refused where it is DECLARED, naming the path, instead of failing
|
|
5392
|
-
* quietly at runtime. Lifting this is a compute-engine change, not a storage one: nothing
|
|
5393
|
-
* about the columns needs to move for it.
|
|
5394
|
-
*/
|
|
5395
5038
|
function assertInertNesting(meta, path) {
|
|
5396
5039
|
for (const p of meta.parts ?? []) {
|
|
5397
5040
|
const where = `${path}.${p.key}`;
|
|
@@ -5405,29 +5048,6 @@ function assertInertNesting(meta, path) {
|
|
|
5405
5048
|
if (p.meta.parts) assertInertNesting(p.meta, where);
|
|
5406
5049
|
}
|
|
5407
5050
|
}
|
|
5408
|
-
/**
|
|
5409
|
-
* Storage columns of a composite: sub-PATH → column type. Every part contributes its own
|
|
5410
|
-
* columns, whatever they are.
|
|
5411
|
-
*
|
|
5412
|
-
* A part is a NODE, not necessarily a leaf: a filling that is itself a composite carries
|
|
5413
|
-
* its own `columns`, and those are what it contributes — under its role name as a prefix,
|
|
5414
|
-
* joined by the same `__` the physical column name uses (`at` filled by `f.geo` →
|
|
5415
|
-
* `at__lat`/`at__lng`/`at__label`). A part with no `columns` is a scalar and contributes
|
|
5416
|
-
* exactly one entry, its own `column`, under its role name. That is where the walk STOPS,
|
|
5417
|
-
* and every walk stops there: a scalar is the only thing a composite can bottom out in.
|
|
5418
|
-
*
|
|
5419
|
-
* The loop is one level deep because the result is built INDUCTIVELY, not by re-walking a
|
|
5420
|
-
* tree. A filling is materialized before the composite that holds it reads it, so
|
|
5421
|
-
* `p.meta.columns` was already produced by this same function and already carries the FULL
|
|
5422
|
-
* sub-path of everything below it. Depth therefore costs nothing here and is bounded by
|
|
5423
|
-
* nothing but the column NAME the joined path produces — see `storageColumns` (shelf.ts)
|
|
5424
|
-
* for the join, and `MAX_COLUMN_NAME` (compose.ts) for the budget that name must fit.
|
|
5425
|
-
*
|
|
5426
|
-
* The result is the FLAT storage projection of the composite. The STRUCTURE lives in
|
|
5427
|
-
* `FieldMeta.parts`, which is what the value↔columns walks (`nestComposite`/
|
|
5428
|
-
* `flattenComposite` in shelf.ts) read: they need to know where one nested object ends and
|
|
5429
|
-
* the next begins, and a joined key cannot say that on its own.
|
|
5430
|
-
*/
|
|
5431
5051
|
function partColumns(parts) {
|
|
5432
5052
|
const cols = {};
|
|
5433
5053
|
for (const p of parts) {
|
|
@@ -5437,63 +5057,6 @@ function partColumns(parts) {
|
|
|
5437
5057
|
}
|
|
5438
5058
|
return cols;
|
|
5439
5059
|
}
|
|
5440
|
-
/**
|
|
5441
|
-
* A composite's write-time row shape, DERIVED from its resolved parts. `shape` carries
|
|
5442
|
-
* the composite's DEFAULT schema per role — the literal the factory writes by hand
|
|
5443
|
-
* (`{ value: numSchema, unit: z.string().refine(…) }`) — and each part decides which
|
|
5444
|
-
* schema guards its (role-named) key:
|
|
5445
|
-
*
|
|
5446
|
-
* - a plain STORED part is keyed by its role name, matching the value object and the
|
|
5447
|
-
* column `<field>__<role>` that `flattenEmbeddedAt`/`nestEmbeddedAt` read. A role left
|
|
5448
|
-
* to its default filling keeps the default schema — byte-identical to the hand-written
|
|
5449
|
-
* literal, so every existing structure/unit/range code and message is preserved.
|
|
5450
|
-
* - an OVERRIDDEN stored part is validated by its OWN zod (`meta.zod`) instead of the
|
|
5451
|
-
* default schema for that role. This is what makes a RETYPED filling real: an
|
|
5452
|
-
* `f.int({})` magnitude rejects 5.5 (its own `int` check) and gets an integer column,
|
|
5453
|
-
* and an `f.relation({…})` currency accepts a record id instead of being measured
|
|
5454
|
-
* against an ISO-4217 string rule it was never meant to satisfy. The filling also
|
|
5455
|
-
* brings its own optionality and its own min/max — an author replacing a filling
|
|
5456
|
-
* takes over that role's constraints, so `f.real({ rules: { min: 0 } })` is how a
|
|
5457
|
-
* replaced magnitude keeps a bound.
|
|
5458
|
-
* - a COMPUTED-AND-STORED part ('computedStored') is keyed by its role name like any
|
|
5459
|
-
* other part, but its schema is `z.unknown().optional()`: the SERVER owns the value, so
|
|
5460
|
-
* whatever the client sends there is ignored — `stripServerOwnedParts` deletes it a
|
|
5461
|
-
* moment later and the write path recomputes the column. It is stripped rather than
|
|
5462
|
-
* `z.never()`-rejected because a read hands the client that key (it is a real column,
|
|
5463
|
-
* present in every response), so rejecting it would break a read → PATCH-the-whole-object
|
|
5464
|
-
* round trip.
|
|
5465
|
-
* - a CLIENT-OWNED stored part of a composite also accepts
|
|
5466
|
-
* an explicit `null` (`tolerateNull`), which is what closes the read → write round trip
|
|
5467
|
-
* for a PARTIALLY FILLED composite. A SELECT returns every sub-column, so a record whose
|
|
5468
|
-
* magnitude column is set and whose unit column is empty reads back as
|
|
5469
|
-
* `{value: 900, unit: null}` (`nestEmbeddedAt` drops the key only when EVERY column is
|
|
5470
|
-
* empty — `0` is a value), and feeding that exact object back used to fail with
|
|
5471
|
-
* `measured_structure`: the write path refused the shape its own read produced. The null
|
|
5472
|
-
* slot now parses through and is written back as NULL, so the round trip is closed and
|
|
5473
|
-
* clearing ONE role of a filled composite works instead of erroring.
|
|
5474
|
-
* NULL, not absence: a read emits every stored role (empty ones as `null`), so `null` is
|
|
5475
|
-
* how "this record has no value there" arrives, while an ABSENT key is a writer that
|
|
5476
|
-
* never mentioned a role it owns — which stays the structure error it has always been
|
|
5477
|
-
* (`{unit: 'kg'}` with no magnitude, `{lat: 50}` with no longitude). That is the
|
|
5478
|
-
* all-or-nothing rule for a DECLARATION, and it is untouched.
|
|
5479
|
-
* A composite used to be able to override this: `required: true` kept every client-owned
|
|
5480
|
-
* slot MANDATORY, null included, so that "tolerate null" could not turn a demanded field
|
|
5481
|
-
* optional. Nothing demands a value now, so there is no override and the rule above is the
|
|
5482
|
-
* only one.
|
|
5483
|
-
*/
|
|
5484
|
-
/**
|
|
5485
|
-
* How much a CLIENT-OWNED slot forgives in the row shape: exactly one thing, an explicit
|
|
5486
|
-
* `null`. `.nullable()`, never `.nullish()` — a part that is MISSING entirely is still a
|
|
5487
|
-
* malformed row (`{lat: 5.5}` is half a coordinate), which is the one rule about composites
|
|
5488
|
-
* that judges CORRECTNESS rather than presence.
|
|
5489
|
-
*
|
|
5490
|
-
* There used to be a `RowTolerance` of three levels, and the field's `required` is what chose
|
|
5491
|
-
* between them: 'strict' when the field demanded a value, 'null-or-missing' when the value was
|
|
5492
|
-
* ABSENT under that demand — the third existing only so an absent required composite did not
|
|
5493
|
-
* report the same absence twice, once as `required` and once as a structure code. With nothing
|
|
5494
|
-
* able to demand a value there is no first issue to restate, so both levels and the type
|
|
5495
|
-
* itself are gone.
|
|
5496
|
-
*/
|
|
5497
5060
|
function partsRowShape(shape, parts) {
|
|
5498
5061
|
const out = { ...shape };
|
|
5499
5062
|
for (const p of parts) if (p.mode === "computedStored") out[p.key] = unknown().optional();
|
|
@@ -5503,16 +5066,6 @@ function partsRowShape(shape, parts) {
|
|
|
5503
5066
|
}
|
|
5504
5067
|
return out;
|
|
5505
5068
|
}
|
|
5506
|
-
/**
|
|
5507
|
-
* Drops SERVER-OWNED part slots from a successfully parsed composite row: a computed-and-stored
|
|
5508
|
-
* role, keyed by its role name. Its value belongs to the schema, not to the writer — it is
|
|
5509
|
-
* recomputed and written by the server on the same write (`applyStoredParts`) — so a client
|
|
5510
|
-
* sending one back is stripped rather than rejected: the schema wins either way.
|
|
5511
|
-
*
|
|
5512
|
-
* Stripping (not `z.never()`-rejecting, as a computed-and-stored top-level FIELD does) is
|
|
5513
|
-
* what keeps a read → PATCH-the-whole-object round trip working: a read hands the client that
|
|
5514
|
-
* slot straight out of its column, so rejecting the echo would reject every full-object PATCH.
|
|
5515
|
-
*/
|
|
5516
5069
|
function stripServerOwnedParts(value, parts) {
|
|
5517
5070
|
let out = value;
|
|
5518
5071
|
for (const p of parts) if (p.mode === "computedStored" && p.key in out) {
|
|
@@ -5522,28 +5075,7 @@ function stripServerOwnedParts(value, parts) {
|
|
|
5522
5075
|
return out;
|
|
5523
5076
|
}
|
|
5524
5077
|
//#endregion
|
|
5525
|
-
//#region ../sdk/
|
|
5526
|
-
/**
|
|
5527
|
-
* Field presets are thin wrappers around the primitives in fields.ts.
|
|
5528
|
-
*
|
|
5529
|
-
* Each preset = one kind (one widget). Presets do not accept `format`;
|
|
5530
|
-
* they are semantic types themselves. min/max/step go through `config`.
|
|
5531
|
-
*
|
|
5532
|
-
* Presets are added to `f` through `composeF`, which guarantees no preset
|
|
5533
|
-
* overrides a primitive.
|
|
5534
|
-
*
|
|
5535
|
-
* The cyclic import from fields.ts is safe: factories/helpers are hoisted declarations,
|
|
5536
|
-
* and presets call them only inside their function bodies.
|
|
5537
|
-
*/
|
|
5538
|
-
/**
|
|
5539
|
-
* Email — kind 'email', email validation, inputType 'email'.
|
|
5540
|
-
*
|
|
5541
|
-
* @layer preset
|
|
5542
|
-
* @base string
|
|
5543
|
-
* @prim text
|
|
5544
|
-
* @widget email
|
|
5545
|
-
* @example f.email({ label: 'mod.fields.contact' })
|
|
5546
|
-
*/
|
|
5078
|
+
//#region ../sdk/dist/field-presets.js
|
|
5547
5079
|
function email(o) {
|
|
5548
5080
|
return declare("email", o);
|
|
5549
5081
|
}
|
|
@@ -5561,15 +5093,6 @@ registerPreset("email", "string", {}, {
|
|
|
5561
5093
|
};
|
|
5562
5094
|
}
|
|
5563
5095
|
});
|
|
5564
|
-
/**
|
|
5565
|
-
* Phone — kind 'tel', loose format /^\+?[\d\s()-]{4,}$/, inputType 'tel'.
|
|
5566
|
-
*
|
|
5567
|
-
* @layer preset
|
|
5568
|
-
* @base string
|
|
5569
|
-
* @prim text
|
|
5570
|
-
* @widget tel
|
|
5571
|
-
* @example f.tel({ label: 'mod.fields.phone' })
|
|
5572
|
-
*/
|
|
5573
5096
|
function tel(o) {
|
|
5574
5097
|
return declare("tel", o);
|
|
5575
5098
|
}
|
|
@@ -5587,15 +5110,6 @@ registerPreset("tel", "string", {}, {
|
|
|
5587
5110
|
};
|
|
5588
5111
|
}
|
|
5589
5112
|
});
|
|
5590
|
-
/**
|
|
5591
|
-
* Password — kind 'password', inputType 'password'. No strict schema. No multiple.
|
|
5592
|
-
*
|
|
5593
|
-
* @layer preset
|
|
5594
|
-
* @base string
|
|
5595
|
-
* @prim text
|
|
5596
|
-
* @widget password
|
|
5597
|
-
* @example f.password({ label: 'mod.fields.password' })
|
|
5598
|
-
*/
|
|
5599
5113
|
function password(o) {
|
|
5600
5114
|
return declare("password", o);
|
|
5601
5115
|
}
|
|
@@ -5614,20 +5128,6 @@ registerPreset("password", "string", {}, {
|
|
|
5614
5128
|
};
|
|
5615
5129
|
}
|
|
5616
5130
|
});
|
|
5617
|
-
/**
|
|
5618
|
-
* Internal API-token list — keyValue-shaped collection (name + write-only
|
|
5619
|
-
* token + timestamps), custom renderer (`kind:'internalApiToken'`, own
|
|
5620
|
-
* create/revoke UI — same trick as url()/perWeekday()). "Internal" = not a
|
|
5621
|
-
* general-purpose field type for plugin shelves, only for the core account
|
|
5622
|
-
* settings page. `token` is `kind:'password'` so maskSecrets/preserveTree
|
|
5623
|
-
* already mask/preserve it for free.
|
|
5624
|
-
*
|
|
5625
|
-
* @layer preset
|
|
5626
|
-
* @base group
|
|
5627
|
-
* @prim —
|
|
5628
|
-
* @widget internalApiToken
|
|
5629
|
-
* @example f.internalApiToken({ label: 'mod.fields.apiTokens' })
|
|
5630
|
-
*/
|
|
5631
5131
|
function internalApiToken(o) {
|
|
5632
5132
|
return group({
|
|
5633
5133
|
scope: "nest",
|
|
@@ -5643,18 +5143,6 @@ function internalApiToken(o) {
|
|
|
5643
5143
|
}
|
|
5644
5144
|
});
|
|
5645
5145
|
}
|
|
5646
|
-
/**
|
|
5647
|
-
* Internal "connected apps" list — the OAuth clients (claude.ai & co) the user
|
|
5648
|
-
* has authorized for MCP access. Same "internal" caveat and custom-renderer
|
|
5649
|
-
* trick as internalApiToken(): read-only rows + a revoke action, never part of
|
|
5650
|
-
* the parent form's save.
|
|
5651
|
-
*
|
|
5652
|
-
* @layer preset
|
|
5653
|
-
* @base group
|
|
5654
|
-
* @prim —
|
|
5655
|
-
* @widget internalOauthGrants
|
|
5656
|
-
* @example f.internalOauthGrants({ label: 'mod.fields.oauthGrants' })
|
|
5657
|
-
*/
|
|
5658
5146
|
function internalOauthGrants(o) {
|
|
5659
5147
|
return group({
|
|
5660
5148
|
scope: "nest",
|
|
@@ -5668,18 +5156,6 @@ function internalOauthGrants(o) {
|
|
|
5668
5156
|
}
|
|
5669
5157
|
});
|
|
5670
5158
|
}
|
|
5671
|
-
/**
|
|
5672
|
-
* The account page's linked-accounts section. Same trick as `internalApiToken`: a container
|
|
5673
|
-
* with a custom `ui.kind`, drawn by its own renderer, which reads `/api/auth/links` itself.
|
|
5674
|
-
* The declared children are what one ROW holds — the renderer reads them through the slot
|
|
5675
|
-
* contract, so this is a declaration, not a layout.
|
|
5676
|
-
*
|
|
5677
|
-
* @layer preset
|
|
5678
|
-
* @base group
|
|
5679
|
-
* @prim —
|
|
5680
|
-
* @widget internalIdentityLinks
|
|
5681
|
-
* @example f.internalIdentityLinks({ label: 'auth.linksTitle' })
|
|
5682
|
-
*/
|
|
5683
5159
|
function internalIdentityLinks(o) {
|
|
5684
5160
|
return group({
|
|
5685
5161
|
scope: "nest",
|
|
@@ -5694,15 +5170,6 @@ function internalIdentityLinks(o) {
|
|
|
5694
5170
|
});
|
|
5695
5171
|
}
|
|
5696
5172
|
var SLUG_RE = /^[a-z0-9-]+$/;
|
|
5697
|
-
/**
|
|
5698
|
-
* Slug — kind 'slug', `^[a-z0-9-]+$`.
|
|
5699
|
-
*
|
|
5700
|
-
* @layer preset
|
|
5701
|
-
* @base string
|
|
5702
|
-
* @prim text
|
|
5703
|
-
* @widget slug
|
|
5704
|
-
* @example f.slug({ label: 'mod.fields.slug' })
|
|
5705
|
-
*/
|
|
5706
5173
|
function slug(o) {
|
|
5707
5174
|
return declare("slug", o);
|
|
5708
5175
|
}
|
|
@@ -5717,21 +5184,6 @@ registerPreset("slug", "string", {}, {
|
|
|
5717
5184
|
};
|
|
5718
5185
|
}
|
|
5719
5186
|
});
|
|
5720
|
-
/**
|
|
5721
|
-
* Identifier — a code printed for a machine and read back by one: a serial number, an order
|
|
5722
|
-
* number, a policy number, an IMEI. What a person actually DOES with one is copy it, which is
|
|
5723
|
-
* why this is a type of its own and not `f.string({ ui: { voice: 'data' } })`: the machine face
|
|
5724
|
-
* is one word on any string now, but the click that copies belongs to the kind.
|
|
5725
|
-
*
|
|
5726
|
-
* Deliberately unvalidated beyond a length: an identifier's format belongs to whoever issued
|
|
5727
|
-
* it, and a pattern here would reject the next manufacturer's.
|
|
5728
|
-
*
|
|
5729
|
-
* @layer preset
|
|
5730
|
-
* @base string
|
|
5731
|
-
* @prim text
|
|
5732
|
-
* @widget identifier
|
|
5733
|
-
* @example f.identifier({ label: 'mod.fields.serialNumber' })
|
|
5734
|
-
*/
|
|
5735
5187
|
function identifier(o) {
|
|
5736
5188
|
return declare("identifier", o);
|
|
5737
5189
|
}
|
|
@@ -5746,20 +5198,6 @@ registerPreset("identifier", "string", {}, {
|
|
|
5746
5198
|
};
|
|
5747
5199
|
}
|
|
5748
5200
|
});
|
|
5749
|
-
/**
|
|
5750
|
-
* The machine-readable zone of a travel document — the identifier taken to its limit: printed
|
|
5751
|
-
* for a scanner, not for a person. Fixed pitch, chevrons kept as the filler they are, and the
|
|
5752
|
-
* line breaks preserved, because an MRZ's line structure is part of what it encodes.
|
|
5753
|
-
*
|
|
5754
|
-
* Unvalidated on purpose, like `identifier`: the ICAO line formats differ by document type, and
|
|
5755
|
-
* a strip transcribed from a real document is worth storing even when it does not check out.
|
|
5756
|
-
*
|
|
5757
|
-
* @layer preset
|
|
5758
|
-
* @base string
|
|
5759
|
-
* @prim text
|
|
5760
|
-
* @widget mrz
|
|
5761
|
-
* @example f.mrz({ label: 'documents.personal_document.fields.mrz' })
|
|
5762
|
-
*/
|
|
5763
5201
|
function mrz(o) {
|
|
5764
5202
|
return declare("mrz", o);
|
|
5765
5203
|
}
|
|
@@ -5775,15 +5213,6 @@ registerPreset("mrz", "string", {}, {
|
|
|
5775
5213
|
}
|
|
5776
5214
|
});
|
|
5777
5215
|
var COLOR_RE = /^#[0-9a-fA-F]{6}$/;
|
|
5778
|
-
/**
|
|
5779
|
-
* Hex color — kind 'color', `#rrggbb` + swatch widget.
|
|
5780
|
-
*
|
|
5781
|
-
* @layer preset
|
|
5782
|
-
* @base string
|
|
5783
|
-
* @prim text
|
|
5784
|
-
* @widget color
|
|
5785
|
-
* @example f.color({ label: 'mod.fields.color' })
|
|
5786
|
-
*/
|
|
5787
5216
|
function color(o) {
|
|
5788
5217
|
return declare("color", o);
|
|
5789
5218
|
}
|
|
@@ -5798,15 +5227,6 @@ registerPreset("color", "string", {}, {
|
|
|
5798
5227
|
};
|
|
5799
5228
|
}
|
|
5800
5229
|
});
|
|
5801
|
-
/**
|
|
5802
|
-
* CSS color name — kind 'colorname'.
|
|
5803
|
-
*
|
|
5804
|
-
* @layer preset
|
|
5805
|
-
* @base string
|
|
5806
|
-
* @prim text
|
|
5807
|
-
* @widget colorname
|
|
5808
|
-
* @example f.colorname({ label: 'mod.fields.colorName' })
|
|
5809
|
-
*/
|
|
5810
5230
|
function colorname(o) {
|
|
5811
5231
|
return declare("colorname", o);
|
|
5812
5232
|
}
|
|
@@ -5821,15 +5241,6 @@ registerPreset("colorname", "string", {}, {
|
|
|
5821
5241
|
};
|
|
5822
5242
|
}
|
|
5823
5243
|
});
|
|
5824
|
-
/**
|
|
5825
|
-
* Shelf heading — kind 'title'. Heading renderer.
|
|
5826
|
-
*
|
|
5827
|
-
* @layer preset
|
|
5828
|
-
* @base string
|
|
5829
|
-
* @prim text
|
|
5830
|
-
* @widget title
|
|
5831
|
-
* @example f.title({ label: 'mod.fields.name' })
|
|
5832
|
-
*/
|
|
5833
5244
|
function title(o) {
|
|
5834
5245
|
return declare("title", o);
|
|
5835
5246
|
}
|
|
@@ -5844,16 +5255,6 @@ registerPreset("title", "string", {}, {
|
|
|
5844
5255
|
};
|
|
5845
5256
|
}
|
|
5846
5257
|
});
|
|
5847
|
-
/**
|
|
5848
|
-
* Link — kind 'link', edited as string, view = clickable <a target=_blank>.
|
|
5849
|
-
* Stores the raw URL as text (unlike composite f.url). Loose validation.
|
|
5850
|
-
*
|
|
5851
|
-
* @layer preset
|
|
5852
|
-
* @base string
|
|
5853
|
-
* @prim text
|
|
5854
|
-
* @widget link
|
|
5855
|
-
* @example f.link({ label: 'mod.fields.link' })
|
|
5856
|
-
*/
|
|
5857
5258
|
function link(o) {
|
|
5858
5259
|
return declare("link", o);
|
|
5859
5260
|
}
|
|
@@ -5872,9 +5273,7 @@ registerPreset("link", "string", {}, {
|
|
|
5872
5273
|
}
|
|
5873
5274
|
});
|
|
5874
5275
|
var TEL_RE = /^\+?[\d\s()-]{4,}$/;
|
|
5875
|
-
/** Loose URL: any scheme:// OR dotted host (optional port/path). No spaces. */
|
|
5876
5276
|
var LINK_RE = /^([a-z][a-z0-9+.-]*:\/\/\S+|[\w-]+(\.[\w-]+)+(:\d+)?(\/\S*)?)$/i;
|
|
5877
|
-
/** CSS named colors (CSS Color Module L4) for f.colorname. */
|
|
5878
5277
|
var CSS_COLOR_NAMES = /* @__PURE__ */ new Set([
|
|
5879
5278
|
"aliceblue",
|
|
5880
5279
|
"antiquewhite",
|
|
@@ -6026,15 +5425,6 @@ var CSS_COLOR_NAMES = /* @__PURE__ */ new Set([
|
|
|
6026
5425
|
"yellowgreen",
|
|
6027
5426
|
"transparent"
|
|
6028
5427
|
]);
|
|
6029
|
-
/**
|
|
6030
|
-
* Tag — non-empty string + chip widget. `multiple` → array of tags (`tags`).
|
|
6031
|
-
*
|
|
6032
|
-
* @layer preset
|
|
6033
|
-
* @base string
|
|
6034
|
-
* @prim text
|
|
6035
|
-
* @widget tag
|
|
6036
|
-
* @example f.tag({ label: 'mod.fields.tag' })
|
|
6037
|
-
*/
|
|
6038
5428
|
function tag(o) {
|
|
6039
5429
|
return declare("tag", o);
|
|
6040
5430
|
}
|
|
@@ -6055,21 +5445,6 @@ registerPreset("tag", "string", {}, {
|
|
|
6055
5445
|
};
|
|
6056
5446
|
}
|
|
6057
5447
|
});
|
|
6058
|
-
/**
|
|
6059
|
-
* A location written the way it is said aloud: workshop › shelf 2 › box. A `string` fixed to
|
|
6060
|
-
* `multiple`, exactly as `f.tags` is, because a place inside a place inside a place IS a
|
|
6061
|
-
* sequence — the plurality is the type, not a modifier someone remembered to add.
|
|
6062
|
-
*
|
|
6063
|
-
* Distinct from `f.tags`, which stores the same shape: tags are an unordered SET, and a path is
|
|
6064
|
-
* an ordered CHAIN where each step is inside the one before it. Drawn as chips, that
|
|
6065
|
-
* containment — the only thing the value actually carries — is lost.
|
|
6066
|
-
*
|
|
6067
|
-
* @layer preset
|
|
6068
|
-
* @base string
|
|
6069
|
-
* @prim text
|
|
6070
|
-
* @widget path
|
|
6071
|
-
* @example f.path({ label: 'things.storage_location.fields.path' })
|
|
6072
|
-
*/
|
|
6073
5448
|
function path(o) {
|
|
6074
5449
|
return declare("path", {
|
|
6075
5450
|
...o,
|
|
@@ -6087,30 +5462,12 @@ registerPreset("path", "string", {}, {
|
|
|
6087
5462
|
};
|
|
6088
5463
|
}
|
|
6089
5464
|
});
|
|
6090
|
-
/**
|
|
6091
|
-
* Tags — `tag({ multiple: true })`.
|
|
6092
|
-
*
|
|
6093
|
-
* @layer preset
|
|
6094
|
-
* @base string
|
|
6095
|
-
* @prim text
|
|
6096
|
-
* @widget tag
|
|
6097
|
-
* @example f.tags({ label: 'mod.fields.tags' })
|
|
6098
|
-
*/
|
|
6099
5465
|
function tags(o) {
|
|
6100
5466
|
return tag({
|
|
6101
5467
|
...o,
|
|
6102
5468
|
multiple: true
|
|
6103
5469
|
});
|
|
6104
5470
|
}
|
|
6105
|
-
/**
|
|
6106
|
-
* Markdown — text() + Markdown editor.
|
|
6107
|
-
*
|
|
6108
|
-
* @layer preset
|
|
6109
|
-
* @base text
|
|
6110
|
-
* @prim text
|
|
6111
|
-
* @widget markdown
|
|
6112
|
-
* @example f.markdown({ label: 'mod.fields.body' })
|
|
6113
|
-
*/
|
|
6114
5471
|
function markdown(o) {
|
|
6115
5472
|
return declare("markdown", o);
|
|
6116
5473
|
}
|
|
@@ -6118,16 +5475,6 @@ registerPreset("markdown", "text", {}, {
|
|
|
6118
5475
|
kind: "markdown",
|
|
6119
5476
|
widget: "markdown"
|
|
6120
5477
|
});
|
|
6121
|
-
/**
|
|
6122
|
-
* Code snippet — text() + monospace code editor.
|
|
6123
|
-
* `rules.language` is a highlighting hint (json/yaml/bash/ini…), stored in hints.
|
|
6124
|
-
*
|
|
6125
|
-
* @layer preset
|
|
6126
|
-
* @base text
|
|
6127
|
-
* @prim text
|
|
6128
|
-
* @widget snippet
|
|
6129
|
-
* @example f.snippet({ label: 'mod.fields.config' })
|
|
6130
|
-
*/
|
|
6131
5478
|
function snippet(o) {
|
|
6132
5479
|
return declare("snippet", o);
|
|
6133
5480
|
}
|
|
@@ -6146,22 +5493,9 @@ registerPreset("snippet", "text", {}, {
|
|
|
6146
5493
|
};
|
|
6147
5494
|
}
|
|
6148
5495
|
});
|
|
6149
|
-
/** A NUL byte, or U+FFFD left behind by decoding non-UTF-8 bytes as text. */
|
|
6150
5496
|
function isBinaryText(v) {
|
|
6151
5497
|
return v.includes("\0") || v.includes("�");
|
|
6152
5498
|
}
|
|
6153
|
-
/**
|
|
6154
|
-
* Source — plain text in one column, shaped like a file in the UI.
|
|
6155
|
-
* Written through the API or by uploading a file; never hand-edited in the web form
|
|
6156
|
-
* (hints.noEditControl). The content is never rendered — only name + Upload/Download.
|
|
6157
|
-
* `rules.ext` is the displayed/downloaded extension, `rules.maxBytes` the size cap.
|
|
6158
|
-
*
|
|
6159
|
-
* @layer preset
|
|
6160
|
-
* @base text
|
|
6161
|
-
* @prim text
|
|
6162
|
-
* @widget source
|
|
6163
|
-
* @example f.source({ label: 'mod.fields.file' })
|
|
6164
|
-
*/
|
|
6165
5499
|
function source(o) {
|
|
6166
5500
|
return declare("source", o);
|
|
6167
5501
|
}
|
|
@@ -6196,15 +5530,6 @@ registerPreset("source", "text", {}, {
|
|
|
6196
5530
|
};
|
|
6197
5531
|
}
|
|
6198
5532
|
});
|
|
6199
|
-
/**
|
|
6200
|
-
* Rating 1..N — integer + star widget. rules.max is the upper bound (default 5).
|
|
6201
|
-
*
|
|
6202
|
-
* @layer preset
|
|
6203
|
-
* @base int
|
|
6204
|
-
* @prim number
|
|
6205
|
-
* @widget rating
|
|
6206
|
-
* @example f.rating({ label: 'mod.fields.rating' })
|
|
6207
|
-
*/
|
|
6208
5533
|
function rating(o) {
|
|
6209
5534
|
return declare("rating", o);
|
|
6210
5535
|
}
|
|
@@ -6220,16 +5545,6 @@ registerPreset("rating", "int", { config: { max: 5 } }, {
|
|
|
6220
5545
|
};
|
|
6221
5546
|
}
|
|
6222
5547
|
});
|
|
6223
|
-
/** Duration in seconds — integer + duration widget ("2d 3h 30m 15s").
|
|
6224
|
-
* rules.max is the maximum in seconds (default 604800 = 1 week).
|
|
6225
|
-
* rules.step is the step in seconds (default 60 = 1 minute).
|
|
6226
|
-
*
|
|
6227
|
-
* @layer preset
|
|
6228
|
-
* @base int
|
|
6229
|
-
* @prim number
|
|
6230
|
-
* @widget duration
|
|
6231
|
-
* @example f.duration({ label: 'mod.fields.runtime' })
|
|
6232
|
-
*/
|
|
6233
5548
|
function duration(o) {
|
|
6234
5549
|
return declare("duration", o);
|
|
6235
5550
|
}
|
|
@@ -6253,15 +5568,6 @@ registerPreset("duration", "int", { config: {
|
|
|
6253
5568
|
};
|
|
6254
5569
|
}
|
|
6255
5570
|
});
|
|
6256
|
-
/** Reminder — date with visual urgency. kind 'reminder', prim 'date'.
|
|
6257
|
-
* rules.lead = day threshold (default 30). rules.min/rules.max = ISO dates.
|
|
6258
|
-
*
|
|
6259
|
-
* @layer preset
|
|
6260
|
-
* @base date
|
|
6261
|
-
* @prim date
|
|
6262
|
-
* @widget reminder
|
|
6263
|
-
* @example f.reminder({ label: 'mod.fields.renewal' })
|
|
6264
|
-
*/
|
|
6265
5571
|
function reminder(o) {
|
|
6266
5572
|
return declare("reminder", o);
|
|
6267
5573
|
}
|
|
@@ -6280,24 +5586,6 @@ registerPreset("reminder", "date", { lead: 30 }, {
|
|
|
6280
5586
|
};
|
|
6281
5587
|
}
|
|
6282
5588
|
});
|
|
6283
|
-
/** Timetable — a set of times of day, read as a schedule rather than as a bag of values.
|
|
6284
|
-
* kind 'timetable', prim 'time', always `multiple`.
|
|
6285
|
-
*
|
|
6286
|
-
* Same shape as `f.reminder`/`f.age` over `date`: a semantic type whose renderer knows what
|
|
6287
|
-
* the values MEAN. Everything it shows beyond the times themselves — when the service starts
|
|
6288
|
-
* and ends, how many runs there are, which one is next — is derived from the values at render
|
|
6289
|
-
* time, never stored: a "next departure" is wrong the moment it is written down, exactly the
|
|
6290
|
-
* argument `f.age` already makes for an age.
|
|
6291
|
-
*
|
|
6292
|
-
* Storage is a plain multiple `time`, so a field can be switched to this from
|
|
6293
|
-
* `f.time({ multiple: true })` and back with no migration.
|
|
6294
|
-
*
|
|
6295
|
-
* @layer preset
|
|
6296
|
-
* @base time
|
|
6297
|
-
* @prim time
|
|
6298
|
-
* @widget timetable
|
|
6299
|
-
* @example f.timetable({ label: 'mod.fields.weekdays' })
|
|
6300
|
-
*/
|
|
6301
5589
|
function timetable(o) {
|
|
6302
5590
|
return declare("timetable", {
|
|
6303
5591
|
...o,
|
|
@@ -6308,18 +5596,6 @@ registerPreset("timetable", "time", {}, {
|
|
|
6308
5596
|
kind: "timetable",
|
|
6309
5597
|
widget: "timetable"
|
|
6310
5598
|
});
|
|
6311
|
-
/** Age — date whose whole-year age (as of today) renders alongside it, e.g. a birth date.
|
|
6312
|
-
* kind 'age', prim 'date'. The age itself is computed at RENDER time, never stored: unlike a
|
|
6313
|
-
* `mutate`-driven computed field, it must be right on every day that passes, not only the day
|
|
6314
|
-
* the record was last written, so this is display metadata over `date`, the same shape as
|
|
6315
|
-
* `f.reminder`'s own relative-to-today note.
|
|
6316
|
-
*
|
|
6317
|
-
* @layer preset
|
|
6318
|
-
* @base date
|
|
6319
|
-
* @prim date
|
|
6320
|
-
* @widget age
|
|
6321
|
-
* @example f.age({ label: 'mod.fields.birthDate' })
|
|
6322
|
-
*/
|
|
6323
5599
|
function age(o) {
|
|
6324
5600
|
return declare("age", o);
|
|
6325
5601
|
}
|
|
@@ -6330,15 +5606,6 @@ registerPreset("age", "date", {}, {
|
|
|
6330
5606
|
return typeOf("date").build(o, parts);
|
|
6331
5607
|
}
|
|
6332
5608
|
});
|
|
6333
|
-
/**
|
|
6334
|
-
* Percentage 0..100 — real with rules:{min:0,max:100}.
|
|
6335
|
-
*
|
|
6336
|
-
* @layer preset
|
|
6337
|
-
* @base real
|
|
6338
|
-
* @prim number
|
|
6339
|
-
* @widget number
|
|
6340
|
-
* @example f.percent({ label: 'mod.fields.progress' })
|
|
6341
|
-
*/
|
|
6342
5609
|
function percent(o) {
|
|
6343
5610
|
return real({
|
|
6344
5611
|
...o,
|
|
@@ -6349,15 +5616,6 @@ function percent(o) {
|
|
|
6349
5616
|
}
|
|
6350
5617
|
});
|
|
6351
5618
|
}
|
|
6352
|
-
/**
|
|
6353
|
-
* Year — int with rules:{min:1900,max:2100}; bounds can be overridden via rules.min/max.
|
|
6354
|
-
*
|
|
6355
|
-
* @layer preset
|
|
6356
|
-
* @base int
|
|
6357
|
-
* @prim number
|
|
6358
|
-
* @widget number
|
|
6359
|
-
* @example f.year({ label: 'mod.fields.releaseYear' })
|
|
6360
|
-
*/
|
|
6361
5619
|
function year(o) {
|
|
6362
5620
|
return int({
|
|
6363
5621
|
...o,
|
|
@@ -6368,15 +5626,6 @@ function year(o) {
|
|
|
6368
5626
|
}
|
|
6369
5627
|
});
|
|
6370
5628
|
}
|
|
6371
|
-
/**
|
|
6372
|
-
* Weight — always grams (int). kind 'weight', prim 'number'.
|
|
6373
|
-
*
|
|
6374
|
-
* @layer preset
|
|
6375
|
-
* @base int
|
|
6376
|
-
* @prim number
|
|
6377
|
-
* @widget weight
|
|
6378
|
-
* @example f.weight({ label: 'mod.fields.weight' })
|
|
6379
|
-
*/
|
|
6380
5629
|
function weight(o) {
|
|
6381
5630
|
return declare("weight", o);
|
|
6382
5631
|
}
|
|
@@ -6394,12 +5643,6 @@ registerPreset("weight", "int", { config: { min: 1 } }, {
|
|
|
6394
5643
|
};
|
|
6395
5644
|
}
|
|
6396
5645
|
});
|
|
6397
|
-
/**
|
|
6398
|
-
* The three extents of a box, in declaration order — which is also the column order, so
|
|
6399
|
-
* `w` is the magnitude `magnitudeSub()` hands to list sorting and to a filter by field key.
|
|
6400
|
-
* The unit is NOT a role: dimensions are always millimetres, and nothing in the app converts
|
|
6401
|
-
* units (see `docs/decisions.md`, 2026-08-05), so it stays a hint rather than a fourth column.
|
|
6402
|
-
*/
|
|
6403
5646
|
var DIMENSION_ROLES = () => [
|
|
6404
5647
|
role("w", ["number"], "int"),
|
|
6405
5648
|
role("h", ["number"], "int"),
|
|
@@ -6423,27 +5666,9 @@ registerType("dimensions", {
|
|
|
6423
5666
|
structureCode: "dimensions_structure",
|
|
6424
5667
|
hints: () => ({ unit: "mm" })
|
|
6425
5668
|
});
|
|
6426
|
-
/**
|
|
6427
|
-
* Dimensions — box dimensions {w,h,d}, int mm across the real sub-columns `key__w|h|d`.
|
|
6428
|
-
*
|
|
6429
|
-
* @layer preset
|
|
6430
|
-
* @base int
|
|
6431
|
-
* @prim text
|
|
6432
|
-
* @widget dimensions
|
|
6433
|
-
* @example f.dimensions({ label: 'mod.fields.size' })
|
|
6434
|
-
*/
|
|
6435
5669
|
function dimensions(o) {
|
|
6436
5670
|
return declare("dimensions", o);
|
|
6437
5671
|
}
|
|
6438
|
-
/**
|
|
6439
|
-
* Country — a string whose values are the country list, rendered with flags.
|
|
6440
|
-
*
|
|
6441
|
-
* @layer preset
|
|
6442
|
-
* @base string
|
|
6443
|
-
* @prim text
|
|
6444
|
-
* @widget text
|
|
6445
|
-
* @example f.country({ label: 'mod.fields.country' })
|
|
6446
|
-
*/
|
|
6447
5672
|
function country(raw) {
|
|
6448
5673
|
return string({
|
|
6449
5674
|
...raw,
|
|
@@ -6453,22 +5678,6 @@ function country(raw) {
|
|
|
6453
5678
|
noSearch: true
|
|
6454
5679
|
});
|
|
6455
5680
|
}
|
|
6456
|
-
/**
|
|
6457
|
-
* Currency — the ISO 4217 code the record's amounts are in, declared ONCE per record.
|
|
6458
|
-
* The money twin of `f.unit`: an ordinary string over the global `currencies` list, so it
|
|
6459
|
-
* is an ordinary column (groupable, filterable) and its picker is an expanded list rather
|
|
6460
|
-
* than a combobox wedged into some amount's editor.
|
|
6461
|
-
*
|
|
6462
|
-
* A record carries ONE currency. Two amounts on one record that need different currencies
|
|
6463
|
-
* are two records — `multiple: true` here means "the currencies this record deals in"
|
|
6464
|
-
* (what an account holds), and binds no amount.
|
|
6465
|
-
*
|
|
6466
|
-
* @layer preset
|
|
6467
|
-
* @base string
|
|
6468
|
-
* @prim text
|
|
6469
|
-
* @widget text
|
|
6470
|
-
* @example f.currency({ label: 'mod.fields.currency' })
|
|
6471
|
-
*/
|
|
6472
5681
|
function currency(raw) {
|
|
6473
5682
|
return string({
|
|
6474
5683
|
...raw,
|
|
@@ -6478,22 +5687,6 @@ function currency(raw) {
|
|
|
6478
5687
|
noSearch: true
|
|
6479
5688
|
});
|
|
6480
5689
|
}
|
|
6481
|
-
/**
|
|
6482
|
-
* A monetary amount — a plain number in a `real` column, whose currency is a field of the
|
|
6483
|
-
* record (`currencyFrom`). Summable, sortable and filterable by its own name, like any
|
|
6484
|
-
* other number.
|
|
6485
|
-
*
|
|
6486
|
-
* It used to be a composite `{value, currency}`: the amount then lived in `price__value`,
|
|
6487
|
-
* reachable only through `magnitudeSub`, a sum over a collection had to re-assemble
|
|
6488
|
-
* objects in JavaScript, and the currency needed a second control inside the amount's own
|
|
6489
|
-
* editor — the one place a collapsed control must never be (skill field-render).
|
|
6490
|
-
*
|
|
6491
|
-
* @layer preset
|
|
6492
|
-
* @base real
|
|
6493
|
-
* @prim number
|
|
6494
|
-
* @widget money
|
|
6495
|
-
* @example f.money({ label: 'mod.fields.price' })
|
|
6496
|
-
*/
|
|
6497
5690
|
function money(o) {
|
|
6498
5691
|
return declare("money", o);
|
|
6499
5692
|
}
|
|
@@ -6552,57 +5745,13 @@ var presets = {
|
|
|
6552
5745
|
internalOauthGrants
|
|
6553
5746
|
};
|
|
6554
5747
|
//#endregion
|
|
6555
|
-
//#region ../sdk/
|
|
6556
|
-
/**
|
|
6557
|
-
* Editorial "blocks" — record-page layout sugar over `group()` (bento grids, split
|
|
6558
|
-
* columns, tabs, …), exactly like `row`/`sheet` in fields.ts. Each factory sets
|
|
6559
|
-
* the group's `kind` (renderer resolution — see `elementKind()`) and stashes its own
|
|
6560
|
-
* layout metadata on `ContainerNode.view`, read by the block's own renderer, never by the
|
|
6561
|
-
* generic layout core.
|
|
6562
|
-
*
|
|
6563
|
-
* `calendar`/`journal`/`manifest`/`table` are the one exception to "sugar over a fresh
|
|
6564
|
-
* `group()`": their `source` is already a KEYED, `multiple: true` collection built
|
|
6565
|
-
* elsewhere (`row`/`sheet` in fields.ts — there is no separate factory to DECLARE a
|
|
6566
|
-
* tabular collection), and they stamp `kind`/`view` directly onto THAT group
|
|
6567
|
-
* (`stampCollection`) rather than wrapping it in a new keyless one — see
|
|
6568
|
-
* `stampCollection`'s own comment for why the wrapping shape does not reach the layout
|
|
6569
|
-
* engine's storage-group branch. `table` specifically is a VIEW over a collection built
|
|
6570
|
-
* elsewhere, never a second way to declare one. `calendar`/`journal`/`manifest`/`table`'s
|
|
6571
|
-
* `source` stays typed `ContainerNode` (already built, already keyed) — the exception is about
|
|
6572
|
-
* this wrapping, not about naming, so it is untouched by `SlotInput` below.
|
|
6573
|
-
*
|
|
6574
|
-
* Every OTHER el-valued slot a factory here declares — a single element (`figure.image`,
|
|
6575
|
-
* `masthead.title`, `route.from`, …) or a list (`masthead.meta`, `compare.left`, …) — takes
|
|
6576
|
-
* a NAMED MAP: see `SlotInput`'s own doc comment for the single case, `LayoutInput`
|
|
6577
|
-
* (materialize/decl.ts) for the list case. The slot names the ROLE; the map's own property
|
|
6578
|
-
* name(s) become the STORAGE key(s).
|
|
6579
|
-
*/
|
|
5748
|
+
//#region ../sdk/dist/blocks.js
|
|
6580
5749
|
var isSpanned = (i) => typeof i === "object" && i !== null && "span" in i && "el" in i;
|
|
6581
|
-
/**
|
|
6582
|
-
* Validates a block's single-element slot and hands back the one-entry map it is. A slot
|
|
6583
|
-
* is not built here: every slot a block lays out is merged into ONE map (`merge` below)
|
|
6584
|
-
* and walked by `group()` exactly once, like any other container's `fields`.
|
|
6585
|
-
*
|
|
6586
|
-
* A missing/nullish slot is the same "got 0" failure as an empty map, not a native
|
|
6587
|
-
* TypeError from `Object.keys(undefined)` — every empty-slot failure mode speaks the same
|
|
6588
|
-
* message, whether the author wrote `{}`, left the option out, or passed `undefined`.
|
|
6589
|
-
*/
|
|
6590
5750
|
function slot(block, name, input) {
|
|
6591
5751
|
const count = input == null ? 0 : Object.keys(input).length;
|
|
6592
5752
|
if (count !== 1) throw new Error(`[field.${block}] slot '${name}' expects exactly one entry, got ${count}`);
|
|
6593
5753
|
return input;
|
|
6594
5754
|
}
|
|
6595
|
-
/**
|
|
6596
|
-
* A block's children, in the order the block lays them out. Every slot is a NAMED MAP, so
|
|
6597
|
-
* flattening several slots into one child list is a MERGE: the names the author gave are
|
|
6598
|
-
* already there and nothing has to be generated or derived from a position. Insertion
|
|
6599
|
-
* order is declaration order, which is also the order every ORDERED NAME LIST the layout
|
|
6600
|
-
* metadata below carries (`first`/`second`, `meta`, `conditions`, …) reports its names in —
|
|
6601
|
-
* a NAME, never a count against a flattened position (Task 5).
|
|
6602
|
-
*
|
|
6603
|
-
* A name claimed by two slots is a storage-key collision, not a layout choice — the flat
|
|
6604
|
-
* form used to let both through and store one of them nowhere.
|
|
6605
|
-
*/
|
|
6606
5755
|
function merge(block, ...slots) {
|
|
6607
5756
|
const out = {};
|
|
6608
5757
|
for (const s of slots) {
|
|
@@ -6614,21 +5763,6 @@ function merge(block, ...slots) {
|
|
|
6614
5763
|
}
|
|
6615
5764
|
return out;
|
|
6616
5765
|
}
|
|
6617
|
-
/**
|
|
6618
|
-
* Asymmetric card grid. '1x1' is the implicit default — only wider spans are recorded, BY
|
|
6619
|
-
* NAME (Task 5): each tile IS one declared field, so `spans` is keyed by that field's own
|
|
6620
|
-
* storage name — the same name the renderer already addresses it by — never a position in
|
|
6621
|
-
* the built list.
|
|
6622
|
-
*
|
|
6623
|
-
* `fields` takes a NAMED MAP of `BentoItem` values, handed to `group()` which walks it
|
|
6624
|
-
* exactly like every other container's `fields`.
|
|
6625
|
-
*
|
|
6626
|
-
* @layer block
|
|
6627
|
-
* @base group
|
|
6628
|
-
* @prim —
|
|
6629
|
-
* @widget bento
|
|
6630
|
-
* @example f.bento({ fields: { name: f.string({ label: '…' }), model: { span: '2x1', el: f.string({ label: '…' }) } } })
|
|
6631
|
-
*/
|
|
6632
5766
|
function bento(opts) {
|
|
6633
5767
|
const spans = {};
|
|
6634
5768
|
const fields = Object.fromEntries(Object.entries(opts.fields).map(([name, item]) => {
|
|
@@ -6649,23 +5783,6 @@ function bento(opts) {
|
|
|
6649
5783
|
}
|
|
6650
5784
|
};
|
|
6651
5785
|
}
|
|
6652
|
-
/**
|
|
6653
|
-
* Two-column split (default ratio 1:1). Columns are flattened; the left/right boundary is
|
|
6654
|
-
* remembered.
|
|
6655
|
-
*
|
|
6656
|
-
* Each column is a LIST slot (a named map — see `LayoutInput`'s own doc comment). The
|
|
6657
|
-
* tuple POSITION (index 0/1) stays the left/right layout signal, unrelated to storage
|
|
6658
|
-
* naming; each column's own fields are named the same way `fields` is everywhere else, and
|
|
6659
|
-
* the two columns are MERGED into one map for `group()` (`merge`, above). `view.first`/
|
|
6660
|
-
* `view.second` (Task 5) are the ORDERED LISTS of each column's own field names — the
|
|
6661
|
-
* renderer reaches every child by name, never a count against a flattened position.
|
|
6662
|
-
*
|
|
6663
|
-
* @layer block
|
|
6664
|
-
* @base group
|
|
6665
|
-
* @prim —
|
|
6666
|
-
* @widget split
|
|
6667
|
-
* @example f.split({ columns: [{ left: f.string({ label: '…' }) }, { right: f.string({ label: '…' }) }] })
|
|
6668
|
-
*/
|
|
6669
5786
|
function split(opts) {
|
|
6670
5787
|
return {
|
|
6671
5788
|
...group({
|
|
@@ -6680,14 +5797,6 @@ function split(opts) {
|
|
|
6680
5797
|
}
|
|
6681
5798
|
};
|
|
6682
5799
|
}
|
|
6683
|
-
/** Even N-column grid (default 3).
|
|
6684
|
-
*
|
|
6685
|
-
* @layer block
|
|
6686
|
-
* @base group
|
|
6687
|
-
* @prim —
|
|
6688
|
-
* @widget grid
|
|
6689
|
-
* @example f.grid({ columns: 3, fields: { a: f.string({ label: '…' }), b: f.string({ label: '…' }) } })
|
|
6690
|
-
*/
|
|
6691
5800
|
function grid(opts) {
|
|
6692
5801
|
return {
|
|
6693
5802
|
...group({
|
|
@@ -6702,17 +5811,6 @@ function grid(opts) {
|
|
|
6702
5811
|
}
|
|
6703
5812
|
};
|
|
6704
5813
|
}
|
|
6705
|
-
/**
|
|
6706
|
-
* A visual, an optional pull quote, and supporting facts. `view` names each role (Task 5):
|
|
6707
|
-
* `visual`/`quote` are single field names (`quote` `undefined` when not declared),
|
|
6708
|
-
* `facts` is the ORDERED LIST of the fact fields' own storage names.
|
|
6709
|
-
*
|
|
6710
|
-
* @layer block
|
|
6711
|
-
* @base group
|
|
6712
|
-
* @prim —
|
|
6713
|
-
* @widget spread
|
|
6714
|
-
* @example f.spread({ visual: { photo: f.image({ label: '…' }) }, quote: { quote: f.string({ label: '…' }) }, facts: { fact: f.string({ label: '…' }) } })
|
|
6715
|
-
*/
|
|
6716
5814
|
function spread(opts) {
|
|
6717
5815
|
const visual = slot("spread", "visual", opts.visual);
|
|
6718
5816
|
const quote = opts.quote !== void 0 ? slot("spread", "quote", opts.quote) : void 0;
|
|
@@ -6729,17 +5827,6 @@ function spread(opts) {
|
|
|
6729
5827
|
}
|
|
6730
5828
|
};
|
|
6731
5829
|
}
|
|
6732
|
-
/**
|
|
6733
|
-
* Main content plus a side rail. Flattened main-then-aside; the boundary is remembered.
|
|
6734
|
-
* `view.main`/`view.aside` (Task 5) are the ORDERED LISTS of each region's own field
|
|
6735
|
-
* names — see `split`'s own doc comment for what that means and why.
|
|
6736
|
-
*
|
|
6737
|
-
* @layer block
|
|
6738
|
-
* @base group
|
|
6739
|
-
* @prim —
|
|
6740
|
-
* @widget aside
|
|
6741
|
-
* @example f.aside({ fields: { main: f.string({ label: '…' }) }, aside: { side: f.string({ label: '…' }) } })
|
|
6742
|
-
*/
|
|
6743
5830
|
function aside(opts) {
|
|
6744
5831
|
return {
|
|
6745
5832
|
...group({
|
|
@@ -6753,20 +5840,6 @@ function aside(opts) {
|
|
|
6753
5840
|
}
|
|
6754
5841
|
};
|
|
6755
5842
|
}
|
|
6756
|
-
/**
|
|
6757
|
-
* Tabbed sections. Each tab is a NAMED GROUP of its own fields (Task 5) — `t.name` becomes
|
|
6758
|
-
* the storage key of a NESTED, keyless (`scope: 'hoist'`, storage-transparent — see
|
|
6759
|
-
* `group`'s own `scope` doc comment) group wrapping `t.fields`, so the renderer reaches a
|
|
6760
|
-
* tab's own children by descending into that name (`SlotScope`, `render/slot.tsx`) instead
|
|
6761
|
-
* of slicing a flattened list by a cumulative per-tab count. A duplicate `name` across
|
|
6762
|
-
* tabs is a storage-key collision, exactly like `merge`'s own duplicate-name guard.
|
|
6763
|
-
*
|
|
6764
|
-
* @layer block
|
|
6765
|
-
* @base group
|
|
6766
|
-
* @prim —
|
|
6767
|
-
* @widget tabs
|
|
6768
|
-
* @example f.tabs({ tabs: [{ name: 'overview', label: '…', fields: { a: f.string({ label: '…' }) } }] })
|
|
6769
|
-
*/
|
|
6770
5843
|
function tabs(opts) {
|
|
6771
5844
|
const fields = {};
|
|
6772
5845
|
for (const t of opts.tabs) {
|
|
@@ -6788,17 +5861,6 @@ function tabs(opts) {
|
|
|
6788
5861
|
}
|
|
6789
5862
|
};
|
|
6790
5863
|
}
|
|
6791
|
-
/**
|
|
6792
|
-
* Collapsible sections. Each section is a NAMED GROUP of its own fields (Task 5) — see
|
|
6793
|
-
* `tabs`'s own doc comment for what that means and why; the renderer descends into a
|
|
6794
|
-
* section's own children by name (`SlotScope`) instead of a cumulative count.
|
|
6795
|
-
*
|
|
6796
|
-
* @layer block
|
|
6797
|
-
* @base group
|
|
6798
|
-
* @prim —
|
|
6799
|
-
* @widget accordion
|
|
6800
|
-
* @example f.accordion({ sections: [{ name: 'details', label: '…', fields: { a: f.string({ label: '…' }) } }] })
|
|
6801
|
-
*/
|
|
6802
5864
|
function accordion(opts) {
|
|
6803
5865
|
const fields = {};
|
|
6804
5866
|
for (const s of opts.sections) {
|
|
@@ -6821,16 +5883,6 @@ function accordion(opts) {
|
|
|
6821
5883
|
}
|
|
6822
5884
|
};
|
|
6823
5885
|
}
|
|
6824
|
-
/** Flowing prose paragraph, optionally with a dropped first capital letter on ONE named
|
|
6825
|
-
* field — `dropCap` names WHICH one (Task 5), never "whichever field was declared
|
|
6826
|
-
* first." Must be one of `fields`'s own storage keys.
|
|
6827
|
-
*
|
|
6828
|
-
* @layer block
|
|
6829
|
-
* @base group
|
|
6830
|
-
* @prim —
|
|
6831
|
-
* @widget prose
|
|
6832
|
-
* @example f.prose({ fields: { body: f.text({ label: '…' }) }, dropCap: 'body' })
|
|
6833
|
-
*/
|
|
6834
5886
|
function prose(opts) {
|
|
6835
5887
|
if (opts.dropCap !== void 0 && !(opts.dropCap in opts.fields)) throw new Error(`[field.prose] dropCap '${opts.dropCap}' is not one of this block's own fields`);
|
|
6836
5888
|
return {
|
|
@@ -6846,17 +5898,6 @@ function prose(opts) {
|
|
|
6846
5898
|
}
|
|
6847
5899
|
};
|
|
6848
5900
|
}
|
|
6849
|
-
/** A small labeled plate of fields: one TITLE (large, semibold), then zero or more
|
|
6850
|
-
* SUBTITLE fields (small, tracked-out, muted) beneath it. Which field is the title is
|
|
6851
|
-
* NAMED (Task 5), the same way `figure.image`/`masthead.title` is — never "whichever
|
|
6852
|
-
* field was declared first."
|
|
6853
|
-
*
|
|
6854
|
-
* @layer block
|
|
6855
|
-
* @base group
|
|
6856
|
-
* @prim —
|
|
6857
|
-
* @widget plaque
|
|
6858
|
-
* @example f.plaque({ title: { a: f.string({ label: '…' }) }, subtitle: { b: f.string({ label: '…' }) } })
|
|
6859
|
-
*/
|
|
6860
5901
|
function plaque(opts) {
|
|
6861
5902
|
const title = slot("plaque", "title", opts.title);
|
|
6862
5903
|
return {
|
|
@@ -6873,14 +5914,6 @@ function plaque(opts) {
|
|
|
6873
5914
|
}
|
|
6874
5915
|
};
|
|
6875
5916
|
}
|
|
6876
|
-
/** A ledger-style stack of label/value rows.
|
|
6877
|
-
*
|
|
6878
|
-
* @layer block
|
|
6879
|
-
* @base group
|
|
6880
|
-
* @prim —
|
|
6881
|
-
* @widget ledger
|
|
6882
|
-
* @example f.ledger({ fields: { a: f.string({ label: '…' }) } })
|
|
6883
|
-
*/
|
|
6884
5917
|
function ledger(opts) {
|
|
6885
5918
|
return {
|
|
6886
5919
|
...group({
|
|
@@ -6892,14 +5925,6 @@ function ledger(opts) {
|
|
|
6892
5925
|
view: { kind: "ledger" }
|
|
6893
5926
|
};
|
|
6894
5927
|
}
|
|
6895
|
-
/** A row of headline statistics.
|
|
6896
|
-
*
|
|
6897
|
-
* @layer block
|
|
6898
|
-
* @base group
|
|
6899
|
-
* @prim —
|
|
6900
|
-
* @widget stats
|
|
6901
|
-
* @example f.stats({ fields: { count: f.real({ label: '…' }) } })
|
|
6902
|
-
*/
|
|
6903
5928
|
function stats(opts) {
|
|
6904
5929
|
return {
|
|
6905
5930
|
...group({
|
|
@@ -6911,14 +5936,6 @@ function stats(opts) {
|
|
|
6911
5936
|
view: { kind: "stats" }
|
|
6912
5937
|
};
|
|
6913
5938
|
}
|
|
6914
|
-
/** A cluster of small tag-like facets.
|
|
6915
|
-
*
|
|
6916
|
-
* @layer block
|
|
6917
|
-
* @base group
|
|
6918
|
-
* @prim —
|
|
6919
|
-
* @widget facets
|
|
6920
|
-
* @example f.facets({ fields: { tag: f.string({ label: '…' }) } })
|
|
6921
|
-
*/
|
|
6922
5939
|
function facets(opts) {
|
|
6923
5940
|
return {
|
|
6924
5941
|
...group({
|
|
@@ -6930,14 +5947,6 @@ function facets(opts) {
|
|
|
6930
5947
|
view: { kind: "facets" }
|
|
6931
5948
|
};
|
|
6932
5949
|
}
|
|
6933
|
-
/** A monospace terminal/code block, with an optional i18n caption.
|
|
6934
|
-
*
|
|
6935
|
-
* @layer block
|
|
6936
|
-
* @base group
|
|
6937
|
-
* @prim —
|
|
6938
|
-
* @widget terminal
|
|
6939
|
-
* @example f.terminal({ fields: { log: f.text({ label: '…' }) }, caption: 'mod.blocks.terminalCaption' })
|
|
6940
|
-
*/
|
|
6941
5950
|
function terminal(opts) {
|
|
6942
5951
|
return {
|
|
6943
5952
|
...group({
|
|
@@ -6952,14 +5961,6 @@ function terminal(opts) {
|
|
|
6952
5961
|
}
|
|
6953
5962
|
};
|
|
6954
5963
|
}
|
|
6955
|
-
/** A toned callout box (default tone: note).
|
|
6956
|
-
*
|
|
6957
|
-
* @layer block
|
|
6958
|
-
* @base group
|
|
6959
|
-
* @prim —
|
|
6960
|
-
* @widget callout
|
|
6961
|
-
* @example f.callout({ tone: 'warn', fields: { note: f.string({ label: '…' }) } })
|
|
6962
|
-
*/
|
|
6963
5964
|
function callout(opts) {
|
|
6964
5965
|
return {
|
|
6965
5966
|
...group({
|
|
@@ -6974,17 +5975,6 @@ function callout(opts) {
|
|
|
6974
5975
|
}
|
|
6975
5976
|
};
|
|
6976
5977
|
}
|
|
6977
|
-
/**
|
|
6978
|
-
* Two side-by-side sets of fields. Flattened left-then-right; the boundary is remembered.
|
|
6979
|
-
* `view.left`/`view.right` (Task 5) are the ORDERED LISTS of each column's own field
|
|
6980
|
-
* names — see `split`'s own doc comment for what that means and why.
|
|
6981
|
-
*
|
|
6982
|
-
* @layer block
|
|
6983
|
-
* @base group
|
|
6984
|
-
* @prim —
|
|
6985
|
-
* @widget compare
|
|
6986
|
-
* @example f.compare({ left: { a: f.string({ label: '…' }) }, right: { b: f.string({ label: '…' }) } })
|
|
6987
|
-
*/
|
|
6988
5978
|
function compare(opts) {
|
|
6989
5979
|
return {
|
|
6990
5980
|
...group({
|
|
@@ -7002,20 +5992,6 @@ function compare(opts) {
|
|
|
7002
5992
|
}
|
|
7003
5993
|
};
|
|
7004
5994
|
}
|
|
7005
|
-
/**
|
|
7006
|
-
* A picture and its caption as one typographic unit.
|
|
7007
|
-
*
|
|
7008
|
-
* `view` carries the STORAGE NAME of each role (not a count/boolean, Task 5): `image` is
|
|
7009
|
-
* always declared; `caption` is `undefined` when the slot was never given. The renderer
|
|
7010
|
-
* reaches each child through `useSlot`/`<Slot>` by this name — never by position in
|
|
7011
|
-
* `el.fields`, so reordering how `merge()` (above) assembles the fields never matters.
|
|
7012
|
-
*
|
|
7013
|
-
* @layer block
|
|
7014
|
-
* @base group
|
|
7015
|
-
* @prim —
|
|
7016
|
-
* @widget figure
|
|
7017
|
-
* @example f.figure({ image: { photo: f.image({ label: '…' }) }, caption: { caption: f.string({ label: '…' }) } })
|
|
7018
|
-
*/
|
|
7019
5995
|
function figure(opts) {
|
|
7020
5996
|
const image = slot("figure", "image", opts.image);
|
|
7021
5997
|
const caption = opts.caption !== void 0 ? slot("figure", "caption", opts.caption) : void 0;
|
|
@@ -7031,16 +6007,6 @@ function figure(opts) {
|
|
|
7031
6007
|
}
|
|
7032
6008
|
};
|
|
7033
6009
|
}
|
|
7034
|
-
/**
|
|
7035
|
-
* A quoted source with an optional attribution. `view` names each role (Task 5) — see
|
|
7036
|
-
* `figure`'s own doc comment for what that means and why.
|
|
7037
|
-
*
|
|
7038
|
-
* @layer block
|
|
7039
|
-
* @base group
|
|
7040
|
-
* @prim —
|
|
7041
|
-
* @widget epigraph
|
|
7042
|
-
* @example f.epigraph({ source: { quote: f.text({ label: '…' }) }, attribution: { author: f.string({ label: '…' }) } })
|
|
7043
|
-
*/
|
|
7044
6010
|
function epigraph(opts) {
|
|
7045
6011
|
const source = slot("epigraph", "source", opts.source);
|
|
7046
6012
|
const attribution = opts.attribution !== void 0 ? slot("epigraph", "attribution", opts.attribution) : void 0;
|
|
@@ -7056,17 +6022,6 @@ function epigraph(opts) {
|
|
|
7056
6022
|
}
|
|
7057
6023
|
};
|
|
7058
6024
|
}
|
|
7059
|
-
/**
|
|
7060
|
-
* A masthead header: optional overline, title, and trailing meta fields. `view` names each
|
|
7061
|
-
* role (Task 5) — `meta` is the ORDERED LIST of the meta fields' own storage names (declared
|
|
7062
|
-
* insertion order of `opts.meta`), not a count against a flattened position.
|
|
7063
|
-
*
|
|
7064
|
-
* @layer block
|
|
7065
|
-
* @base group
|
|
7066
|
-
* @prim —
|
|
7067
|
-
* @widget masthead
|
|
7068
|
-
* @example f.masthead({ title: { title: f.string({ label: '…' }) }, meta: { subtitle: f.string({ label: '…' }) } })
|
|
7069
|
-
*/
|
|
7070
6025
|
function masthead(opts) {
|
|
7071
6026
|
const overline = opts.overline !== void 0 ? slot("masthead", "overline", opts.overline) : void 0;
|
|
7072
6027
|
const title = slot("masthead", "title", opts.title);
|
|
@@ -7083,14 +6038,6 @@ function masthead(opts) {
|
|
|
7083
6038
|
}
|
|
7084
6039
|
};
|
|
7085
6040
|
}
|
|
7086
|
-
/** A vertical timeline of dated entries.
|
|
7087
|
-
*
|
|
7088
|
-
* @layer block
|
|
7089
|
-
* @base group
|
|
7090
|
-
* @prim —
|
|
7091
|
-
* @widget timeline
|
|
7092
|
-
* @example f.timeline({ fields: { date: f.date({ label: '…' }) } })
|
|
7093
|
-
*/
|
|
7094
6041
|
function timeline(opts) {
|
|
7095
6042
|
return {
|
|
7096
6043
|
...group({
|
|
@@ -7102,14 +6049,6 @@ function timeline(opts) {
|
|
|
7102
6049
|
view: { kind: "timeline" }
|
|
7103
6050
|
};
|
|
7104
6051
|
}
|
|
7105
|
-
/** A single value counting down (or up) to/from a target, with an optional i18n caption.
|
|
7106
|
-
*
|
|
7107
|
-
* @layer block
|
|
7108
|
-
* @base group
|
|
7109
|
-
* @prim —
|
|
7110
|
-
* @widget countdown
|
|
7111
|
-
* @example f.countdown({ source: { deadline: f.date({ label: '…' }) }, caption: 'mod.blocks.countdownCaption' })
|
|
7112
|
-
*/
|
|
7113
6052
|
function countdown(opts) {
|
|
7114
6053
|
const source = slot("countdown", "source", opts.source);
|
|
7115
6054
|
return {
|
|
@@ -7125,14 +6064,6 @@ function countdown(opts) {
|
|
|
7125
6064
|
}
|
|
7126
6065
|
};
|
|
7127
6066
|
}
|
|
7128
|
-
/** A card deck rendered from a single collection source.
|
|
7129
|
-
*
|
|
7130
|
-
* @layer block
|
|
7131
|
-
* @base group
|
|
7132
|
-
* @prim —
|
|
7133
|
-
* @widget deck
|
|
7134
|
-
* @example f.deck({ source: { cards: f.group({ scope: 'nest', multiple: true, fields: { name: f.string({ label: '…' }) } }) } })
|
|
7135
|
-
*/
|
|
7136
6067
|
function deck(opts) {
|
|
7137
6068
|
const source = slot("deck", "source", opts.source);
|
|
7138
6069
|
return {
|
|
@@ -7147,14 +6078,6 @@ function deck(opts) {
|
|
|
7147
6078
|
}
|
|
7148
6079
|
};
|
|
7149
6080
|
}
|
|
7150
|
-
/** A roster rendered from a single collection source.
|
|
7151
|
-
*
|
|
7152
|
-
* @layer block
|
|
7153
|
-
* @base group
|
|
7154
|
-
* @prim —
|
|
7155
|
-
* @widget people
|
|
7156
|
-
* @example f.people({ source: { members: f.group({ scope: 'nest', multiple: true, fields: { name: f.string({ label: '…' }) } }) } })
|
|
7157
|
-
*/
|
|
7158
6081
|
function people(opts) {
|
|
7159
6082
|
const source = slot("people", "source", opts.source);
|
|
7160
6083
|
return {
|
|
@@ -7169,17 +6092,6 @@ function people(opts) {
|
|
|
7169
6092
|
}
|
|
7170
6093
|
};
|
|
7171
6094
|
}
|
|
7172
|
-
/**
|
|
7173
|
-
* A person/entity identity block: optional avatar, name, optional role, and channel links.
|
|
7174
|
-
* `view` names each role (Task 5): `avatar`/`role` are the declared field's own storage
|
|
7175
|
-
* name or `undefined`; `channels` is the ORDERED LIST of the channel fields' own names.
|
|
7176
|
-
*
|
|
7177
|
-
* @layer block
|
|
7178
|
-
* @base group
|
|
7179
|
-
* @prim —
|
|
7180
|
-
* @widget identity
|
|
7181
|
-
* @example f.identity({ name: { name: f.string({ label: '…' }) }, role: { role: f.string({ label: '…' }) } })
|
|
7182
|
-
*/
|
|
7183
6095
|
function identity(opts) {
|
|
7184
6096
|
const avatar = opts.avatar !== void 0 ? slot("identity", "avatar", opts.avatar) : void 0;
|
|
7185
6097
|
const name = slot("identity", "name", opts.name);
|
|
@@ -7198,25 +6110,6 @@ function identity(opts) {
|
|
|
7198
6110
|
}
|
|
7199
6111
|
};
|
|
7200
6112
|
}
|
|
7201
|
-
/** One measure out of an optional max, from ONE OR MORE named sources, with an optional verdict.
|
|
7202
|
-
*
|
|
7203
|
-
* `source` takes several entries for the same reason `f.identity`'s `channels` does — it is a
|
|
7204
|
-
* role that holds a LIST, so `view.source` is an ordered list of names rather than one name.
|
|
7205
|
-
* Two ratings of the same film out of ten are one measure read twice, and drawing them apart is
|
|
7206
|
-
* what makes them incomparable: the eye has to carry the scale between two figures instead of
|
|
7207
|
-
* reading them against a shared one. `media/title` had exactly that, two `f.score` blocks of
|
|
7208
|
-
* `max: 10` held apart inside an `f.compare`, until this took the restriction off.
|
|
7209
|
-
*
|
|
7210
|
-
* NOT `multiple`. A multiple field is an anonymous array; these are NAMED sources, and the name
|
|
7211
|
-
* is what says which reading came from where. The two are different shapes and the block wants
|
|
7212
|
-
* this one.
|
|
7213
|
-
*
|
|
7214
|
-
* @layer block
|
|
7215
|
-
* @base group
|
|
7216
|
-
* @prim —
|
|
7217
|
-
* @widget score
|
|
7218
|
-
* @example f.score({ source: { imdb: f.real({ label: '…' }), tmdb: f.real({ label: '…' }) }, max: 10 })
|
|
7219
|
-
*/
|
|
7220
6113
|
function score(opts) {
|
|
7221
6114
|
const names = Object.keys(opts.source ?? {});
|
|
7222
6115
|
if (names.length === 0) throw new Error(`[field.score] slot 'source' expects at least one entry, got 0`);
|
|
@@ -7235,14 +6128,6 @@ function score(opts) {
|
|
|
7235
6128
|
}
|
|
7236
6129
|
};
|
|
7237
6130
|
}
|
|
7238
|
-
/** A status value with an optional "since" timestamp.
|
|
7239
|
-
*
|
|
7240
|
-
* @layer block
|
|
7241
|
-
* @base group
|
|
7242
|
-
* @prim —
|
|
7243
|
-
* @widget status
|
|
7244
|
-
* @example f.status({ source: { state: f.string({ label: '…' }) }, since: { since: f.date({ label: '…' }) } })
|
|
7245
|
-
*/
|
|
7246
6131
|
function status(opts) {
|
|
7247
6132
|
const source = slot("status", "source", opts.source);
|
|
7248
6133
|
const since = opts.since !== void 0 ? slot("status", "since", opts.since) : void 0;
|
|
@@ -7259,23 +6144,6 @@ function status(opts) {
|
|
|
7259
6144
|
}
|
|
7260
6145
|
};
|
|
7261
6146
|
}
|
|
7262
|
-
/** A gauge value between an optional min and max, with an optional "of" total.
|
|
7263
|
-
*
|
|
7264
|
-
* `direction` is opt-in: when given, the gauge decides in plain JavaScript whether its own
|
|
7265
|
-
* value crossed the bound that matters (`meterTone`, `blocks/meter.tsx`) and reaches for the
|
|
7266
|
-
* tone palette — the fill, the value text and the `of` companion all move together, never
|
|
7267
|
-
* colour alone (a glyph rides along, see the renderer's own comment). Omit it and nothing
|
|
7268
|
-
* about the gauge changes from today. A `'ceiling'` gauge with no declared `max` (config or
|
|
7269
|
-
* the source field's own hints) resolves it from `of`'s own value instead — a budget's cap is
|
|
7270
|
-
* a per-record field, never a compile-time constant.
|
|
7271
|
-
*
|
|
7272
|
-
* @layer block
|
|
7273
|
-
* @base group
|
|
7274
|
-
* @prim —
|
|
7275
|
-
* @widget meter
|
|
7276
|
-
* @example f.meter({ source: { used: f.real({ label: '…' }) }, min: 0, max: 100 })
|
|
7277
|
-
* @example f.meter({ source: { spent: f.real({ label: '…' }) }, of: { budget: f.real({ label: '…' }) }, direction: 'ceiling' })
|
|
7278
|
-
*/
|
|
7279
6147
|
function meter(opts) {
|
|
7280
6148
|
const source = slot("meter", "source", opts.source);
|
|
7281
6149
|
const of = opts.of !== void 0 ? slot("meter", "of", opts.of) : void 0;
|
|
@@ -7295,16 +6163,6 @@ function meter(opts) {
|
|
|
7295
6163
|
}
|
|
7296
6164
|
};
|
|
7297
6165
|
}
|
|
7298
|
-
/**
|
|
7299
|
-
* A line-item receipt with a trailing total. `view.lines` (Task 5) is the ORDERED LIST of
|
|
7300
|
-
* the line fields' own storage names; `view.total` is the total field's own name.
|
|
7301
|
-
*
|
|
7302
|
-
* @layer block
|
|
7303
|
-
* @base group
|
|
7304
|
-
* @prim —
|
|
7305
|
-
* @widget receipt
|
|
7306
|
-
* @example f.receipt({ fields: { line: f.real({ label: '…' }) }, total: { total: f.real({ label: '…' }) } })
|
|
7307
|
-
*/
|
|
7308
6166
|
function receipt(opts) {
|
|
7309
6167
|
const total = slot("receipt", "total", opts.total);
|
|
7310
6168
|
return {
|
|
@@ -7320,14 +6178,6 @@ function receipt(opts) {
|
|
|
7320
6178
|
}
|
|
7321
6179
|
};
|
|
7322
6180
|
}
|
|
7323
|
-
/** A financial balance value, with an optional i18n caption.
|
|
7324
|
-
*
|
|
7325
|
-
* @layer block
|
|
7326
|
-
* @base group
|
|
7327
|
-
* @prim —
|
|
7328
|
-
* @widget balance
|
|
7329
|
-
* @example f.balance({ source: { balance: f.real({ label: '…' }) }, caption: 'mod.blocks.balanceCaption' })
|
|
7330
|
-
*/
|
|
7331
6181
|
function balance(opts) {
|
|
7332
6182
|
const source = slot("balance", "source", opts.source);
|
|
7333
6183
|
return {
|
|
@@ -7343,21 +6193,6 @@ function balance(opts) {
|
|
|
7343
6193
|
}
|
|
7344
6194
|
};
|
|
7345
6195
|
}
|
|
7346
|
-
/**
|
|
7347
|
-
* A from/to route with optional depart/arrive times and duration. `view` names each role
|
|
7348
|
-
* (Task 5) — see `figure`'s own doc comment for what that means and why.
|
|
7349
|
-
*
|
|
7350
|
-
* @layer block
|
|
7351
|
-
* @base group
|
|
7352
|
-
* @prim —
|
|
7353
|
-
* @widget route
|
|
7354
|
-
* A `stub` is the part of the ticket that is TORN OFF and kept — a seat, a gate, a booking
|
|
7355
|
-
* reference. Declaring one makes the block a ticket rather than a line: the two halves are
|
|
7356
|
-
* separated by a perforation, and the fields in the stub sit below it. Without one the block
|
|
7357
|
-
* is exactly what it was, a route from here to there, so no existing call changes.
|
|
7358
|
-
*
|
|
7359
|
-
* @example f.route({ from: { from: f.string({ label: '…' }) }, to: { to: f.string({ label: '…' }) } })
|
|
7360
|
-
*/
|
|
7361
6196
|
function route(opts) {
|
|
7362
6197
|
const from = slot("route", "from", opts.from);
|
|
7363
6198
|
const to = slot("route", "to", opts.to);
|
|
@@ -7381,25 +6216,6 @@ function route(opts) {
|
|
|
7381
6216
|
}
|
|
7382
6217
|
};
|
|
7383
6218
|
}
|
|
7384
|
-
/**
|
|
7385
|
-
* Stamps a block's `kind` (and, for journal/manifest, its part-key config) directly onto
|
|
7386
|
-
* the collection `source` itself, rather than wrapping it in a keyless layout group.
|
|
7387
|
-
*
|
|
7388
|
-
* The earlier shape (`group({ fields: [source], view: {kind} })`) put `kind` on an outer,
|
|
7389
|
-
* KEYLESS wrapper and left `source` — a KEYED, `multiple: true` collection — as an ordinary
|
|
7390
|
-
* child. `elementKind()` (`web-ui/registry.ts`) resolves a group's `kind` ahead of its
|
|
7391
|
-
* storage kind, so that wrapper WOULD have resolved through the registry — but only the
|
|
7392
|
-
* layout engine's KEYLESS-group dispatch (a declared `kind`, resolved through the field
|
|
7393
|
-
* renderer registry — `layout.tsx`) ever looks at it there; the actual collection one level
|
|
7394
|
-
* down still elementKind()s to the generic 'collection' and renders through the ordinary
|
|
7395
|
-
* CollectionPage widget, opaque to the block.
|
|
7396
|
-
*
|
|
7397
|
-
* Stamping `source` itself instead routes it through the layout engine's STORAGE-group
|
|
7398
|
-
* branch (`isStorageGroup`, `layout.tsx`) exactly like `field.check({multiple:true})`'s
|
|
7399
|
-
* `checklist` kind: an ordinary `registerRenderer({kinds:[...]})` Page receives the raw row
|
|
7400
|
-
* array as `value` and the collection's own write path as `onChange` directly — see
|
|
7401
|
-
* `render/blocks/{calendar,journal,manifest,table}.tsx`.
|
|
7402
|
-
*/
|
|
7403
6219
|
function stampCollection(block, slotted, label, view) {
|
|
7404
6220
|
const [name, source] = Object.entries(slot(block, "source", slotted))[0];
|
|
7405
6221
|
if (!hasChildren(source) || source.scope !== "nest" || source.multiple !== true) throw new Error(`[field.${block}] source must be a collection (a 'nest', multiple:true group)`);
|
|
@@ -7411,45 +6227,12 @@ function stampCollection(block, slotted, label, view) {
|
|
|
7411
6227
|
view
|
|
7412
6228
|
};
|
|
7413
6229
|
}
|
|
7414
|
-
/**
|
|
7415
|
-
* A calendar rendered from a single collection source — see `stampCollection`. `date` is a
|
|
7416
|
-
* part KEY inside each collection row (a `date` or `datetime` field), same as `journal`'s
|
|
7417
|
-
* `date`/`manifest`'s `quantity`/`item` — an explicit key, not a guess: a collection with
|
|
7418
|
-
* more than one date-shaped part (e.g. `start`/`end`) would otherwise mark the grid by
|
|
7419
|
-
* whichever one is found first, silently.
|
|
7420
|
-
*
|
|
7421
|
-
* @layer block
|
|
7422
|
-
* @base group
|
|
7423
|
-
* @prim —
|
|
7424
|
-
* @widget calendar
|
|
7425
|
-
* @example f.calendar({ source: { entries: f.group({ scope: 'nest', multiple: true, fields: { day: f.date({ label: '…' }) } }) }, date: 'day' })
|
|
7426
|
-
*/
|
|
7427
6230
|
function calendar(opts) {
|
|
7428
6231
|
return stampCollection("calendar", opts.source, opts.label, {
|
|
7429
6232
|
kind: "calendar",
|
|
7430
6233
|
date: opts.date
|
|
7431
6234
|
});
|
|
7432
6235
|
}
|
|
7433
|
-
/**
|
|
7434
|
-
* A sparkline over a series held in a collection source — see `stampCollection`. `value`
|
|
7435
|
-
* and `date` are part KEYS inside each collection row, the same explicit addressing
|
|
7436
|
-
* `calendar`'s `date` and `journal`'s `date`/`text` use: a collection with more than one
|
|
7437
|
-
* numeric or date-shaped part would otherwise be charted by whichever one is found first.
|
|
7438
|
-
* `date` orders the series; it is never an axis (the sparkline spaces its points evenly —
|
|
7439
|
-
* `render/blocks/series.ts` says why). `delta: true` adds the change against the previous
|
|
7440
|
-
* reading beside the last value.
|
|
7441
|
-
*
|
|
7442
|
-
* The design sketched this as `f.chart({ source: collectionKey, … })`. There is no
|
|
7443
|
-
* `collectionKey` type and no resolver for one anywhere in the tree — the design's own
|
|
7444
|
-
* "As shipped" note had already retired that sketch for journal/manifest/calendar without
|
|
7445
|
-
* updating this row.
|
|
7446
|
-
*
|
|
7447
|
-
* @layer block
|
|
7448
|
-
* @base group
|
|
7449
|
-
* @prim —
|
|
7450
|
-
* @widget chart
|
|
7451
|
-
* @example f.chart({ source: { readings: f.group({ scope: 'nest', multiple: true, fields: { day: f.date({ label: '…' }), kg: f.real({ label: '…' }) } }) }, value: 'kg', date: 'day', delta: true })
|
|
7452
|
-
*/
|
|
7453
6236
|
function chart(opts) {
|
|
7454
6237
|
return stampCollection("chart", opts.source, opts.label, {
|
|
7455
6238
|
kind: "chart",
|
|
@@ -7458,45 +6241,12 @@ function chart(opts) {
|
|
|
7458
6241
|
delta: opts.delta
|
|
7459
6242
|
});
|
|
7460
6243
|
}
|
|
7461
|
-
/**
|
|
7462
|
-
* A calendar heat map of event DENSITY rendered from a collection source — see
|
|
7463
|
-
* `stampCollection`. `date` is a part KEY inside each collection row, the same explicit
|
|
7464
|
-
* addressing `calendar`'s `date` and `chart`'s `value`/`date` use.
|
|
7465
|
-
*
|
|
7466
|
-
* `heatmap` counts ROWS PER DAY; it reads no value column at all, which is the whole
|
|
7467
|
-
* difference from `chart`: a chart plots what a row measured, a heat map plots how often
|
|
7468
|
-
* rows happened. That is why there is no `value` option here and why one must not be added
|
|
7469
|
-
* as a shortcut for "colour by amount" — a magnitude per day is a different question from a
|
|
7470
|
-
* count per day, and answering it through the same cells would make the picture ambiguous.
|
|
7471
|
-
*
|
|
7472
|
-
* The design sketched this as `f.heatmap({ source: collectionKey, … })`. There is no
|
|
7473
|
-
* `collectionKey` type and no resolver for one anywhere in the tree — the same correction
|
|
7474
|
-
* `f.chart` records above.
|
|
7475
|
-
*
|
|
7476
|
-
* @layer block
|
|
7477
|
-
* @base group
|
|
7478
|
-
* @prim —
|
|
7479
|
-
* @widget heatmap
|
|
7480
|
-
* @example f.heatmap({ source: { sessions: f.group({ scope: 'nest', multiple: true, fields: { day: f.date({ label: '…' }) } }) }, date: 'day' })
|
|
7481
|
-
*/
|
|
7482
6244
|
function heatmap(opts) {
|
|
7483
6245
|
return stampCollection("heatmap", opts.source, opts.label, {
|
|
7484
6246
|
kind: "heatmap",
|
|
7485
6247
|
date: opts.date
|
|
7486
6248
|
});
|
|
7487
|
-
}
|
|
7488
|
-
/**
|
|
7489
|
-
* A journal-style log rendered from a single collection source — see `stampCollection`.
|
|
7490
|
-
* `date` and `text` are part KEYS inside each collection row, NOT `LayoutEl` positions:
|
|
7491
|
-
* they address parts INSIDE the collection's own rows, which is not a slot a block lays
|
|
7492
|
-
* out itself.
|
|
7493
|
-
*
|
|
7494
|
-
* @layer block
|
|
7495
|
-
* @base group
|
|
7496
|
-
* @prim —
|
|
7497
|
-
* @widget journal
|
|
7498
|
-
* @example f.journal({ source: { entries: f.group({ scope: 'nest', multiple: true, fields: { day: f.date({ label: '…' }), note: f.text({ label: '…' }) } }) }, date: 'day', text: 'note' })
|
|
7499
|
-
*/
|
|
6249
|
+
}
|
|
7500
6250
|
function journal(opts) {
|
|
7501
6251
|
return stampCollection("journal", opts.source, opts.label, {
|
|
7502
6252
|
kind: "journal",
|
|
@@ -7504,14 +6254,6 @@ function journal(opts) {
|
|
|
7504
6254
|
text: opts.text
|
|
7505
6255
|
});
|
|
7506
6256
|
}
|
|
7507
|
-
/** A nutrition facts panel, with an optional leading energy value.
|
|
7508
|
-
*
|
|
7509
|
-
* @layer block
|
|
7510
|
-
* @base group
|
|
7511
|
-
* @prim —
|
|
7512
|
-
* @widget nutrition
|
|
7513
|
-
* @example f.nutrition({ fields: { fat: f.real({ label: '…' }) }, energy: { calories: f.real({ label: '…' }) } })
|
|
7514
|
-
*/
|
|
7515
6257
|
function nutrition(opts) {
|
|
7516
6258
|
const energy = opts.energy !== void 0 ? slot("nutrition", "energy", opts.energy) : void 0;
|
|
7517
6259
|
return {
|
|
@@ -7527,17 +6269,6 @@ function nutrition(opts) {
|
|
|
7527
6269
|
}
|
|
7528
6270
|
};
|
|
7529
6271
|
}
|
|
7530
|
-
/**
|
|
7531
|
-
* A specimen card: a title, an optional subtitle, and a set of condition facets. `view`
|
|
7532
|
-
* names each role (Task 5) — `conditions` is the ORDERED LIST of the condition fields' own
|
|
7533
|
-
* storage names, not a count against a flattened position.
|
|
7534
|
-
*
|
|
7535
|
-
* @layer block
|
|
7536
|
-
* @base group
|
|
7537
|
-
* @prim —
|
|
7538
|
-
* @widget specimen
|
|
7539
|
-
* @example f.specimen({ title: { name: f.string({ label: '…' }) }, conditions: { condition: f.string({ label: '…' }) } })
|
|
7540
|
-
*/
|
|
7541
6272
|
function specimen(opts) {
|
|
7542
6273
|
const title = slot("specimen", "title", opts.title);
|
|
7543
6274
|
const subtitle = opts.subtitle !== void 0 ? slot("specimen", "subtitle", opts.subtitle) : void 0;
|
|
@@ -7554,26 +6285,6 @@ function specimen(opts) {
|
|
|
7554
6285
|
}
|
|
7555
6286
|
};
|
|
7556
6287
|
}
|
|
7557
|
-
/**
|
|
7558
|
-
* Measurements against the range they were supposed to fall in, rendered from a single
|
|
7559
|
-
* collection source — see `stampCollection`. `analyte`, `value`, `unit`, `low` and `high` are
|
|
7560
|
-
* part KEYS inside each collection row, NOT `LayoutEl` positions — the same reasoning as
|
|
7561
|
-
* `manifest`'s `quantity`/`item`.
|
|
7562
|
-
*
|
|
7563
|
-
* The point is the COMPARISON. A `f.table` over the same rows prints the bounds as two more
|
|
7564
|
-
* columns and leaves the reader to do it; here each value sits on its own band with the
|
|
7565
|
-
* reference span marked on it, so "outside the range" is seen rather than worked out. That is
|
|
7566
|
-
* also the only thing on the row worth a colour — it is what the reader has to act on.
|
|
7567
|
-
*
|
|
7568
|
-
* `unit`, `low` and `high` are optional: a measurement with no published range (a culture, a
|
|
7569
|
-
* description) still belongs in the same list and simply gets no band.
|
|
7570
|
-
*
|
|
7571
|
-
* @layer block
|
|
7572
|
-
* @base group
|
|
7573
|
-
* @prim —
|
|
7574
|
-
* @widget assay
|
|
7575
|
-
* @example f.assay({ source: { results: f.group({ scope: 'nest', multiple: true, fields: { analyte: f.string({ label: '…' }), value: f.real({ label: '…' }) } }) }, analyte: 'analyte', value: 'value' })
|
|
7576
|
-
*/
|
|
7577
6288
|
function assay(opts) {
|
|
7578
6289
|
return stampCollection("assay", opts.source, opts.label, {
|
|
7579
6290
|
kind: "assay",
|
|
@@ -7584,32 +6295,6 @@ function assay(opts) {
|
|
|
7584
6295
|
...opts.high ? { high: opts.high } : {}
|
|
7585
6296
|
});
|
|
7586
6297
|
}
|
|
7587
|
-
/**
|
|
7588
|
-
* Value moving from one place to another, where the MOVEMENT is the subject — not one more
|
|
7589
|
-
* labelled row among the record's fields. The sum is set large in the machine voice, and the two
|
|
7590
|
-
* ends read as a path beneath it.
|
|
7591
|
-
*
|
|
7592
|
-
* amount — what moved. The one required role, and the reason the block exists
|
|
7593
|
-
* from — where it left. Absent on money that only arrived
|
|
7594
|
-
* to — where it arrived. Absent on money that only left
|
|
7595
|
-
* meta — the record's own remaining fields, under a rule: date, category, reference
|
|
7596
|
-
*
|
|
7597
|
-
* An absent end is not drawn, and that is the whole reading: money that left an account and
|
|
7598
|
-
* arrived nowhere IS an expense. It is read off which ends the record actually has, never
|
|
7599
|
-
* guessed from the shape of a value. What the movement MEANS beyond that — whether this
|
|
7600
|
-
* particular kind of transfer is good news — is carried by the classifier's own option `tone`,
|
|
7601
|
-
* the same declaration every other coloured value in the product uses.
|
|
7602
|
-
*
|
|
7603
|
-
* Distinct from `f.route`, which also has two ends: there the journey is the subject and the
|
|
7604
|
-
* ends are places, so it carries times and a duration; here the ends are accounts and the
|
|
7605
|
-
* subject is the quantity.
|
|
7606
|
-
*
|
|
7607
|
-
* @layer block
|
|
7608
|
-
* @base group
|
|
7609
|
-
* @prim —
|
|
7610
|
-
* @widget flow
|
|
7611
|
-
* @example f.flow({ amount: { amount: f.money({ label: '…' }) }, from: { source: f.relation({ label: '…' }) } })
|
|
7612
|
-
*/
|
|
7613
6298
|
function flow(opts) {
|
|
7614
6299
|
const amount = slot("flow", "amount", opts.amount);
|
|
7615
6300
|
const from = opts.from !== void 0 ? slot("flow", "from", opts.from) : void 0;
|
|
@@ -7629,17 +6314,6 @@ function flow(opts) {
|
|
|
7629
6314
|
}
|
|
7630
6315
|
};
|
|
7631
6316
|
}
|
|
7632
|
-
/**
|
|
7633
|
-
* A packing/cargo manifest rendered from a single collection source — see `stampCollection`.
|
|
7634
|
-
* `quantity` and `item` are part KEYS inside each collection row, NOT `LayoutEl` positions —
|
|
7635
|
-
* the same reasoning as `journal`'s `date`/`text`.
|
|
7636
|
-
*
|
|
7637
|
-
* @layer block
|
|
7638
|
-
* @base group
|
|
7639
|
-
* @prim —
|
|
7640
|
-
* @widget manifest
|
|
7641
|
-
* @example f.manifest({ source: { cargo: f.group({ scope: 'nest', multiple: true, fields: { item: f.string({ label: '…' }), qty: f.real({ label: '…' }) } }) }, quantity: 'qty', item: 'item' })
|
|
7642
|
-
*/
|
|
7643
6317
|
function manifest(opts) {
|
|
7644
6318
|
return stampCollection("manifest", opts.source, opts.label, {
|
|
7645
6319
|
kind: "manifest",
|
|
@@ -7647,26 +6321,6 @@ function manifest(opts) {
|
|
|
7647
6321
|
item: opts.item
|
|
7648
6322
|
});
|
|
7649
6323
|
}
|
|
7650
|
-
/**
|
|
7651
|
-
* A real table over a single collection source — see `stampCollection`.
|
|
7652
|
-
*
|
|
7653
|
-
* A plain collection (`f.row({ key, multiple })`) is a LAYOUT: repeated rows whose columns
|
|
7654
|
-
* line up. A table is an INSTRUMENT: its header sorts, `groupBy` buckets the rows, `totals`
|
|
7655
|
-
* closes it with an aggregate, `numbered` counts them. The server sees the same rows either
|
|
7656
|
-
* way — this factory declares no storage and changes none.
|
|
7657
|
-
*
|
|
7658
|
-
* `sort`, `groupBy` and every key of `totals` name a field of the ROW, exactly as `journal`'s
|
|
7659
|
-
* `date`/`text` and `manifest`'s `quantity`/`item` do: an explicit key, never a guess at which
|
|
7660
|
-
* column looks sortable or summable. `groupBy` needs no categorical filter — the list panel
|
|
7661
|
-
* groups a text field only when `views.groupBy` names it explicitly (`sdk/shelf.ts`), and this
|
|
7662
|
-
* IS that explicit naming.
|
|
7663
|
-
*
|
|
7664
|
-
* @layer block
|
|
7665
|
-
* @base group
|
|
7666
|
-
* @prim —
|
|
7667
|
-
* @widget table
|
|
7668
|
-
* @example f.table({ source: { rows: f.row({ scope: 'nest', multiple: true, fields: { a: f.string({ label: 'mod.fields.a' }) } }) }, sort: 'a' })
|
|
7669
|
-
*/
|
|
7670
6324
|
function table(opts) {
|
|
7671
6325
|
return stampCollection("table", opts.source, opts.label, {
|
|
7672
6326
|
kind: "table",
|
|
@@ -7680,28 +6334,6 @@ function table(opts) {
|
|
|
7680
6334
|
...opts.numbered ? { numbered: true } : {}
|
|
7681
6335
|
});
|
|
7682
6336
|
}
|
|
7683
|
-
/**
|
|
7684
|
-
* An identity document, drawn as the card it is: a caption and its number across the top, a
|
|
7685
|
-
* portrait beside the holder's own fields, the issuing details under a rule, and the
|
|
7686
|
-
* machine-readable strip at the foot. `view` names each role (Task 5) — see `masthead`'s own
|
|
7687
|
-
* doc comment for what an ordered name list means.
|
|
7688
|
-
*
|
|
7689
|
-
* overline — the caption at the top left ("PASSPORT", "DRIVING LICENCE")
|
|
7690
|
-
* number — the document's number, set apart at the top right
|
|
7691
|
-
* photo — the portrait. One field, rendered in a portrait frame beside the body
|
|
7692
|
-
* meta — the card's OWN fields, labelled, at each field's declared span
|
|
7693
|
-
* footer — the fields below the rule: issued, authority, record number
|
|
7694
|
-
* mrz — the machine-readable strip, set in monospace at the foot
|
|
7695
|
-
*
|
|
7696
|
-
* Every role is optional but `number`: a bank card has no `mrz`, a library card no `photo`,
|
|
7697
|
-
* and the card simply omits the part it was given nothing for.
|
|
7698
|
-
*
|
|
7699
|
-
* @layer block
|
|
7700
|
-
* @base group
|
|
7701
|
-
* @prim —
|
|
7702
|
-
* @widget idcard
|
|
7703
|
-
* @example f.idcard({ number: { number: f.string({ label: '…' }) }, meta: { issued: f.string({ label: '…' }) } })
|
|
7704
|
-
*/
|
|
7705
6337
|
function idcard(opts) {
|
|
7706
6338
|
const overline = opts.overline !== void 0 ? slot("idcard", "overline", opts.overline) : void 0;
|
|
7707
6339
|
const number = slot("idcard", "number", opts.number);
|
|
@@ -7723,16 +6355,6 @@ function idcard(opts) {
|
|
|
7723
6355
|
}
|
|
7724
6356
|
};
|
|
7725
6357
|
}
|
|
7726
|
-
/**
|
|
7727
|
-
* Properties inspector — a compact key-value metadata grid (columns default 2).
|
|
7728
|
-
* Formatted pairs with clear labels and concise values (like Notion / Linear property bars).
|
|
7729
|
-
*
|
|
7730
|
-
* @layer block
|
|
7731
|
-
* @base group
|
|
7732
|
-
* @prim —
|
|
7733
|
-
* @widget properties
|
|
7734
|
-
* @example f.properties({ columns: 2, fields: { a: f.string({ label: '…' }) } })
|
|
7735
|
-
*/
|
|
7736
6358
|
function properties(opts) {
|
|
7737
6359
|
return {
|
|
7738
6360
|
...group({
|
|
@@ -7747,14 +6369,6 @@ function properties(opts) {
|
|
|
7747
6369
|
}
|
|
7748
6370
|
};
|
|
7749
6371
|
}
|
|
7750
|
-
/** Vertical stack — 1-column full-width flow of fields.
|
|
7751
|
-
*
|
|
7752
|
-
* @layer block
|
|
7753
|
-
* @base group
|
|
7754
|
-
* @prim —
|
|
7755
|
-
* @widget stack
|
|
7756
|
-
* @example f.stack({ fields: { a: f.string({ label: '…' }) } })
|
|
7757
|
-
*/
|
|
7758
6372
|
function stack(opts) {
|
|
7759
6373
|
return {
|
|
7760
6374
|
...group({
|
|
@@ -7766,7 +6380,6 @@ function stack(opts) {
|
|
|
7766
6380
|
view: { kind: "stack" }
|
|
7767
6381
|
};
|
|
7768
6382
|
}
|
|
7769
|
-
/** The block factories, merged into the `f` namespace beside row/table/sheet — see fields.ts. */
|
|
7770
6383
|
var blocks = {
|
|
7771
6384
|
bento,
|
|
7772
6385
|
split,
|
|
@@ -8550,7 +7163,7 @@ var require_data = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
8550
7163
|
};
|
|
8551
7164
|
}));
|
|
8552
7165
|
//#endregion
|
|
8553
|
-
//#region ../sdk/
|
|
7166
|
+
//#region ../sdk/dist/fields/constants.js
|
|
8554
7167
|
var import_src = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
8555
7168
|
var LANGUAGES_LIST = require_data();
|
|
8556
7169
|
var LANGUAGES = {};
|
|
@@ -8606,58 +7219,10 @@ import_src.default.getAllCodes().map((code) => ({
|
|
|
8606
7219
|
name: import_src.default.getNativeName(code)
|
|
8607
7220
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
8608
7221
|
//#endregion
|
|
8609
|
-
//#region ../sdk/
|
|
8610
|
-
/**
|
|
8611
|
-
* The field type system — the heart of the platform. A pure, isomorphic module.
|
|
8612
|
-
* One field description → three consumers:
|
|
8613
|
-
* 1. `zod` — validation (identical on server and client)
|
|
8614
|
-
* 2. `column` — the DB column type
|
|
8615
|
-
* 3. `prim`/`kind` — keys for resolving the renderer (see web/render/registry)
|
|
8616
|
-
*/
|
|
8617
|
-
/**
|
|
8618
|
-
* Does this node carry a value of its own? True for a leaf field, for a static display
|
|
8619
|
-
* value, and for a composite — a composite's parts do not replace its own meta.
|
|
8620
|
-
*/
|
|
7222
|
+
//#region ../sdk/dist/fields.js
|
|
8621
7223
|
var hasValue = (x) => x.type !== void 0 && typeof x.type === "object";
|
|
8622
|
-
/**
|
|
8623
|
-
* Does this node carry children? True for every container and for a composite. An EMPTY
|
|
8624
|
-
* `fields` still counts: a row with no cells is a container that happens to hold nothing,
|
|
8625
|
-
* not a leaf.
|
|
8626
|
-
*/
|
|
8627
7224
|
var hasChildren = (x) => Array.isArray(x.fields);
|
|
8628
|
-
/**
|
|
8629
|
-
* Is this node an ORDINARY NAMED FIELD — the case almost every call site actually means by
|
|
8630
|
-
* "a field", as opposed to a keyless display value (a computed value with no storage) or a
|
|
8631
|
-
* container? `hasValue(x) && !hasChildren(x) && x.key !== undefined` is this exact test,
|
|
8632
|
-
* spelled out by hand at some twenty call sites before this predicate existed.
|
|
8633
|
-
*
|
|
8634
|
-
* That duplication is why this predicate exists: today `hasValue(x) && !hasChildren(x)`
|
|
8635
|
-
* alone answers the same question, because no composite in today's composed schema is a
|
|
8636
|
-
* both-node (see `LayoutNode`'s own doc comment above). The day one exists, a hand-spelled
|
|
8637
|
-
* `hasValue && !hasChildren` silently reclassifies it as a container, while this predicate
|
|
8638
|
-
* — which also checks `key` — still has to be taught what to do with it. One place to fix
|
|
8639
|
-
* instead of twenty. See docs/decisions.md, 2026-08-26, "HAZARD for whoever produces the
|
|
8640
|
-
* first both-node".
|
|
8641
|
-
*/
|
|
8642
7225
|
var isNamedField = (x) => hasValue(x) && !hasChildren(x) && x.key !== void 0;
|
|
8643
|
-
/**
|
|
8644
|
-
* A visual or storage group of fields, nestable to any depth — a group inside a group
|
|
8645
|
-
* inside a group, columns expanding recursively (`key__child__grandchild__…`, see
|
|
8646
|
-
* `checkStorage` in `compose.ts`). There is no level count: the only ceiling is the
|
|
8647
|
-
* identifier budget PostgreSQL imposes on a generated name, checked at composition time
|
|
8648
|
-
* (`MAX_COLUMN_NAME`/`MAX_TABLE_NAME`, `packages/sdk/src/compose.ts`), not a depth guard.
|
|
8649
|
-
* `scope` decides which: `'hoist'` (the default) arranges children inside the PARENT's
|
|
8650
|
-
* scope and stores nothing of its own; `'nest'` owns a scope and is stored —
|
|
8651
|
-
* - embedded (no multiple): a nested object over `key__sub` columns;
|
|
8652
|
-
* - collection (multiple): a child table (array of rows).
|
|
8653
|
-
* A 'nest' group is NAMED BY ITS PARENT — the property it is declared under — and takes
|
|
8654
|
-
* only NAMED children (each with a storage key of its own).
|
|
8655
|
-
*
|
|
8656
|
-
* @layer primitive
|
|
8657
|
-
* @prim —
|
|
8658
|
-
* @widget group
|
|
8659
|
-
* @example f.group({ scope: 'nest', label: 'mod.fields.contact', fields: { name: f.string({}) } })
|
|
8660
|
-
*/
|
|
8661
7226
|
function group(o) {
|
|
8662
7227
|
if (o["view"] !== void 0) throw new Error("[group] `view` is gone — presentational options live in `ui: {}` (see CLAUDE.md § field author options and the field-types skill).");
|
|
8663
7228
|
const r = o.rules ?? {};
|
|
@@ -8688,21 +7253,6 @@ function group(o) {
|
|
|
8688
7253
|
fields
|
|
8689
7254
|
};
|
|
8690
7255
|
}
|
|
8691
|
-
/**
|
|
8692
|
-
* Layout group, flow nowrap + horizontal scroll (single line). Sugar over group().
|
|
8693
|
-
*
|
|
8694
|
-
* `multiple` is an ordinary modifier here as everywhere else: `f.row({ scope: 'nest',
|
|
8695
|
-
* multiple: true })` is a COLLECTION of rows, and the renderer aligns its columns (a repeated row is what a
|
|
8696
|
-
* table is). There is no separate factory and no separate `display` value to DECLARE a
|
|
8697
|
-
* tabular collection — `f.table` (`blocks.ts`) is a VIEW stamped onto a collection built
|
|
8698
|
-
* here, never a second way to declare one.
|
|
8699
|
-
*
|
|
8700
|
-
* @layer primitive
|
|
8701
|
-
* @prim —
|
|
8702
|
-
* @widget group
|
|
8703
|
-
* @example f.row({ label: 'mod.fields.row', fields: {} })
|
|
8704
|
-
* @example f.row({ scope: 'nest', label: 'mod.fields.items', multiple: true, fields: {} })
|
|
8705
|
-
*/
|
|
8706
7256
|
function row(o) {
|
|
8707
7257
|
return group({
|
|
8708
7258
|
scope: o.scope,
|
|
@@ -8714,22 +7264,6 @@ function row(o) {
|
|
|
8714
7264
|
ui: { display: "scroll" }
|
|
8715
7265
|
});
|
|
8716
7266
|
}
|
|
8717
|
-
/**
|
|
8718
|
-
* Layout group with a 2D layout (rows×columns → aligned CSS grid). Sugar over group().
|
|
8719
|
-
*
|
|
8720
|
-
* A ROW IS A GROUP. `fields` is a map of rows, each row a map of its cells: the outer
|
|
8721
|
-
* property names the row, the inner ones name the cells (their storage keys, like any
|
|
8722
|
-
* other named child). Each row materializes into a nested `scope: 'hoist'` group, so a
|
|
8723
|
-
* row boundary is the nesting itself and the renderer reads `group.fields` as its rows
|
|
8724
|
-
* instead of scanning a flat list for separator markers. The cells still belong to the
|
|
8725
|
-
* PARENT's scope — a hoist group owns none — so the columns are exactly the ones the flat
|
|
8726
|
-
* form produced, an empty row included (a row group with no cells).
|
|
8727
|
-
*
|
|
8728
|
-
* @layer primitive
|
|
8729
|
-
* @prim —
|
|
8730
|
-
* @widget group
|
|
8731
|
-
* @example f.sheet({ label: 'mod.fields.sheet', fields: { net: { ip: f.string({}) }, hw: { mac: f.string({}) } } })
|
|
8732
|
-
*/
|
|
8733
7267
|
function sheet(o) {
|
|
8734
7268
|
const rows = Object.fromEntries(Object.entries(o.fields).map(([name, cells]) => [name, group({
|
|
8735
7269
|
scope: "hoist",
|
|
@@ -8742,17 +7276,6 @@ function sheet(o) {
|
|
|
8742
7276
|
ui: { display: "sheet" }
|
|
8743
7277
|
});
|
|
8744
7278
|
}
|
|
8745
|
-
/**
|
|
8746
|
-
* Composite URL — embedded-group sugar. Storage: separate nested columns
|
|
8747
|
-
* (`key__scheme`, `key__host`, `key__port`, …) in the same table. The renderer
|
|
8748
|
-
* is custom (a single link/input, `kind:'url'`) — string↔parts via
|
|
8749
|
-
* parseUrl/buildUrl. Partial/invalid URLs are stored by parts.
|
|
8750
|
-
*
|
|
8751
|
-
* @layer primitive
|
|
8752
|
-
* @prim —
|
|
8753
|
-
* @widget url
|
|
8754
|
-
* @example f.url({ label: 'mod.fields.website' })
|
|
8755
|
-
*/
|
|
8756
7279
|
function url(o) {
|
|
8757
7280
|
return group({
|
|
8758
7281
|
scope: "nest",
|
|
@@ -8770,16 +7293,6 @@ function url(o) {
|
|
|
8770
7293
|
}
|
|
8771
7294
|
});
|
|
8772
7295
|
}
|
|
8773
|
-
/**
|
|
8774
|
-
* Keyed collection: one row per value of a single named field of that row — `by` names
|
|
8775
|
-
* which one (default: the first), and its own name is the row's storage key. See the block
|
|
8776
|
-
* comment above for the full contract (container, fixed, unique).
|
|
8777
|
-
*
|
|
8778
|
-
* @layer primitive
|
|
8779
|
-
* @prim —
|
|
8780
|
-
* @widget group
|
|
8781
|
-
* @example f.keyed({ label: 'mod.fields.translations', fields: { lang: f.string({ options: ['en', 'uk'] }) } })
|
|
8782
|
-
*/
|
|
8783
7296
|
function keyed(o) {
|
|
8784
7297
|
const entries = Object.entries(o.fields);
|
|
8785
7298
|
if (entries.length === 0) throw new Error(`[field.keyed] fields must declare at least the key field`);
|
|
@@ -8815,58 +7328,18 @@ function keyed(o) {
|
|
|
8815
7328
|
}
|
|
8816
7329
|
var isCollectionGroup = (g) => g.scope === "nest" && g.multiple === true;
|
|
8817
7330
|
var isEmbeddedGroup = (g) => g.scope === "nest" && g.multiple !== true;
|
|
8818
|
-
/**
|
|
8819
|
-
* Horizontal divider, optionally labeled (a section heading).
|
|
8820
|
-
*
|
|
8821
|
-
* @layer primitive
|
|
8822
|
-
* @prim —
|
|
8823
|
-
* @widget divider
|
|
8824
|
-
* @example f.divider({ label: 'mod.fields.section' })
|
|
8825
|
-
*/
|
|
8826
7331
|
function divider(o) {
|
|
8827
7332
|
return {
|
|
8828
7333
|
el: "divider",
|
|
8829
7334
|
label: o?.label
|
|
8830
7335
|
};
|
|
8831
7336
|
}
|
|
8832
|
-
/**
|
|
8833
|
-
* Reference markdown block by i18n key.
|
|
8834
|
-
*
|
|
8835
|
-
* The key is resolved WITH the variables of the running instance, so the locale string
|
|
8836
|
-
* may name facts that are not constants: `{{origin}}` is this instance's own address
|
|
8837
|
-
* (`https://coffer.example`), which is how a block documents an endpoint without
|
|
8838
|
-
* freezing a developer's `localhost` into every translation. The set is fixed and
|
|
8839
|
-
* ambient — the block declares only its key. See `web-ui/src/render/info-text.ts`.
|
|
8840
|
-
*
|
|
8841
|
-
* @layer primitive
|
|
8842
|
-
* @prim —
|
|
8843
|
-
* @widget info
|
|
8844
|
-
* @example f.info('mod.fields.infoText')
|
|
8845
|
-
*/
|
|
8846
7337
|
function info(textKey) {
|
|
8847
7338
|
return {
|
|
8848
7339
|
el: "info",
|
|
8849
7340
|
text: textKey
|
|
8850
7341
|
};
|
|
8851
7342
|
}
|
|
8852
|
-
/**
|
|
8853
|
-
* What else points AT this record — the inverse of a relation, which is often the more useful
|
|
8854
|
-
* direction on a record page: a box is more usefully "what is in it" than "what it is in".
|
|
8855
|
-
*
|
|
8856
|
-
* Stores NOTHING and has no column: it declares where to look, and the server answers by asking
|
|
8857
|
-
* the pointing shelf. That is why it is a pseudo-element beside `divider` and `info` rather than
|
|
8858
|
-
* a field — there is no value here to validate or save.
|
|
8859
|
-
*
|
|
8860
|
-
* `from` names the pointing side explicitly (which shelf, and WHICH of its fields), never
|
|
8861
|
-
* "everything that happens to point here": a shelf may point at the same target through two
|
|
8862
|
-
* fields — a transaction has a source account and a destination account — and a panel that
|
|
8863
|
-
* merged them would answer a question nobody asked.
|
|
8864
|
-
*
|
|
8865
|
-
* @layer primitive
|
|
8866
|
-
* @prim —
|
|
8867
|
-
* @widget backrefs
|
|
8868
|
-
* @example f.backrefs({ label: 'mod.fields.storedHere', from: { library: 'things', shelf: 'item', field: 'location' } })
|
|
8869
|
-
*/
|
|
8870
7343
|
function backrefs(o) {
|
|
8871
7344
|
return {
|
|
8872
7345
|
el: "backrefs",
|
|
@@ -8874,14 +7347,6 @@ function backrefs(o) {
|
|
|
8874
7347
|
from: o.from
|
|
8875
7348
|
};
|
|
8876
7349
|
}
|
|
8877
|
-
/**
|
|
8878
|
-
* Action button: invokes the handler registered in actionRegistry under the key `value`.
|
|
8879
|
-
*
|
|
8880
|
-
* @layer primitive
|
|
8881
|
-
* @prim —
|
|
8882
|
-
* @widget button
|
|
8883
|
-
* @example f.button({ label: 'mod.fields.run', value: 'runAction' })
|
|
8884
|
-
*/
|
|
8885
7350
|
function button(o) {
|
|
8886
7351
|
return {
|
|
8887
7352
|
el: "button",
|
|
@@ -8891,38 +7356,14 @@ function button(o) {
|
|
|
8891
7356
|
variant: o.variant
|
|
8892
7357
|
};
|
|
8893
7358
|
}
|
|
8894
|
-
/**
|
|
8895
|
-
* Brand for `fromShelf()` — a private key, never producible by a plain object literal, so
|
|
8896
|
-
* `isShelfSource` tells a dynamic options source apart from a literal array or a named-list
|
|
8897
|
-
* string CHEAPLY (one property read) and WITHOUT guessing from shape (`{library,shelf}`
|
|
8898
|
-
* alone is not enough: a plugin's own option-list item could coincidentally carry those
|
|
8899
|
-
* keys). Not exported — callers only ever get a `ShelfOptionsSource` back from `fromShelf`.
|
|
8900
|
-
*/
|
|
8901
7359
|
var SHELF_SOURCE = Symbol("shelfSource");
|
|
8902
|
-
/** Runtime discriminator for `ShelfOptionsSource` — see `SHELF_SOURCE`'s own doc comment. */
|
|
8903
7360
|
function isShelfSource(v) {
|
|
8904
7361
|
return typeof v === "object" && v !== null && v[SHELF_SOURCE] === true;
|
|
8905
7362
|
}
|
|
8906
7363
|
function resolveDate(v) {
|
|
8907
7364
|
return v === "today" ? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) : v;
|
|
8908
7365
|
}
|
|
8909
|
-
/**
|
|
8910
|
-
* Does this field offer preset values? Not `options.length`: a field whose options come
|
|
8911
|
-
* from a NAMED source carries an empty array until composeRegistry resolves it, and every
|
|
8912
|
-
* consumer that asks the question earlier (a `keyed({fixed})` check at declaration time, a
|
|
8913
|
-
* role's `accepts`) would get the wrong answer.
|
|
8914
|
-
*/
|
|
8915
7366
|
var hasOptions = (f) => (f.options?.length ?? 0) > 0 || f.hints["source"] != null;
|
|
8916
|
-
/**
|
|
8917
|
-
* `hasOptions`'s own companion for phase 1 (a DECLARATION, not a built meta) — the same
|
|
8918
|
-
* question ENUMERATED's `test` asks further down. Now that `options` means the same thing
|
|
8919
|
-
* for every type — a literal list, a named list, or `fromShelf(...)` all describe where
|
|
8920
|
-
* this field's choices come from — the question is exactly "is `options` present?", nothing
|
|
8921
|
-
* more: a `relation` field's choices come from records, which is as much an answer as a
|
|
8922
|
-
* literal array is. No separate `opts.source` to check either: composeRegistry only
|
|
8923
|
-
* resolves the two STATIC forms into values, but `opts.options` itself is set at
|
|
8924
|
-
* declaration time regardless of which form it is.
|
|
8925
|
-
*/
|
|
8926
7367
|
var declHasOptions = (d) => d.opts.options != null;
|
|
8927
7368
|
registerType("string", {
|
|
8928
7369
|
kind: "text",
|
|
@@ -8947,18 +7388,9 @@ registerType("string", {
|
|
|
8947
7388
|
};
|
|
8948
7389
|
}
|
|
8949
7390
|
});
|
|
8950
|
-
/**
|
|
8951
|
-
* Single-line text. Arbitrary pattern/messageKey — via `rules`. Multi-line — `text()`.
|
|
8952
|
-
*
|
|
8953
|
-
* @layer primitive
|
|
8954
|
-
* @prim text
|
|
8955
|
-
* @widget text
|
|
8956
|
-
* @example f.string({ label: 'mod.fields.name' })
|
|
8957
|
-
*/
|
|
8958
7391
|
function string(o) {
|
|
8959
7392
|
return declare("string", o);
|
|
8960
7393
|
}
|
|
8961
|
-
/** Shared by localDir/localFile's build(): a server path, judged only when one was entered. */
|
|
8962
7394
|
function pathZod(_o) {
|
|
8963
7395
|
return stringContent((s) => s);
|
|
8964
7396
|
}
|
|
@@ -8975,14 +7407,6 @@ registerType("localDir", {
|
|
|
8975
7407
|
};
|
|
8976
7408
|
}
|
|
8977
7409
|
});
|
|
8978
|
-
/**
|
|
8979
|
-
* Path to a DIRECTORY on the server (autocomplete, within home only).
|
|
8980
|
-
*
|
|
8981
|
-
* @layer primitive
|
|
8982
|
-
* @prim text
|
|
8983
|
-
* @widget localDir
|
|
8984
|
-
* @example f.localDir({ label: 'mod.fields.path' })
|
|
8985
|
-
*/
|
|
8986
7410
|
function localDir(o) {
|
|
8987
7411
|
return declare("localDir", o);
|
|
8988
7412
|
}
|
|
@@ -9000,14 +7424,6 @@ registerType("localFile", {
|
|
|
9000
7424
|
};
|
|
9001
7425
|
}
|
|
9002
7426
|
});
|
|
9003
|
-
/**
|
|
9004
|
-
* Path to a FILE on the server (autocomplete; config.exts filters extensions).
|
|
9005
|
-
*
|
|
9006
|
-
* @layer primitive
|
|
9007
|
-
* @prim text
|
|
9008
|
-
* @widget localFile
|
|
9009
|
-
* @example f.localFile({ label: 'mod.fields.path' })
|
|
9010
|
-
*/
|
|
9011
7427
|
function localFile(o) {
|
|
9012
7428
|
return declare("localFile", o);
|
|
9013
7429
|
}
|
|
@@ -9019,14 +7435,6 @@ registerType("text", {
|
|
|
9019
7435
|
return typeOf("string").build(o, []);
|
|
9020
7436
|
}
|
|
9021
7437
|
});
|
|
9022
|
-
/**
|
|
9023
|
-
* Multi-line text (`kind: 'textarea'`). No `multiple`.
|
|
9024
|
-
*
|
|
9025
|
-
* @layer primitive
|
|
9026
|
-
* @prim text
|
|
9027
|
-
* @widget textarea
|
|
9028
|
-
* @example f.text({ label: 'mod.fields.notes' })
|
|
9029
|
-
*/
|
|
9030
7438
|
function text(o) {
|
|
9031
7439
|
return declare("text", o);
|
|
9032
7440
|
}
|
|
@@ -9038,17 +7446,6 @@ registerType("i18n", {
|
|
|
9038
7446
|
return typeOf("text").build(o, []);
|
|
9039
7447
|
}
|
|
9040
7448
|
});
|
|
9041
|
-
/**
|
|
9042
|
-
* Translatable text (kind 'i18n'). Stores a string — an i18n key OR a literal.
|
|
9043
|
-
* In view it shows `t(value, { defaultValue: value })`: a known key → translation,
|
|
9044
|
-
* any other text → shown as-is. Edit — raw input (key/literal without t()).
|
|
9045
|
-
* Built-in reference catalogs write a key; user records — a plain literal.
|
|
9046
|
-
*
|
|
9047
|
-
* @layer primitive
|
|
9048
|
-
* @prim text
|
|
9049
|
-
* @widget i18n
|
|
9050
|
-
* @example f.i18n({ label: 'mod.fields.title' })
|
|
9051
|
-
*/
|
|
9052
7449
|
function i18n(o) {
|
|
9053
7450
|
return declare("i18n", o);
|
|
9054
7451
|
}
|
|
@@ -9060,14 +7457,6 @@ registerType("smallText", {
|
|
|
9060
7457
|
return typeOf("text").build(o, []);
|
|
9061
7458
|
}
|
|
9062
7459
|
});
|
|
9063
|
-
/**
|
|
9064
|
-
* Multi-line text, scalar size (narrow column ~2 rows). Full-width — `text()`.
|
|
9065
|
-
*
|
|
9066
|
-
* @layer primitive
|
|
9067
|
-
* @prim text
|
|
9068
|
-
* @widget smalltext
|
|
9069
|
-
* @example f.smallText({ label: 'mod.fields.summary' })
|
|
9070
|
-
*/
|
|
9071
7460
|
function smallText(o) {
|
|
9072
7461
|
return declare("smallText", o);
|
|
9073
7462
|
}
|
|
@@ -9091,14 +7480,6 @@ registerType("real", {
|
|
|
9091
7480
|
};
|
|
9092
7481
|
}
|
|
9093
7482
|
});
|
|
9094
|
-
/**
|
|
9095
|
-
* Real number (column 'real'). min/max/step — via `rules`.
|
|
9096
|
-
*
|
|
9097
|
-
* @layer primitive
|
|
9098
|
-
* @prim number
|
|
9099
|
-
* @widget number
|
|
9100
|
-
* @example f.real({ label: 'mod.fields.weight' })
|
|
9101
|
-
*/
|
|
9102
7483
|
function real(o) {
|
|
9103
7484
|
return declare("real", o);
|
|
9104
7485
|
}
|
|
@@ -9122,14 +7503,6 @@ registerType("int", {
|
|
|
9122
7503
|
};
|
|
9123
7504
|
}
|
|
9124
7505
|
});
|
|
9125
|
-
/**
|
|
9126
|
-
* Integer (column 'integer'). min/max/step — via `rules`.
|
|
9127
|
-
*
|
|
9128
|
-
* @layer primitive
|
|
9129
|
-
* @prim number
|
|
9130
|
-
* @widget number
|
|
9131
|
-
* @example f.int({ label: 'mod.fields.count' })
|
|
9132
|
-
*/
|
|
9133
7506
|
function int(o) {
|
|
9134
7507
|
return declare("int", o);
|
|
9135
7508
|
}
|
|
@@ -9183,14 +7556,6 @@ registerType("date", {
|
|
|
9183
7556
|
};
|
|
9184
7557
|
}
|
|
9185
7558
|
});
|
|
9186
|
-
/**
|
|
9187
|
-
* Date value with configurable granularity ('day' | 'month' | 'year', default 'day').
|
|
9188
|
-
*
|
|
9189
|
-
* @layer primitive
|
|
9190
|
-
* @prim date
|
|
9191
|
-
* @widget date
|
|
9192
|
-
* @example f.date({ label: 'mod.fields.purchaseDate' })
|
|
9193
|
-
*/
|
|
9194
7559
|
function date(o) {
|
|
9195
7560
|
return declare("date", o);
|
|
9196
7561
|
}
|
|
@@ -9212,15 +7577,6 @@ registerType("time", {
|
|
|
9212
7577
|
};
|
|
9213
7578
|
}
|
|
9214
7579
|
});
|
|
9215
|
-
/**
|
|
9216
|
-
* Time-of-day value with configurable granularity ('hour' | 'minute' | 'second', default
|
|
9217
|
-
* 'minute' — the same default `f.datetime` has always had).
|
|
9218
|
-
*
|
|
9219
|
-
* @layer primitive
|
|
9220
|
-
* @prim time
|
|
9221
|
-
* @widget time
|
|
9222
|
-
* @example f.time({ label: 'mod.fields.startTime' })
|
|
9223
|
-
*/
|
|
9224
7580
|
function time(o) {
|
|
9225
7581
|
return declare("time", o);
|
|
9226
7582
|
}
|
|
@@ -9241,14 +7597,6 @@ registerType("datetime", {
|
|
|
9241
7597
|
};
|
|
9242
7598
|
}
|
|
9243
7599
|
});
|
|
9244
|
-
/**
|
|
9245
|
-
* Combined date-and-time value with configurable granularity ('hour' | 'minute' | 'second', default 'minute').
|
|
9246
|
-
*
|
|
9247
|
-
* @layer primitive
|
|
9248
|
-
* @prim datetime
|
|
9249
|
-
* @widget datetime
|
|
9250
|
-
* @example f.datetime({ label: 'mod.fields.startedAt' })
|
|
9251
|
-
*/
|
|
9252
7600
|
function datetime(o) {
|
|
9253
7601
|
return declare("datetime", o);
|
|
9254
7602
|
}
|
|
@@ -9265,15 +7613,6 @@ registerType("boolean", {
|
|
|
9265
7613
|
};
|
|
9266
7614
|
}
|
|
9267
7615
|
});
|
|
9268
|
-
/**
|
|
9269
|
-
* Boolean value rendered as a checkbox; defaults to false, nullable in storage (an
|
|
9270
|
-
* unchanged collection row sends `null`, not `undefined`).
|
|
9271
|
-
*
|
|
9272
|
-
* @layer primitive
|
|
9273
|
-
* @prim checkbox
|
|
9274
|
-
* @widget boolean
|
|
9275
|
-
* @example f.boolean({ label: 'mod.fields.active' })
|
|
9276
|
-
*/
|
|
9277
7616
|
function boolean(o) {
|
|
9278
7617
|
return declare("boolean", o);
|
|
9279
7618
|
}
|
|
@@ -9294,14 +7633,6 @@ registerType("triState", {
|
|
|
9294
7633
|
};
|
|
9295
7634
|
}
|
|
9296
7635
|
});
|
|
9297
|
-
/**
|
|
9298
|
-
* Three-way choice: 'yes' / 'no' / 'unknown'.
|
|
9299
|
-
*
|
|
9300
|
-
* @layer primitive
|
|
9301
|
-
* @prim triState
|
|
9302
|
-
* @widget triState
|
|
9303
|
-
* @example f.triState({ label: 'mod.fields.reviewed' })
|
|
9304
|
-
*/
|
|
9305
7636
|
function triState(o) {
|
|
9306
7637
|
return declare("triState", o);
|
|
9307
7638
|
}
|
|
@@ -9319,60 +7650,12 @@ registerType("stepper", {
|
|
|
9319
7650
|
};
|
|
9320
7651
|
}
|
|
9321
7652
|
});
|
|
9322
|
-
/**
|
|
9323
|
-
* The REACHED stage of an ORDERED set of options. ONE value, so a gap is UNREPRESENTABLE
|
|
9324
|
-
* rather than merely rejected: reaching stage 3 IS stages 1 and 2 having happened, and
|
|
9325
|
-
* "3 but not 2" cannot be written down at all — there is no sequential-gate rule anywhere,
|
|
9326
|
-
* in zod or in the UI, because there is nothing to gate. That is the whole difference from
|
|
9327
|
-
* `f.string({ options, strict })`, whose options are PEERS (a weekday, a category), and
|
|
9328
|
-
* from `f.check`, whose boxes tick independently. An empty value is the legitimate
|
|
9329
|
-
* "not started" state, and going back a stage is legitimate too: the field stores where
|
|
9330
|
-
* the record IS, never a history of how it got there.
|
|
9331
|
-
*
|
|
9332
|
-
* STORAGE — the stage's own VALUE, not its index, and which one it is matters because the
|
|
9333
|
-
* two fail differently. An index survives renaming a stage and breaks on a reorder or an
|
|
9334
|
-
* insertion; a value survives a reorder and breaks on a rename. Inserting a stage into the
|
|
9335
|
-
* middle of the set ('customs' between 'shipped' and 'delivered') is the ORDINARY edit of a
|
|
9336
|
-
* domain that grows, and under index storage it silently reinterprets every row already
|
|
9337
|
-
* stored — corruption with no symptom anywhere. A rename is the rarer edit, it leaves a
|
|
9338
|
-
* value the option set no longer contains, and THAT state is loud at both ends: `strict`
|
|
9339
|
-
* (forced, below) rejects the next write of it, and the renderer draws it as an unknown
|
|
9340
|
-
* stage rather than guessing a position for it or clamping it to a neighbour. A rename is
|
|
9341
|
-
* also the case the tree already has a migration for (`TableOps.fill`/`convert`, skill
|
|
9342
|
-
* `data-migrations`); nothing in it renumbers stored rows after an insertion. So: the
|
|
9343
|
-
* stored value is the option's `value`, and the ORDER is `options`' declaration order.
|
|
9344
|
-
*
|
|
9345
|
-
* What "loud" costs, stated plainly because this field advertises it: `strict` rejects on
|
|
9346
|
-
* WRITE, so a record carrying a stale stage cannot be saved AT ALL until the stage is
|
|
9347
|
-
* repaired — not even for an unrelated edit to another field. That is ordinary strict-field
|
|
9348
|
-
* behaviour rather than anything new here, but on a field whose whole promise is failing
|
|
9349
|
-
* loudly it is the half of the promise that costs something.
|
|
9350
|
-
*
|
|
9351
|
-
* `strict` is forced on and is not the author's to choose — a value outside the ordered set
|
|
9352
|
-
* has no position in it, so it can never be a legitimate entry. Never `multiple`: a record
|
|
9353
|
-
* is at exactly one stage.
|
|
9354
|
-
*
|
|
9355
|
-
* @layer primitive
|
|
9356
|
-
* @prim text
|
|
9357
|
-
* @widget stepper
|
|
9358
|
-
* @example f.stepper({ label: 'mod.fields.stage', options: ['ordered', 'shipped', 'delivered'] })
|
|
9359
|
-
*/
|
|
9360
7653
|
function stepper(o) {
|
|
9361
7654
|
return declare("stepper", {
|
|
9362
7655
|
...o,
|
|
9363
7656
|
strict: true
|
|
9364
7657
|
});
|
|
9365
7658
|
}
|
|
9366
|
-
/**
|
|
9367
|
-
* Reject anything beyond a flat equality map: an operator key (`$in`, `$and`, …), an
|
|
9368
|
-
* object-valued entry (which is how a nested operator like `{ type: { $in: [...] } }`
|
|
9369
|
-
* would smuggle itself in), or an explicit `undefined` value all throw, naming the
|
|
9370
|
-
* offending key. `undefined` is rejected alongside objects because it would otherwise
|
|
9371
|
-
* slip through every later layer unnoticed: `Condition` types it as a valid value,
|
|
9372
|
-
* `conditionKeys` skips it so compose validates nothing against the target shelf, and
|
|
9373
|
-
* the picker's query-string builder turns it into the literal string `'undefined'`
|
|
9374
|
-
* (an empty picker with no error anywhere).
|
|
9375
|
-
*/
|
|
9376
7659
|
function assertFlatEqualityFilter(filter) {
|
|
9377
7660
|
for (const [key, value] of Object.entries(filter)) {
|
|
9378
7661
|
if (key.startsWith("$")) throw new Error(`[relation] filter key '${key}' is an operator — only flat equality is allowed here`);
|
|
@@ -9415,17 +7698,6 @@ registerType("relation", {
|
|
|
9415
7698
|
};
|
|
9416
7699
|
}
|
|
9417
7700
|
});
|
|
9418
|
-
/**
|
|
9419
|
-
* Link to records of another shelf (`{library, shelf}`). Stores an integer id (single)
|
|
9420
|
-
* or a JSON array of integer ids (multiple). Rendered as RecordInline + a record picker.
|
|
9421
|
-
* This used to be the record branch of `select()`; split out into a separate primitive
|
|
9422
|
-
* so select stays a pure enum/source list.
|
|
9423
|
-
*
|
|
9424
|
-
* @layer primitive
|
|
9425
|
-
* @prim relation
|
|
9426
|
-
* @widget relation
|
|
9427
|
-
* @example f.relation({ label: 'mod.fields.owner', options: fromShelf({ library: 'people', shelf: 'person' }) })
|
|
9428
|
-
*/
|
|
9429
7701
|
function relation(o) {
|
|
9430
7702
|
return declare("relation", o);
|
|
9431
7703
|
}
|
|
@@ -9452,49 +7724,13 @@ registerType("json", {
|
|
|
9452
7724
|
};
|
|
9453
7725
|
}
|
|
9454
7726
|
});
|
|
9455
|
-
/**
|
|
9456
|
-
* Arbitrary JSON value: a native object/array, or a string parsed as JSON.
|
|
9457
|
-
*
|
|
9458
|
-
* @layer primitive
|
|
9459
|
-
* @prim json
|
|
9460
|
-
* @widget json
|
|
9461
|
-
* @example f.json({ label: 'mod.fields.raw' })
|
|
9462
|
-
*/
|
|
9463
7727
|
function json(o) {
|
|
9464
7728
|
return declare("json", o);
|
|
9465
7729
|
}
|
|
9466
|
-
/**
|
|
9467
|
-
* A content role accepts whatever the author declared, because for `check` the author's
|
|
9468
|
-
* field IS the filling: there is no second override channel to correct one AGAINST (see
|
|
9469
|
-
* `contentRole` below — both branches of its `normalize` hand back the same declaration).
|
|
9470
|
-
*
|
|
9471
|
-
* It used to demand a field owning ONE column, on the grounds that "a composite inside a
|
|
9472
|
-
* composite would need `key__sub__sub`, which nothing on the flatten/nest path builds".
|
|
9473
|
-
* `partColumns` builds it now, and the flatten/nest walks follow it — so the ground is
|
|
9474
|
-
* gone and with it the only thing this role ever refused.
|
|
9475
|
-
*/
|
|
9476
7730
|
var ANY_CONTENT = {
|
|
9477
7731
|
test: () => true,
|
|
9478
7732
|
describe: "the author's own content field"
|
|
9479
7733
|
};
|
|
9480
|
-
/**
|
|
9481
|
-
* A single check's roles are not a fixed table: they come from the author's own `fields`
|
|
9482
|
-
* and `slots`, the way `measured`'s unit role comes from its `units`. There is no second
|
|
9483
|
-
* override channel: `fields` IS how an author fills the content roles, so every role
|
|
9484
|
-
* resolves through its own `fallback` and `check` takes no `parts` spec. Reusing the
|
|
9485
|
-
* author's `FieldMeta` as that fallback is safe for the same reason — the node was
|
|
9486
|
-
* built inside this one `f.check(…)` call and is shared with nothing else.
|
|
9487
|
-
*
|
|
9488
|
-
* The two forms use DIFFERENT role orders, both load-bearing — `roles(opts)` reads
|
|
9489
|
-
* `opts.multiple` (declared data, already resolved to a concrete boolean for THIS
|
|
9490
|
-
* declaration by the time the pipeline calls it once) and returns the matching order, the
|
|
9491
|
-
* same way `numberRange`/`realRange`'s own `roleZod` reads `opts.config`:
|
|
9492
|
-
* - single: content first, then one boolean per slot — declaration order is column
|
|
9493
|
-
* order, so the leading content field is the magnitude `magnitudeSub()` sorts and
|
|
9494
|
-
* filters by;
|
|
9495
|
-
* - multiple: checkbox columns first, then content — its own established child-table
|
|
9496
|
-
* shape (the renderer's checklist reads the toggles as the row's leading columns).
|
|
9497
|
-
*/
|
|
9498
7734
|
var contentRole = (f) => {
|
|
9499
7735
|
const def = {
|
|
9500
7736
|
role: f.key,
|
|
@@ -9511,55 +7747,25 @@ var checkboxRole = (label, i) => {
|
|
|
9511
7747
|
};
|
|
9512
7748
|
return def;
|
|
9513
7749
|
};
|
|
9514
|
-
/** `opts.slots`, defaulted to one unnamed checkbox — read the same way by `roles`/`roleZod`/`hints`. */
|
|
9515
7750
|
function checkSlots(opts) {
|
|
9516
7751
|
const slots = opts.slots;
|
|
9517
7752
|
return slots && slots.length > 0 ? slots : [""];
|
|
9518
7753
|
}
|
|
9519
|
-
/**
|
|
9520
|
-
* The content fields of one `f.check(…)` call: materialized, and validated at `roles()`
|
|
9521
|
-
* time — declaration-time-equivalent, since every real entry point (`defineShelf`, `group()`,
|
|
9522
|
-
* …) materializes synchronously at shelf-definition time — no duplicate key, no
|
|
9523
|
-
* `check\d+`-shaped key (reserved for the checkbox slots).
|
|
9524
|
-
*
|
|
9525
|
-
* A composite content field is ordinary now: `partColumns` expands it into
|
|
9526
|
-
* `<content>__<sub>` and the single form stores it across `<field>__<content>__<sub>`,
|
|
9527
|
-
* exactly as the multiple form has always stored it in its child table. The guard that
|
|
9528
|
-
* refused one in the single form is gone with the flat collector that made it necessary.
|
|
9529
|
-
*/
|
|
9530
7754
|
function checkContentFields(opts) {
|
|
9531
7755
|
const fields = opts.fields;
|
|
9532
7756
|
const contentFields = materializeTree(fields !== void 0 && Object.keys(fields).length > 0 ? fields : { text: string({ label: "core.fields.text" }) });
|
|
9533
7757
|
for (const f of contentFields) if (/^check\d+$/.test(f.key)) throw new Error(`[field.check] content field '${f.key}' is reserved for checkbox slots`);
|
|
9534
7758
|
return contentFields;
|
|
9535
7759
|
}
|
|
9536
|
-
/** `check`'s roles, in the order its CURRENT form (single vs multiple) stores them — see
|
|
9537
|
-
* this section's own header note for why the order differs and why reading `opts.multiple`
|
|
9538
|
-
* here is enough: one `materialize()` call means one already-resolved `multiple`. This is
|
|
9539
|
-
* the ONE call `checkContentFields` gets per build — the pipeline calls `roles(opts)`
|
|
9540
|
-
* once and resolves the result into `parts`, which `roleZod`/`hints` below read back
|
|
9541
|
-
* instead of recomputing. */
|
|
9542
7760
|
function checkRoles(opts) {
|
|
9543
7761
|
const contentRoles = checkContentFields(opts).map(contentRole);
|
|
9544
7762
|
const checkboxRoles = checkSlots(opts).map(checkboxRole);
|
|
9545
7763
|
return opts.multiple ? [...checkboxRoles, ...contentRoles] : [...contentRoles, ...checkboxRoles];
|
|
9546
7764
|
}
|
|
9547
|
-
/**
|
|
9548
|
-
* The CONTENT slice of `check`'s own already-resolved `parts` — everything but the
|
|
9549
|
-
* checkbox roles `roleZod`/`hints` handle separately (the checkbox roles' default zod is
|
|
9550
|
-
* a bare `z.boolean()` with no content behind it; `hints.fields` is documented content-
|
|
9551
|
-
* only). Sliced by the checkbox COUNT (`checkSlots`, declared data, no materialize), off
|
|
9552
|
-
* whichever end `checkRoles` put them on for this form — not a role-name pattern match,
|
|
9553
|
-
* which would re-hardcode the very `check\d+` shape `checkContentFields` already guards as
|
|
9554
|
-
* a RESERVED key, not a way to recognize one.
|
|
9555
|
-
*/
|
|
9556
7765
|
function checkContentParts(opts, parts) {
|
|
9557
7766
|
const slotCount = checkSlots(opts).length;
|
|
9558
7767
|
return opts.multiple ? parts.slice(slotCount) : parts.slice(0, parts.length - slotCount);
|
|
9559
7768
|
}
|
|
9560
|
-
/** `check`'s per-role default zod, from the parts the pipeline already resolved — reading
|
|
9561
|
-
* `p.meta.zod`/`p.role` off each content part instead of re-materializing the author's
|
|
9562
|
-
* `fields` a second time (see `checkContentParts`). */
|
|
9563
7769
|
function checkRoleZod(opts, parts) {
|
|
9564
7770
|
const shape = {};
|
|
9565
7771
|
for (const p of checkContentParts(opts, parts)) shape[p.role] = p.meta.zod;
|
|
@@ -9582,37 +7788,13 @@ registerType("check", {
|
|
|
9582
7788
|
}))
|
|
9583
7789
|
})
|
|
9584
7790
|
});
|
|
9585
|
-
/**
|
|
9586
|
-
* Row(s) of N labeled checkboxes plus content fields. Single (non-multiple) stores real
|
|
9587
|
-
* columns (`check0`, `check1`, …) alongside the content fields; multiple stores a child
|
|
9588
|
-
* table with the checkbox columns first, then the content fields.
|
|
9589
|
-
*
|
|
9590
|
-
* @layer primitive
|
|
9591
|
-
* @prim check
|
|
9592
|
-
* @widget check
|
|
9593
|
-
* @example f.check({ label: 'mod.fields.tasks' })
|
|
9594
|
-
*/
|
|
9595
7791
|
function check(o) {
|
|
9596
7792
|
return declare("check", o);
|
|
9597
7793
|
}
|
|
9598
|
-
/**
|
|
9599
|
-
* A role whose filling must be ENUMERATED: a unit, a currency. Now that `options` means the
|
|
9600
|
-
* same thing for every type — a literal list, a named list, or `fromShelf(...)` all describe
|
|
9601
|
-
* where this field's choices come from — the question is exactly "is `options` present?": a
|
|
9602
|
-
* `relation` filling qualifies exactly like a select does, its choices being its target
|
|
9603
|
-
* shelf's records rather than a literal list.
|
|
9604
|
-
*
|
|
9605
|
-
* Tests the DECLARED opts (`d.opts.options`), not `hasOptions` on a BUILT meta: a named
|
|
9606
|
-
* option source is bound later, at `composeRegistry`, so asking `hasOptions` here — in
|
|
9607
|
-
* phase 1, before that binding — would read an empty array and reject a perfectly good
|
|
9608
|
-
* named-source declaration.
|
|
9609
|
-
*/
|
|
9610
7794
|
var ENUMERATED = {
|
|
9611
7795
|
test: (d) => d.opts.options != null,
|
|
9612
7796
|
describe: "a field with options, or a relation"
|
|
9613
7797
|
};
|
|
9614
|
-
/** Roles of `measured`: the magnitude and its unit. The unit's default depends on THIS
|
|
9615
|
-
* field's own `units`, so it writes its own `normalize` rather than using `role()`. */
|
|
9616
7798
|
var MEASURED_ROLES = (opts) => {
|
|
9617
7799
|
const unitRole = {
|
|
9618
7800
|
role: "unit",
|
|
@@ -9632,8 +7814,6 @@ var MEASURED_ROLES = (opts) => {
|
|
|
9632
7814
|
};
|
|
9633
7815
|
return [role("value", ["number"], "real"), unitRole];
|
|
9634
7816
|
};
|
|
9635
|
-
/** `measured`'s per-role default zod: the magnitude (int via step:1, kept for compat) and
|
|
9636
|
-
* the unit, validated against `opts.units`. */
|
|
9637
7817
|
function measuredRoleZod(opts) {
|
|
9638
7818
|
const cfg = opts.config ?? {};
|
|
9639
7819
|
const units = resolveUnits(opts.units);
|
|
@@ -9646,14 +7826,6 @@ function measuredRoleZod(opts) {
|
|
|
9646
7826
|
unit: string$1().refine((v) => units.some((u) => u.value === v), { message: vmsg("measured_unit") })
|
|
9647
7827
|
};
|
|
9648
7828
|
}
|
|
9649
|
-
/**
|
|
9650
|
-
* `measured`'s own structure-failure report: not a flat code, but a SEARCH of the failed
|
|
9651
|
-
* `rowSchema`'s own issues for one that already carries a vmsg-coded message (the unit
|
|
9652
|
-
* role's own `measured_unit` refine) — that message is forwarded verbatim instead of the
|
|
9653
|
-
* flat `measured_structure` code, so the caller learns WHICH part failed. Falls back to
|
|
9654
|
-
* `measured_structure` when no such issue is found (a genuinely malformed row, not a bad
|
|
9655
|
-
* unit).
|
|
9656
|
-
*/
|
|
9657
7829
|
function measuredStructureCode(error) {
|
|
9658
7830
|
const vmsgIssue = error.issues.find((iss) => {
|
|
9659
7831
|
try {
|
|
@@ -9682,15 +7854,6 @@ registerType("measured", {
|
|
|
9682
7854
|
};
|
|
9683
7855
|
}
|
|
9684
7856
|
});
|
|
9685
|
-
/**
|
|
9686
|
-
* Magnitude + per-row unit stored together as a composite {value, unit}; the unit is
|
|
9687
|
-
* validated against `units`.
|
|
9688
|
-
*
|
|
9689
|
-
* @layer primitive
|
|
9690
|
-
* @prim measured
|
|
9691
|
-
* @widget measured
|
|
9692
|
-
* @example f.measured({ label: 'mod.fields.weight', units: 'weight' })
|
|
9693
|
-
*/
|
|
9694
7857
|
function measured(o) {
|
|
9695
7858
|
return declare("measured", o);
|
|
9696
7859
|
}
|
|
@@ -9713,16 +7876,6 @@ registerType("unit", {
|
|
|
9713
7876
|
};
|
|
9714
7877
|
}
|
|
9715
7878
|
});
|
|
9716
|
-
/**
|
|
9717
|
-
* Declares the unit ONCE per record. Every `amount` below it is a plain number on
|
|
9718
|
-
* this scale, so sums and comparisons are ordinary SQL. Deliberately not a
|
|
9719
|
-
* conversion mechanism: units.ts holds labels, not factors.
|
|
9720
|
-
*
|
|
9721
|
-
* @layer primitive
|
|
9722
|
-
* @prim text
|
|
9723
|
-
* @widget unit
|
|
9724
|
-
* @example f.unit({ label: 'mod.fields.unit', options: 'weight' })
|
|
9725
|
-
*/
|
|
9726
7879
|
function unit(o) {
|
|
9727
7880
|
return declare("unit", o);
|
|
9728
7881
|
}
|
|
@@ -9749,20 +7902,9 @@ registerType("amount", {
|
|
|
9749
7902
|
};
|
|
9750
7903
|
}
|
|
9751
7904
|
});
|
|
9752
|
-
/**
|
|
9753
|
-
* A plain number whose unit is declared elsewhere (`unitFrom`). Stored in a `real`
|
|
9754
|
-
* column: summable, comparable and orderable in SQL, unlike `measured`, which pairs
|
|
9755
|
-
* a number with a per-row unit across two sub-columns.
|
|
9756
|
-
*
|
|
9757
|
-
* @layer primitive
|
|
9758
|
-
* @prim number
|
|
9759
|
-
* @widget amount
|
|
9760
|
-
* @example f.amount({ label: 'mod.fields.quantity', unitFrom: 'unit' })
|
|
9761
|
-
*/
|
|
9762
7905
|
function amount(o) {
|
|
9763
7906
|
return declare("amount", o);
|
|
9764
7907
|
}
|
|
9765
|
-
/** Roles of `geo`: the coordinate pair and an optional human label. */
|
|
9766
7908
|
var GEO_ROLES = () => [
|
|
9767
7909
|
role("lat", ["number"], "real"),
|
|
9768
7910
|
role("lng", ["number"], "real"),
|
|
@@ -9782,30 +7924,9 @@ registerType("geo", {
|
|
|
9782
7924
|
structureCode: "geo_structure",
|
|
9783
7925
|
acceptsEmptyRow: true
|
|
9784
7926
|
});
|
|
9785
|
-
/**
|
|
9786
|
-
* Geographic point composite {lat, lng, label?}; lat/lng validated to valid coordinate
|
|
9787
|
-
* ranges.
|
|
9788
|
-
*
|
|
9789
|
-
* @layer primitive
|
|
9790
|
-
* @prim geo
|
|
9791
|
-
* @widget geo
|
|
9792
|
-
* @example f.geo({ label: 'mod.fields.location' })
|
|
9793
|
-
*/
|
|
9794
7927
|
function geo(o) {
|
|
9795
7928
|
return declare("geo", o);
|
|
9796
7929
|
}
|
|
9797
|
-
/**
|
|
9798
|
-
* Roles of `illustrated`: the prose and the picture that illustrates it.
|
|
9799
|
-
*
|
|
9800
|
-
* ORDER IS LOAD-BEARING. `partColumns` emits columns in role order and `magnitudeSub`
|
|
9801
|
-
* (sdk/shelf.ts) takes the first one, so it decides what `?about=…` filters and what a list
|
|
9802
|
-
* column sorts by. `text` first points both at the prose; `image` first would point them at a
|
|
9803
|
-
* file name.
|
|
9804
|
-
*
|
|
9805
|
-
* Both roles use a PREDICATE rather than a prim list: "renders as a picture" and "is prose" are
|
|
9806
|
-
* properties of the field's `kind`, not of its storage family — every file field stores JSON and
|
|
9807
|
-
* every text field stores `text`, so a prim list could not tell an image from a PDF.
|
|
9808
|
-
*/
|
|
9809
7930
|
var ILLUSTRATED_IMAGE_KINDS = /* @__PURE__ */ new Set([
|
|
9810
7931
|
"image",
|
|
9811
7932
|
"media",
|
|
@@ -9826,23 +7947,6 @@ var ILLUSTRATED_ROLES = () => [role("text", {
|
|
|
9826
7947
|
test: (d) => ILLUSTRATED_IMAGE_KINDS.has(typeOf(d.factory).kind),
|
|
9827
7948
|
describe: "an image field (image, media, avatar, cover, poster)"
|
|
9828
7949
|
}, "image")];
|
|
9829
|
-
/**
|
|
9830
|
-
* A composite's per-role default zod, read from each role's OWN fallback filling instead of
|
|
9831
|
-
* being hand-written per type — `partsRowShape` substitutes a part's zod only when the
|
|
9832
|
-
* author OVERRODE the filling (`p.overridden`), so whatever this returns is the entire
|
|
9833
|
-
* validation a DEFAULT filling gets. Hand-writing it is how `illustrated`'s picture role
|
|
9834
|
-
* ended up with `z.unknown()`: a bare `f.illustrated({})`, the factory's own @example,
|
|
9835
|
-
* accepted any garbage in `image` and lost whatever rules the prose filling declares.
|
|
9836
|
-
* Reading the fallbacks makes the two agree by construction, the way `f.period` builds its
|
|
9837
|
-
* own default shape from its resolved sub-fields. Each fallback's zod is already
|
|
9838
|
-
* `optionalize(…, false)`, which maps `''`/`null` to `undefined` — the nullish tolerance the
|
|
9839
|
-
* previous literal spelled out by hand.
|
|
9840
|
-
*
|
|
9841
|
-
* Shared by every composite whose default schema needs nothing from the call's own opts
|
|
9842
|
-
* (`illustrated`, `attachment`) — a type whose per-role shape IS derived from its opts
|
|
9843
|
-
* (`period`'s granularity) or from the resolved fillings (`check`'s content roles) builds
|
|
9844
|
-
* its own instead.
|
|
9845
|
-
*/
|
|
9846
7950
|
function defaultRoleZod(roles) {
|
|
9847
7951
|
const shape = {};
|
|
9848
7952
|
for (const r of roles()) {
|
|
@@ -9862,38 +7966,9 @@ registerType("illustrated", {
|
|
|
9862
7966
|
collectionView: "illustratedList",
|
|
9863
7967
|
hints: (o) => o.config ? { config: o.config } : {}
|
|
9864
7968
|
});
|
|
9865
|
-
/**
|
|
9866
|
-
* A picture and its description as ONE value: `{ text, image }`, stored across
|
|
9867
|
-
* `<key>__text` / `<key>__image`. The renderer wraps the prose around the picture, so a
|
|
9868
|
-
* record page states "what this is, shown and told" as one editorial unit instead of two
|
|
9869
|
-
* fields with an empty rectangle between them.
|
|
9870
|
-
*
|
|
9871
|
-
* `multiple: true` is the ordinary modifier and takes the collection-group path, exactly as
|
|
9872
|
-
* `f.period` and `f.check` do — a child table whose columns are the resolved parts.
|
|
9873
|
-
*
|
|
9874
|
-
* @layer primitive
|
|
9875
|
-
* @prim illustrated
|
|
9876
|
-
* @widget illustrated
|
|
9877
|
-
* @example f.illustrated({ label: 'mod.fields.about' })
|
|
9878
|
-
*/
|
|
9879
7969
|
function illustrated(o) {
|
|
9880
7970
|
return declare("illustrated", o);
|
|
9881
7971
|
}
|
|
9882
|
-
/**
|
|
9883
|
-
* Roles of `attachment`: the name the owner gives the document, and the document itself.
|
|
9884
|
-
*
|
|
9885
|
-
* ORDER IS LOAD-BEARING, for the same reason it is in `ILLUSTRATED_ROLES` above:
|
|
9886
|
-
* `partColumns` emits columns in role order and `magnitudeSub` (sdk/shelf.ts) takes the
|
|
9887
|
-
* first one, so it decides what `?<key>=…` filters and what a list column sorts by. `label`
|
|
9888
|
-
* first points both at the words a person wrote; `file` first would point them at a JSON
|
|
9889
|
-
* blob whose lexical order is an accident of how the entry's keys happen to be serialized.
|
|
9890
|
-
*
|
|
9891
|
-
* Both roles are PRIM LISTS, not predicates — unlike `illustrated`, which needs a predicate
|
|
9892
|
-
* because it must tell a picture from a PDF and every file kind stores the same prim. This
|
|
9893
|
-
* type accepts the whole file family, which is exactly what `prim: 'file'` names
|
|
9894
|
-
* (`registerFileType`, below in this file — the roles resolve lazily, long after it runs),
|
|
9895
|
-
* so a list says it precisely and a predicate would only restate it at more length.
|
|
9896
|
-
*/
|
|
9897
7972
|
var ATTACHMENT_ROLES = () => [role("label", ["text"], "string"), role("file", ["file"], "file")];
|
|
9898
7973
|
registerType("attachment", {
|
|
9899
7974
|
kind: "attachment",
|
|
@@ -9905,35 +7980,9 @@ registerType("attachment", {
|
|
|
9905
7980
|
acceptsEmptyRow: true,
|
|
9906
7981
|
collectionView: "attachmentList"
|
|
9907
7982
|
});
|
|
9908
|
-
/**
|
|
9909
|
-
* An uploaded file and the label that names it as ONE value: `{ label, file }`, stored
|
|
9910
|
-
* across `<key>__label` / `<key>__file`. A file field alone can only ever show what the
|
|
9911
|
-
* uploader happened to call the file — `scan_20240817_final(2).pdf` — so the record page
|
|
9912
|
-
* states what a document IS beside it instead of leaving the reader to open every one.
|
|
9913
|
-
*
|
|
9914
|
-
* `multiple: true` is the ordinary modifier and takes the collection-group path, exactly as
|
|
9915
|
-
* `f.illustrated` and `f.period` do — a child table whose columns are the resolved parts.
|
|
9916
|
-
* The `file` role accepts any of the file family and defaults to `f.file` (accept `*`); a
|
|
9917
|
-
* shelf that wants the narrower document uploader fills it: `fields: { file: f.document({}) }`.
|
|
9918
|
-
*
|
|
9919
|
-
* @layer primitive
|
|
9920
|
-
* @prim attachment
|
|
9921
|
-
* @widget attachment
|
|
9922
|
-
* @example f.attachment({ label: 'mod.fields.attachment' })
|
|
9923
|
-
*/
|
|
9924
7983
|
function attachment(o) {
|
|
9925
7984
|
return declare("attachment", o);
|
|
9926
7985
|
}
|
|
9927
|
-
/**
|
|
9928
|
-
* 7 values, Mon–Sun. A thin wrapper over keyed({fixed}): a fixed collection,
|
|
9929
|
-
* keyed by the weekday list, one real value per day. Its own kind 'per-weekday'
|
|
9930
|
-
* dispatches a compact renderer. Storage: a grandchild table of {day, value} rows.
|
|
9931
|
-
*
|
|
9932
|
-
* @layer primitive
|
|
9933
|
-
* @prim —
|
|
9934
|
-
* @widget per-weekday
|
|
9935
|
-
* @example f.perWeekday({ label: 'mod.fields.hours' })
|
|
9936
|
-
*/
|
|
9937
7986
|
function perWeekday(o) {
|
|
9938
7987
|
return {
|
|
9939
7988
|
...keyed({
|
|
@@ -9956,45 +8005,18 @@ function perWeekday(o) {
|
|
|
9956
8005
|
kind: "per-weekday"
|
|
9957
8006
|
};
|
|
9958
8007
|
}
|
|
9959
|
-
/**
|
|
9960
|
-
* How a cross-field ordering check (a range's `from > to`, a period's) reads one endpoint
|
|
9961
|
-
* once `stripServerOwnedParts` has removed the slots the schema owns. The check must judge
|
|
9962
|
-
* the value the record will actually CARRY, never the client's echo of a server-owned slot:
|
|
9963
|
-
* that echo is discarded on the same write, so letting it decide would reject legitimate
|
|
9964
|
-
* writes over a value that never reaches storage, and would never see the value that does.
|
|
9965
|
-
*
|
|
9966
|
-
* - a CLIENT-owned (plain `stored`) endpoint → the parsed value, under its role name;
|
|
9967
|
-
* - a COMPUTED-AND-STORED endpoint → undefined, so the pair is not compared at all: the
|
|
9968
|
-
* server produces that value later in the write (a constant included — it is written by
|
|
9969
|
-
* the same pass as a function) and nothing here can know it ahead of time.
|
|
9970
|
-
*
|
|
9971
|
-
* With every role left to its default filling every endpoint is client-owned, and this is
|
|
9972
|
-
* exactly `row[role]` — the comparison, its code and its message are unchanged.
|
|
9973
|
-
*/
|
|
9974
8008
|
function endpointReader(parts, keys) {
|
|
9975
8009
|
return (role, stored) => stored?.[keys[role]];
|
|
9976
8010
|
}
|
|
9977
|
-
/** period's own granularity, defaulted like every other config-driven composite —
|
|
9978
|
-
* read the same way by `roles`/`roleZod`/`hints`. */
|
|
9979
8011
|
function periodGranularity(opts) {
|
|
9980
8012
|
return opts.config?.granularity ?? "day";
|
|
9981
8013
|
}
|
|
9982
|
-
/** The endpoint factory at a given granularity — a ported date/datetime FieldDecl. It
|
|
9983
|
-
* takes no key: an endpoint stores under its ROLE name (`from`/`until`), which
|
|
9984
|
-
* `resolveParts` supplies, so there was never a second name to pass here. */
|
|
9985
8014
|
function periodMkSubDecl(granularity) {
|
|
9986
8015
|
return (label) => granularity === "datetime" ? datetime({ label }) : date({
|
|
9987
8016
|
label,
|
|
9988
8017
|
rules: { granularity: granularity === "month" ? "month" : "day" }
|
|
9989
8018
|
});
|
|
9990
8019
|
}
|
|
9991
|
-
/**
|
|
9992
|
-
* One endpoint role of a `period`: its default depends on THIS field's own granularity
|
|
9993
|
-
* (`mkSubDecl`), so it writes its own `normalize` rather than using `role()`. A CORRECTED
|
|
9994
|
-
* filling still keeps the composite's own granularity (`canonical.opts`) — the same
|
|
9995
|
-
* `date`/`datetime` config the role's own default carries — with the author's
|
|
9996
|
-
* label/required/ui/value layered on top, exactly like `retypeTo`'s default.
|
|
9997
|
-
*/
|
|
9998
8020
|
function periodRole(key, label, mkSubDecl) {
|
|
9999
8021
|
const def = {
|
|
10000
8022
|
role: key,
|
|
@@ -10015,15 +8037,10 @@ function periodRole(key, label, mkSubDecl) {
|
|
|
10015
8037
|
};
|
|
10016
8038
|
return def;
|
|
10017
8039
|
}
|
|
10018
|
-
/** Roles of a single `period`: the two date/datetime endpoints, at the type's granularity. */
|
|
10019
8040
|
function periodRoles(opts) {
|
|
10020
8041
|
const mkSubDecl = periodMkSubDecl(periodGranularity(opts));
|
|
10021
8042
|
return [periodRole("from", "core.period.from", mkSubDecl), periodRole("until", "core.period.until", mkSubDecl)];
|
|
10022
8043
|
}
|
|
10023
|
-
/** period's per-role default zod: each role unconditionally `.optional()`, at the type's
|
|
10024
|
-
* own granularity (`periodMkSubDecl`) — the schema a role falls back to when the author
|
|
10025
|
-
* did not override it (`partsRowShape`'s own rule: an overridden part is validated by its
|
|
10026
|
-
* OWN zod instead, never this default). */
|
|
10027
8044
|
function periodRoleZod(opts) {
|
|
10028
8045
|
const zod = materialize(periodMkSubDecl(periodGranularity(opts))("")).zod.optional();
|
|
10029
8046
|
return {
|
|
@@ -10031,9 +8048,6 @@ function periodRoleZod(opts) {
|
|
|
10031
8048
|
until: zod
|
|
10032
8049
|
};
|
|
10033
8050
|
}
|
|
10034
|
-
/** period's own ordering check: `from > until` is rejected — the same `endpointReader`
|
|
10035
|
-
* `rangeRefine` uses, comparing the ISO date/datetime strings directly (they sort
|
|
10036
|
-
* lexicographically) instead of `rangeRefine`'s numeric endpoints. */
|
|
10037
8051
|
function periodRefine(stored, parts) {
|
|
10038
8052
|
const endpoint = endpointReader(parts, partValueKeys(parts));
|
|
10039
8053
|
const f = endpoint("from", stored);
|
|
@@ -10058,39 +8072,20 @@ registerType("period", {
|
|
|
10058
8072
|
granularity: periodGranularity(opts)
|
|
10059
8073
|
})
|
|
10060
8074
|
});
|
|
10061
|
-
/**
|
|
10062
|
-
* Date/datetime period composite {from, until}; `multiple` → an array of periods stored
|
|
10063
|
-
* as a child table.
|
|
10064
|
-
*
|
|
10065
|
-
* @layer primitive
|
|
10066
|
-
* @prim period
|
|
10067
|
-
* @widget period
|
|
10068
|
-
* @example f.period({ label: 'mod.fields.employment' })
|
|
10069
|
-
*/
|
|
10070
8075
|
function period(raw) {
|
|
10071
8076
|
return declare("period", raw);
|
|
10072
8077
|
}
|
|
10073
|
-
/** Keys a file entry may carry. Everything else is rejected — see fileEntryIssue. */
|
|
10074
8078
|
var FILE_KEYS = /* @__PURE__ */ new Set([
|
|
10075
8079
|
"name",
|
|
10076
8080
|
"mime",
|
|
10077
8081
|
"size"
|
|
10078
8082
|
]);
|
|
10079
|
-
/** Keys that mean "the author pasted a remote address" — the one mistake worth naming. */
|
|
10080
8083
|
var FILE_URL_KEYS = [
|
|
10081
8084
|
"url",
|
|
10082
8085
|
"src",
|
|
10083
8086
|
"href",
|
|
10084
8087
|
"link"
|
|
10085
8088
|
];
|
|
10086
|
-
/**
|
|
10087
|
-
* A file entry points at a file already uploaded to the server: `{ name }`, where
|
|
10088
|
-
* name is the bare filename returned by POST /api/upload. Anything else — a remote
|
|
10089
|
-
* URL, an extra key, a path — is refused here, so a record can never hold a
|
|
10090
|
-
* reference the server cannot serve. `mime`/`size` are accepted (legacy payloads
|
|
10091
|
-
* and the web uploader send them) but the server overwrites them from disk.
|
|
10092
|
-
* Returns a vmsg code, or null when the entry is well-formed.
|
|
10093
|
-
*/
|
|
10094
8089
|
function fileEntryIssue(it) {
|
|
10095
8090
|
if (typeof it !== "object" || it === null || Array.isArray(it)) return "file_structure";
|
|
10096
8091
|
const rec = it;
|
|
@@ -10104,11 +8099,6 @@ function fileEntryIssue(it) {
|
|
|
10104
8099
|
if (rec["size"] !== void 0 && typeof rec["size"] !== "number") return "file_structure";
|
|
10105
8100
|
return null;
|
|
10106
8101
|
}
|
|
10107
|
-
/**
|
|
10108
|
-
* Shared by the whole file family's build(): a scalar {name,...} OR an array
|
|
10109
|
-
* [{name,...},...], native or JSON-string, stored as JSON regardless of `multiple` (the
|
|
10110
|
-
* renderer's gallery mode draws the single/many difference, not zod).
|
|
10111
|
-
*/
|
|
10112
8102
|
function fileZod() {
|
|
10113
8103
|
return unknown().superRefine((raw, ctx) => {
|
|
10114
8104
|
if (typeof raw === "string") try {
|
|
@@ -10134,10 +8124,6 @@ function fileZod() {
|
|
|
10134
8124
|
}
|
|
10135
8125
|
});
|
|
10136
8126
|
}
|
|
10137
|
-
/**
|
|
10138
|
-
* Registers one file-family TypeDef: same zod/column/json for all of them, `kind` (and
|
|
10139
|
-
* `widget`, always identical to `kind` in this family) is the only thing that varies.
|
|
10140
|
-
*/
|
|
10141
8127
|
function registerFileType(kind) {
|
|
10142
8128
|
registerType(kind, {
|
|
10143
8129
|
kind,
|
|
@@ -10155,123 +8141,41 @@ function registerFileType(kind) {
|
|
|
10155
8141
|
});
|
|
10156
8142
|
}
|
|
10157
8143
|
registerFileType("file");
|
|
10158
|
-
/**
|
|
10159
|
-
* Generic uploaded file reference `{ name }`, pointing at a file already on the server.
|
|
10160
|
-
*
|
|
10161
|
-
* @layer primitive
|
|
10162
|
-
* @prim file
|
|
10163
|
-
* @widget file
|
|
10164
|
-
* @example f.file({ label: 'mod.fields.attachment' })
|
|
10165
|
-
*/
|
|
10166
8144
|
function file(o) {
|
|
10167
8145
|
return declare("file", o);
|
|
10168
8146
|
}
|
|
10169
8147
|
registerFileType("document");
|
|
10170
|
-
/**
|
|
10171
|
-
* Uploaded document reference `{ name }`, rendered as a document widget.
|
|
10172
|
-
*
|
|
10173
|
-
* @layer primitive
|
|
10174
|
-
* @prim file
|
|
10175
|
-
* @widget document
|
|
10176
|
-
* @example f.document({ label: 'mod.fields.doc' })
|
|
10177
|
-
*/
|
|
10178
8148
|
function document(o) {
|
|
10179
8149
|
return declare("document", o);
|
|
10180
8150
|
}
|
|
10181
8151
|
registerFileType("audio");
|
|
10182
|
-
/**
|
|
10183
|
-
* Uploaded audio file reference `{ name }`, rendered as an audio player.
|
|
10184
|
-
*
|
|
10185
|
-
* @layer primitive
|
|
10186
|
-
* @prim file
|
|
10187
|
-
* @widget audio
|
|
10188
|
-
* @example f.audio({ label: 'mod.fields.recording' })
|
|
10189
|
-
*/
|
|
10190
8152
|
function audio(o) {
|
|
10191
8153
|
return declare("audio", o);
|
|
10192
8154
|
}
|
|
10193
8155
|
registerFileType("video");
|
|
10194
|
-
/**
|
|
10195
|
-
* Uploaded video file reference `{ name }`, rendered as a video player.
|
|
10196
|
-
*
|
|
10197
|
-
* @layer primitive
|
|
10198
|
-
* @prim file
|
|
10199
|
-
* @widget video
|
|
10200
|
-
* @example f.video({ label: 'mod.fields.clip' })
|
|
10201
|
-
*/
|
|
10202
8156
|
function video(o) {
|
|
10203
8157
|
return declare("video", o);
|
|
10204
8158
|
}
|
|
10205
8159
|
registerFileType("image");
|
|
10206
|
-
/**
|
|
10207
|
-
* Uploaded image reference `{ name }`, rendered as an inline image.
|
|
10208
|
-
*
|
|
10209
|
-
* @layer primitive
|
|
10210
|
-
* @prim file
|
|
10211
|
-
* @widget image
|
|
10212
|
-
* @example f.image({ label: 'mod.fields.photo' })
|
|
10213
|
-
*/
|
|
10214
8160
|
function image(o) {
|
|
10215
8161
|
return declare("image", o);
|
|
10216
8162
|
}
|
|
10217
8163
|
registerFileType("media");
|
|
10218
|
-
/**
|
|
10219
|
-
* Uploaded media reference `{ name }`, rendered as a generic media widget (mixed
|
|
10220
|
-
* image/video).
|
|
10221
|
-
*
|
|
10222
|
-
* @layer primitive
|
|
10223
|
-
* @prim file
|
|
10224
|
-
* @widget media
|
|
10225
|
-
* @example f.media({ label: 'mod.fields.attachment' })
|
|
10226
|
-
*/
|
|
10227
8164
|
function media(o) {
|
|
10228
8165
|
return declare("media", o);
|
|
10229
8166
|
}
|
|
10230
8167
|
registerFileType("avatar");
|
|
10231
|
-
/**
|
|
10232
|
-
* Uploaded image reference `{ name }`, rendered as a circular avatar.
|
|
10233
|
-
*
|
|
10234
|
-
* @layer primitive
|
|
10235
|
-
* @prim file
|
|
10236
|
-
* @widget avatar
|
|
10237
|
-
* @example f.avatar({ label: 'mod.fields.avatar' })
|
|
10238
|
-
*/
|
|
10239
8168
|
function avatar(o) {
|
|
10240
8169
|
return declare("avatar", o);
|
|
10241
8170
|
}
|
|
10242
8171
|
registerFileType("cover");
|
|
10243
|
-
/**
|
|
10244
|
-
* Uploaded image reference `{ name }`, rendered as a wide cover image.
|
|
10245
|
-
*
|
|
10246
|
-
* @layer primitive
|
|
10247
|
-
* @prim file
|
|
10248
|
-
* @widget cover
|
|
10249
|
-
* @example f.cover({ label: 'mod.fields.cover' })
|
|
10250
|
-
*/
|
|
10251
8172
|
function cover(o) {
|
|
10252
8173
|
return declare("cover", o);
|
|
10253
8174
|
}
|
|
10254
8175
|
registerFileType("poster");
|
|
10255
|
-
/**
|
|
10256
|
-
* Like `image` on the backend, but rendered large (portrait 2:3 poster).
|
|
10257
|
-
*
|
|
10258
|
-
* @layer primitive
|
|
10259
|
-
* @prim file
|
|
10260
|
-
* @widget poster
|
|
10261
|
-
* @example f.poster({ label: 'mod.fields.poster' })
|
|
10262
|
-
*/
|
|
10263
8176
|
function poster(o) {
|
|
10264
8177
|
return declare("poster", o);
|
|
10265
8178
|
}
|
|
10266
|
-
/**
|
|
10267
|
-
* Sugar over `keyed`: a collection table of {key, value} rows, `key` unique within a
|
|
10268
|
-
* record; the value cell's type is set via `fields.value` (default: plain string).
|
|
10269
|
-
*
|
|
10270
|
-
* @layer primitive
|
|
10271
|
-
* @prim —
|
|
10272
|
-
* @widget group
|
|
10273
|
-
* @example f.keyValue({ label: 'mod.fields.secrets' })
|
|
10274
|
-
*/
|
|
10275
8179
|
function keyValue(raw) {
|
|
10276
8180
|
const { fields, ...rest } = raw;
|
|
10277
8181
|
const o = normalizeOpts(rest);
|
|
@@ -10293,9 +8197,7 @@ function keyValue(raw) {
|
|
|
10293
8197
|
by: "key"
|
|
10294
8198
|
});
|
|
10295
8199
|
}
|
|
10296
|
-
/** Roles of a range: the two endpoints, integer or real depending on `isInt`. */
|
|
10297
8200
|
var RANGE_ROLES = (isInt) => [role("from", ["number"], isInt ? "int" : "real"), role("to", ["number"], isInt ? "int" : "real")];
|
|
10298
|
-
/** Shared by numberRange/realRange's `roleZod`: the endpoint schema, int or real per `isInt`. */
|
|
10299
8201
|
function rangeEndpointZod(isInt, opts) {
|
|
10300
8202
|
const cfg = opts.config ?? {};
|
|
10301
8203
|
let n = number$1().finite();
|
|
@@ -10304,11 +8206,6 @@ function rangeEndpointZod(isInt, opts) {
|
|
|
10304
8206
|
if (cfg.max != null) n = n.max(cfg.max);
|
|
10305
8207
|
return n;
|
|
10306
8208
|
}
|
|
10307
|
-
/** Shared by numberRange/realRange's `refine`: `from > to` is rejected. Strip FIRST, then
|
|
10308
|
-
* compare — the check must judge the value the record will actually carry, never the
|
|
10309
|
-
* client's echo of an endpoint the SCHEMA owns (buildComposite strips it before calling
|
|
10310
|
-
* this). Each endpoint is read under the key it occupies in the value object — its role
|
|
10311
|
-
* name, always, now. */
|
|
10312
8209
|
function rangeRefine(stored, parts) {
|
|
10313
8210
|
const endpoint = endpointReader(parts, partValueKeys(parts));
|
|
10314
8211
|
const fromV = endpoint("from", stored);
|
|
@@ -10340,25 +8237,9 @@ function registerRangeType(kind, isInt) {
|
|
|
10340
8237
|
}
|
|
10341
8238
|
registerRangeType("numberRange", true);
|
|
10342
8239
|
registerRangeType("realRange", false);
|
|
10343
|
-
/**
|
|
10344
|
-
* Integer range composite {from, to}; `from > to` is rejected.
|
|
10345
|
-
*
|
|
10346
|
-
* @layer primitive
|
|
10347
|
-
* @prim numberRange
|
|
10348
|
-
* @widget numberRange
|
|
10349
|
-
* @example f.numberRange({ label: 'mod.fields.ageRange' })
|
|
10350
|
-
*/
|
|
10351
8240
|
function numberRange(o) {
|
|
10352
8241
|
return declare("numberRange", o);
|
|
10353
8242
|
}
|
|
10354
|
-
/**
|
|
10355
|
-
* Real-number range composite {from, to}; `from > to` is rejected.
|
|
10356
|
-
*
|
|
10357
|
-
* @layer primitive
|
|
10358
|
-
* @prim realRange
|
|
10359
|
-
* @widget realRange
|
|
10360
|
-
* @example f.realRange({ label: 'mod.fields.priceRange' })
|
|
10361
|
-
*/
|
|
10362
8243
|
function realRange(o) {
|
|
10363
8244
|
return declare("realRange", o);
|
|
10364
8245
|
}
|
|
@@ -10393,23 +8274,9 @@ registerType("embed", {
|
|
|
10393
8274
|
};
|
|
10394
8275
|
}
|
|
10395
8276
|
});
|
|
10396
|
-
/**
|
|
10397
|
-
* External content snapshot by URL — oEmbed/OpenGraph metadata (title, thumbnail,
|
|
10398
|
-
* provider, …) captured at save time via /api/embed.
|
|
10399
|
-
*
|
|
10400
|
-
* @layer primitive
|
|
10401
|
-
* @prim embed
|
|
10402
|
-
* @widget embed
|
|
10403
|
-
* @example f.embed({ label: 'mod.fields.link' })
|
|
10404
|
-
*/
|
|
10405
8277
|
function embed(o) {
|
|
10406
8278
|
return declare("embed", o);
|
|
10407
8279
|
}
|
|
10408
|
-
/**
|
|
10409
|
-
* Base primitives (storage-aligned) + structural types. Presets (thin wrappers
|
|
10410
|
-
* over these primitives) are added below from field-presets.ts and CANNOT override
|
|
10411
|
-
* any key from here (guard in composeF).
|
|
10412
|
-
*/
|
|
10413
8280
|
var PRIMITIVES = {
|
|
10414
8281
|
string,
|
|
10415
8282
|
text,
|
|
@@ -10459,7 +8326,6 @@ var PRIMITIVES = {
|
|
|
10459
8326
|
button,
|
|
10460
8327
|
backrefs
|
|
10461
8328
|
};
|
|
10462
|
-
/** Assembles `f`, guaranteeing no preset/block shadows a primitive. */
|
|
10463
8329
|
function composeF(presets, blocks) {
|
|
10464
8330
|
for (const k of Object.keys(presets)) if (k in PRIMITIVES) throw new Error(`[f] preset '${k}' overrides a primitive`);
|
|
10465
8331
|
for (const k of Object.keys(blocks)) if (k in PRIMITIVES) throw new Error(`[f] block '${k}' overrides a primitive`);
|
|
@@ -10500,14 +8366,12 @@ function toClient(field) {
|
|
|
10500
8366
|
};
|
|
10501
8367
|
}
|
|
10502
8368
|
//#endregion
|
|
10503
|
-
//#region ../sdk/
|
|
8369
|
+
//#region ../sdk/dist/settings.js
|
|
10504
8370
|
function defineSettings(s) {
|
|
10505
8371
|
return materializeDef(s);
|
|
10506
8372
|
}
|
|
10507
8373
|
//#endregion
|
|
10508
|
-
//#region ../sdk/
|
|
10509
|
-
/** Flat [storageKey, FieldMeta] pairs of the parent table's columns.
|
|
10510
|
-
* layout group → flattened; embedded group → prefix `key__`; collection → skipped. */
|
|
8374
|
+
//#region ../sdk/dist/shelf.js
|
|
10511
8375
|
function fieldEntries(items) {
|
|
10512
8376
|
const out = [];
|
|
10513
8377
|
for (const raw of items) {
|
|
@@ -10521,38 +8385,12 @@ function fieldEntries(items) {
|
|
|
10521
8385
|
}
|
|
10522
8386
|
return out;
|
|
10523
8387
|
}
|
|
10524
|
-
/**
|
|
10525
|
-
* Rejects a keyless computed element (`field.x({ value: fn })`, no `key`) anywhere in
|
|
10526
|
-
* the tree: at the shelf's own top level, inside a keyless layout group (`f.group`/
|
|
10527
|
-
* `f.row`/`f.sheet` without a storage key), AND inside a keyed embedded group or
|
|
10528
|
-
* collection row. A keyless computed node never carries a key of its own regardless of what it is
|
|
10529
|
-
* nested in, so the enclosing group having a storage key does not give it one — there
|
|
10530
|
-
* is still no mechanism that computes and returns such an element: `applyStoredParts`
|
|
10531
|
-
* (`@coffer-org/server/part-injection`) only ever writes into a KEYED unit — a
|
|
10532
|
-
* top-level field with `fm.compute`, or a composite part, and every composite part now
|
|
10533
|
-
* always has a key too (its role name) — so a bare keyless value node has no column to
|
|
10534
|
-
* land in either way. Left unrejected, a keyless computed element nested inside a
|
|
10535
|
-
* keyed group would silently render blank forever instead of failing loudly at
|
|
10536
|
-
* `defineShelf` time — recurse into every group, not only keyless ones.
|
|
10537
|
-
*/
|
|
10538
8388
|
function assertNoKeylessCompute(items, owner) {
|
|
10539
8389
|
for (const it of items) {
|
|
10540
8390
|
if ("value" in it && typeof it.value === "function") throw new Error(`${owner}: computed element needs a key — the server computes it and needs a name to return it under`);
|
|
10541
8391
|
if (hasChildren(it)) assertNoKeylessCompute(it.fields, owner);
|
|
10542
8392
|
}
|
|
10543
8393
|
}
|
|
10544
|
-
/**
|
|
10545
|
-
* The declaration-time checks that are about a FIELD LIST, not about a shelf: they read
|
|
10546
|
-
* `fields` and nothing else, so they are equally true of an extend's fields — an extend
|
|
10547
|
-
* stores composites in its own table through the very same write path (`upsertExtendRecord`
|
|
10548
|
-
* → `applyStoredParts`) and renders them through the same layout walk.
|
|
10549
|
-
*
|
|
10550
|
-
* They live in ONE function called by both `defineShelf` and `defineExtend` rather than
|
|
10551
|
-
* being copied: `defineExtend` had NEITHER, so a keyless computed element on an extend
|
|
10552
|
-
* rendered blank forever instead of throwing, and a mixed-ownership composite on an extend
|
|
10553
|
-
* warned about nothing at all. `owner` is the prefix each diagnostic is tagged with
|
|
10554
|
-
* (`[shelf] things/item`, `[extend] device_info`), the one thing the two callers differ in.
|
|
10555
|
-
*/
|
|
10556
8394
|
function checkFieldDeclaration(fields, owner) {
|
|
10557
8395
|
assertNoKeylessCompute(fields, owner);
|
|
10558
8396
|
}
|
|
@@ -10572,32 +8410,12 @@ function defineShelf(m) {
|
|
|
10572
8410
|
checkFieldDeclaration(built.fields, `[shelf] ${built.library}/${built.shelf}`);
|
|
10573
8411
|
return built;
|
|
10574
8412
|
}
|
|
10575
|
-
/**
|
|
10576
|
-
* Contract of a real entity:
|
|
10577
|
-
* id — uuid (the ONLY required system field, auto).
|
|
10578
|
-
* The rest (including name) are regular fields in ShelfDef.fields.
|
|
10579
|
-
* created_at/updated_at — system infrastructure (outside fields, auto-managed).
|
|
10580
|
-
*/
|
|
10581
|
-
/**
|
|
10582
|
-
* Keyed fields of the record itself, in declaration order: top level and plain
|
|
10583
|
-
* layout groups. An embedded group or a collection describes a sub-record, so its
|
|
10584
|
-
* fields are not candidates for the record's own heading.
|
|
10585
|
-
*/
|
|
10586
8413
|
function ownFieldEntries(items) {
|
|
10587
8414
|
const out = [];
|
|
10588
8415
|
for (const it of items) if (isNamedField(it)) out.push([it.key, it.type]);
|
|
10589
8416
|
else if (hasChildren(it) && !isCollectionGroup(it) && !isEmbeddedGroup(it)) out.push(...ownFieldEntries(it.fields));
|
|
10590
8417
|
return out;
|
|
10591
8418
|
}
|
|
10592
|
-
/**
|
|
10593
|
-
* Key of the record's title field (lists, relation pickers, inlines, record heading).
|
|
10594
|
-
* Priority: views.title → the first keyed kind:'title' field → 'id'.
|
|
10595
|
-
*
|
|
10596
|
-
* The title is declared, never guessed: a shelf that wants a heading marks the field
|
|
10597
|
-
* with field.title(), or names another field through views.title when the heading is
|
|
10598
|
-
* not a text field (a date, a computed value). A shelf that declares nothing resolves
|
|
10599
|
-
* to 'id' and renders no heading — every field stays in the body with its own label.
|
|
10600
|
-
*/
|
|
10601
8419
|
function titleKey(m) {
|
|
10602
8420
|
if (m.views?.title) return m.views.title;
|
|
10603
8421
|
const declared = ownFieldEntries(m.fields).find(([, f]) => f.kind === "title");
|