@shwfed/config 3.0.12 → 3.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/actions/buttons/2026-04-18/com.shwfed.actions.button.http.request.json/config.d.vue.ts +2 -2
  3. package/dist/runtime/components/actions/buttons/2026-04-18/com.shwfed.actions.button.http.request.json/config.vue.d.ts +2 -2
  4. package/dist/runtime/components/actions/buttons/2026-04-18/com.shwfed.actions.button.http.request.json.confirm/config.d.vue.ts +2 -2
  5. package/dist/runtime/components/actions/buttons/2026-04-18/com.shwfed.actions.button.http.request.json.confirm/config.vue.d.ts +2 -2
  6. package/dist/runtime/components/actions/buttons/2026-06-08/com.shwfed.actions.button.http.request.json.batch/config.d.vue.ts +2 -2
  7. package/dist/runtime/components/actions/buttons/2026-06-08/com.shwfed.actions.button.http.request.json.batch/config.vue.d.ts +2 -2
  8. package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/config.d.vue.ts +8 -8
  9. package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/config.vue.d.ts +8 -8
  10. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/config.d.vue.ts +10 -8
  11. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/config.vue +272 -286
  12. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/config.vue.d.ts +10 -8
  13. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/runtime.d.vue.ts +10 -8
  14. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/runtime.vue +91 -291
  15. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/runtime.vue.d.ts +10 -8
  16. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/schema.d.ts +4 -28
  17. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital/schema.js +14 -33
  18. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.realestate.lease/config.d.vue.ts +96 -0
  19. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.realestate.lease/config.vue +385 -0
  20. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.realestate.lease/config.vue.d.ts +96 -0
  21. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.realestate.lease/items-colgroup.d.vue.ts +3 -0
  22. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.realestate.lease/items-colgroup.vue +20 -0
  23. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.realestate.lease/items-colgroup.vue.d.ts +3 -0
  24. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.realestate.lease/runtime.d.vue.ts +96 -0
  25. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.realestate.lease/runtime.vue +177 -0
  26. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.realestate.lease/runtime.vue.d.ts +96 -0
  27. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.realestate.lease/schema.d.ts +102 -0
  28. package/dist/runtime/components/config/blocks/2026-07-17/com.shwfed.block.invoice.realestate.lease/schema.js +186 -0
  29. package/dist/runtime/components/form/fields/2026-04-22/com.shwfed.form.field.text/config.d.vue.ts +22 -22
  30. package/dist/runtime/components/form/fields/2026-04-22/com.shwfed.form.field.text/config.vue.d.ts +22 -22
  31. package/dist/runtime/components/form/fields/2026-04-22/com.shwfed.form.field.textarea/config.d.vue.ts +2 -2
  32. package/dist/runtime/components/form/fields/2026-04-22/com.shwfed.form.field.textarea/config.vue.d.ts +2 -2
  33. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.number/config.d.vue.ts +22 -22
  34. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.number/config.vue.d.ts +22 -22
  35. package/dist/runtime/components/form/utils/validation.js +9 -7
  36. package/dist/runtime/components/table/columns/2026-05-24/com.shwfed.table.column.combobox-single.remote.options-remote/config.d.vue.ts +2 -2
  37. package/dist/runtime/components/table/columns/2026-05-24/com.shwfed.table.column.combobox-single.remote.options-remote/config.vue.d.ts +2 -2
  38. package/dist/runtime/components/table/columns/2026-05-24/com.shwfed.table.column.combobox-single.remote.options-static/config.d.vue.ts +2 -2
  39. package/dist/runtime/components/table/columns/2026-05-24/com.shwfed.table.column.combobox-single.remote.options-static/config.vue.d.ts +2 -2
  40. package/dist/runtime/components/table/columns/2026-05-25/com.shwfed.table.column.combobox-multi.remote.options-remote/config.d.vue.ts +2 -2
  41. package/dist/runtime/components/table/columns/2026-05-25/com.shwfed.table.column.combobox-multi.remote.options-remote/config.vue.d.ts +2 -2
  42. package/dist/runtime/components/table/columns/2026-05-25/com.shwfed.table.column.combobox-multi.remote.options-static/config.d.vue.ts +2 -2
  43. package/dist/runtime/components/table/columns/2026-05-25/com.shwfed.table.column.combobox-multi.remote.options-static/config.vue.d.ts +2 -2
  44. package/dist/runtime/components/table/columns/2026-05-26/com.shwfed.table.column.combobox-multi.remote/config.d.vue.ts +2 -2
  45. package/dist/runtime/components/table/columns/2026-05-26/com.shwfed.table.column.combobox-multi.remote/config.vue.d.ts +2 -2
  46. package/dist/runtime/components/table/columns/2026-05-26/com.shwfed.table.column.combobox-single.remote/config.d.vue.ts +2 -2
  47. package/dist/runtime/components/table/columns/2026-05-26/com.shwfed.table.column.combobox-single.remote/config.vue.d.ts +2 -2
  48. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.combobox-multi/config.d.vue.ts +2 -2
  49. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.combobox-multi/config.vue.d.ts +2 -2
  50. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.combobox-single/config.d.vue.ts +2 -2
  51. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.combobox-single/config.vue.d.ts +2 -2
  52. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.combobox-multi/config.d.vue.ts +2 -2
  53. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.combobox-multi/config.vue.d.ts +2 -2
  54. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.combobox-single/config.d.vue.ts +2 -2
  55. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.combobox-single/config.vue.d.ts +2 -2
  56. package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/config.vue +57 -247
  57. package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/prompt.md +2 -2
  58. package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/runtime.vue +29 -126
  59. package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/schema.d.ts +5 -27
  60. package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/schema.js +10 -37
  61. package/dist/runtime/components/ui/invoice/InvoiceSheet.d.vue.ts +66 -0
  62. package/dist/runtime/components/ui/invoice/InvoiceSheet.vue +182 -0
  63. package/dist/runtime/components/ui/invoice/InvoiceSheet.vue.d.ts +66 -0
  64. package/dist/runtime/components/ui/invoice/index.d.ts +1 -0
  65. package/dist/runtime/components/ui/invoice/index.js +1 -0
  66. package/dist/runtime/components/ui/invoice-config/InvoiceConfigShell.d.vue.ts +36 -0
  67. package/dist/runtime/components/ui/invoice-config/InvoiceConfigShell.vue +41 -0
  68. package/dist/runtime/components/ui/invoice-config/InvoiceConfigShell.vue.d.ts +36 -0
  69. package/dist/runtime/components/{config/blocks/2026-07-17/com.shwfed.block.invoice.digital/expr-field.vue → ui/invoice-config/InvoiceExprField.vue} +4 -4
  70. package/dist/runtime/components/{config/blocks/2026-07-17/com.shwfed.block.invoice.digital/json-scope-provider.vue → ui/invoice-config/InvoiceJsonScope.vue} +3 -3
  71. package/dist/runtime/components/{config/blocks/2026-07-17/com.shwfed.block.invoice.digital/row-scope-provider.vue → ui/invoice-config/InvoiceRowScope.vue} +2 -2
  72. package/dist/runtime/components/ui/invoice-config/index.d.ts +4 -0
  73. package/dist/runtime/components/ui/invoice-config/index.js +4 -0
  74. package/dist/runtime/share/invoice-eval.d.ts +13 -0
  75. package/dist/runtime/share/invoice-eval.js +88 -0
  76. package/dist/runtime/share/invoice.d.ts +68 -0
  77. package/dist/runtime/share/invoice.js +35 -0
  78. package/dist/runtime/share/slot-renderer.vue +83 -24
  79. package/dist/runtime/vendor/cel-js/CLAUDE.md +3 -1
  80. package/dist/runtime/vendor/cel-js/PROMPT.md +31 -2
  81. package/dist/runtime/vendor/cel-js/lib/functions.js +28 -1
  82. package/dist/runtime/vendor/cel-js/lib/registry.js +2 -1
  83. package/package.json +1 -1
  84. /package/dist/runtime/components/{config/blocks/2026-07-17/com.shwfed.block.invoice.digital/expr-field.d.vue.ts → ui/invoice-config/InvoiceExprField.d.vue.ts} +0 -0
  85. /package/dist/runtime/components/{config/blocks/2026-07-17/com.shwfed.block.invoice.digital/expr-field.vue.d.ts → ui/invoice-config/InvoiceExprField.vue.d.ts} +0 -0
  86. /package/dist/runtime/components/{config/blocks/2026-07-17/com.shwfed.block.invoice.digital/json-scope-provider.d.vue.ts → ui/invoice-config/InvoiceJsonScope.d.vue.ts} +0 -0
  87. /package/dist/runtime/components/{config/blocks/2026-07-17/com.shwfed.block.invoice.digital/json-scope-provider.vue.d.ts → ui/invoice-config/InvoiceJsonScope.vue.d.ts} +0 -0
  88. /package/dist/runtime/components/{config/blocks/2026-07-17/com.shwfed.block.invoice.digital/row-scope-provider.d.vue.ts → ui/invoice-config/InvoiceRowScope.d.vue.ts} +0 -0
  89. /package/dist/runtime/components/{config/blocks/2026-07-17/com.shwfed.block.invoice.digital/row-scope-provider.vue.d.ts → ui/invoice-config/InvoiceRowScope.vue.d.ts} +0 -0
  90. /package/dist/runtime/{components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital → share}/rmb.d.ts +0 -0
  91. /package/dist/runtime/{components/config/blocks/2026-07-17/com.shwfed.block.invoice.digital → share}/rmb.js +0 -0
@@ -0,0 +1,88 @@
1
+ import { computed, onMounted, ref, watch } from "vue";
2
+ import { Effect, Option } from "effect";
3
+ import { Fetch } from "fx-fetch";
4
+ import { cel } from "../utils/cel.js";
5
+ import { celBindings, injectCELContext } from "../utils/cel-context.js";
6
+ import { fetchJsonOption } from "./request.js";
7
+ import { toCapital } from "./rmb.js";
8
+ const MONEY_FORMAT = new Intl.NumberFormat("zh-CN", {
9
+ minimumFractionDigits: 2,
10
+ maximumFractionDigits: 2
11
+ });
12
+ function toFiniteNumber(v) {
13
+ if (typeof v === "number") return Number.isFinite(v) ? v : null;
14
+ if (typeof v === "string" && v.trim() !== "") {
15
+ const n = Number(v);
16
+ return Number.isFinite(n) ? n : null;
17
+ }
18
+ return null;
19
+ }
20
+ export function useInvoiceEval(requestExpr) {
21
+ const inheritedContext = injectCELContext();
22
+ const json = ref(void 0);
23
+ const jsonOption = computed(
24
+ () => json.value === void 0 ? Option.none() : Option.some(json.value)
25
+ );
26
+ const hasRequest = computed(() => {
27
+ const expr = requestExpr();
28
+ return !!expr && expr.trim().length > 0;
29
+ });
30
+ function baseContext() {
31
+ const inherited = celBindings(inheritedContext);
32
+ if (!hasRequest.value && "json" in inherited) return inherited;
33
+ return { ...inherited, json: jsonOption.value };
34
+ }
35
+ async function refresh() {
36
+ const expr = requestExpr();
37
+ if (!expr || expr.trim().length === 0) {
38
+ json.value = void 0;
39
+ return;
40
+ }
41
+ const program = Effect.gen(function* () {
42
+ const body = yield* fetchJsonOption(
43
+ yield* cel(expr, celBindings(inheritedContext))
44
+ );
45
+ return Option.getOrUndefined(body);
46
+ });
47
+ try {
48
+ json.value = await Effect.runPromise(Effect.provide(program, Fetch.layer));
49
+ } catch (err) {
50
+ console.warn("[shwfed-invoice] request failed", err);
51
+ json.value = void 0;
52
+ }
53
+ }
54
+ onMounted(() => void refresh());
55
+ watch(() => requestExpr() ?? "", () => void refresh());
56
+ function evaluate(expression, scope) {
57
+ if (!expression || expression.trim().length === 0) return void 0;
58
+ try {
59
+ return Effect.runSync(cel(expression, { ...baseContext(), ...scope }));
60
+ } catch (err) {
61
+ console.warn("[shwfed-invoice] expression evaluation failed", expression, err);
62
+ return void 0;
63
+ }
64
+ }
65
+ function text(expression, scope) {
66
+ const v = evaluate(expression, scope);
67
+ return v === null || v === void 0 ? "" : String(v);
68
+ }
69
+ function money(expression, scope) {
70
+ const v = evaluate(expression, scope);
71
+ if (v === null || v === void 0) return "";
72
+ if (typeof v === "number") return Number.isFinite(v) ? MONEY_FORMAT.format(v) : "";
73
+ return String(v);
74
+ }
75
+ function plain(expression, scope) {
76
+ const v = evaluate(expression, scope);
77
+ if (v === null || v === void 0) return "";
78
+ return String(v);
79
+ }
80
+ function bool(expression, scope) {
81
+ return evaluate(expression, scope) === true;
82
+ }
83
+ function capital(expression) {
84
+ const n = toFiniteNumber(evaluate(expression));
85
+ return n === null ? "" : toCapital(n) ?? "";
86
+ }
87
+ return { json, evaluate, text, money, plain, bool, capital };
88
+ }
@@ -0,0 +1,68 @@
1
+ import { Schema } from 'effect';
2
+ import type { Environment } from '../vendor/cel-js/lib/index.js';
3
+ import type { ResultType } from './expression.js';
4
+ /**
5
+ * The pieces every 数电票 block shares. The 税总 invoice family (通用数电票、不动产
6
+ * 经营租赁服务、不动产销售、建筑服务…) is one 版式 with a per-业务 label, per-业务
7
+ * 明细 columns and a per-业务 备注 — everything else (抬头 / 购销双方 / 合计 /
8
+ * 价税合计 / 开票人) is byte-identical across members. This module holds the schema
9
+ * half of that overlap; `ui/invoice` holds the rendering half and `share/
10
+ * invoice-eval.ts` the per-栏 evaluation.
11
+ *
12
+ * Deliberately NOT a "common fields" factory: each block composes its own
13
+ * `Schema.Struct` so that `type` / `compatibilityDate` stay literal `Schema.tag`s
14
+ * at the folder that owns them. A factory would have to take those as arguments
15
+ * and hand back a half-built struct — more indirection than the duplication it
16
+ * would remove.
17
+ */
18
+ export type Configure = (env: Environment) => void;
19
+ /**
20
+ * 票面绝大多数栏位是文本。上下文取值的表达式常被 CEL 推断为 `dyn`(只在运行时收
21
+ * 窄),所以与 animated-number / card 的字符串字段一样容忍 `dyn` / `optional`。
22
+ */
23
+ export declare const textResult: ResultType;
24
+ /**
25
+ * 金额类栏位同时接受 `number` 与 `string`:格式化规则是「返回 string 原样打印,
26
+ * 返回 number 按票面惯例格式化」(见 `share/invoice-eval.ts` 的 `money`)。后端常以
27
+ * string 传小数以免丢精度,作者也可借此完全接管格式。
28
+ */
29
+ export declare const moneyResult: ResultType;
30
+ export declare const listResult: ResultType;
31
+ export declare const boolResult: ResultType;
32
+ /**
33
+ * 票面自己的(可选)取数:`request` 求值出的 HTTP 响应体,票面**每一栏**都能读。
34
+ *
35
+ * 与「不内建 DataSource」的边界不冲突 —— 只有 `request`,没有 `data`:票面各栏本来
36
+ * 就是逐栏 CEL 映射,`json` 只是多了一个可读的上下文变量,而不是又一层「列表 → 行」
37
+ * 的取数抽象。不配置时 `json` 为 `none`,票面照旧从外层上下文(`item` / 详情页变量)
38
+ * 取值。
39
+ *
40
+ * 也导出给各块的 `config.vue` —— 它直接渲染 `ExpressionEditor`(不经过 schema),
41
+ * 不共享这份声明的话,编辑器会把 `json` 误报成 `Unknown variable`。镜像 card 块的
42
+ * `JSON_VAR`。
43
+ */
44
+ export declare const JSON_VAR: {
45
+ readonly type: "optional<dyn>";
46
+ readonly label: "HTTP 响应体";
47
+ readonly description: "HTTP 响应体(配置了「请求」时为已解析 JSON,否则为 none)";
48
+ };
49
+ /**
50
+ * 登记 `json`,但外层 `configure` 已经声明过就跳过 —— 宿主(详情页)本来就可能把自己
51
+ * 的响应体以 `json` 喂进来,重复声明会被 CEL 注册表拒掉(`'json' is already
52
+ * registered`),整个配置面直接抛错。与 `registerLoopVariablesIfAbsent` 同解。
53
+ *
54
+ * 外层已有时以**外层的声明为准**(类型通常是 `dyn` 而不是这里的 `optional<dyn>`)。
55
+ * 运行期的对应规则见 `share/invoice-eval.ts` 的 `baseContext`:配了「请求」本块的
56
+ * 响应体遮蔽外层的 `json`,没配则外层的 `json` 原样透传。
57
+ */
58
+ export declare function registerJsonIfAbsent(env: Environment): void;
59
+ /**
60
+ * 购买方 / 销售方信息栏。票面只有「名称」与「统一社会信用代码/纳税人识别号」两栏 ——
61
+ * 开户行与账号在真实发票上属于「备注」栏的自由文本(不是独立栏位),因此不建模,
62
+ * 由各块的备注承载。整个发票家族逐字相同,故在此共享。
63
+ */
64
+ export declare function partySchema(configure: Configure, role: '购买方' | '销售方'): Schema.Struct<{
65
+ name: Schema.Schema<string, string, never>;
66
+ taxId: Schema.Schema<string, string, never>;
67
+ }>;
68
+ export type PartyValue = Schema.Schema.Type<ReturnType<typeof partySchema>>;
@@ -0,0 +1,35 @@
1
+ import { Schema } from "effect";
2
+ import { Expression } from "./expression.js";
3
+ export const textResult = (t) => t === "string" || t === "dyn" || t.startsWith("optional");
4
+ export const moneyResult = (t) => t === "number" || t === "string" || t === "dyn" || t.startsWith("optional");
5
+ export const listResult = (t) => t === "dyn" || t.startsWith("list") || t.startsWith("optional");
6
+ export const boolResult = (t) => t === "bool" || t === "dyn" || t.startsWith("optional");
7
+ export const JSON_VAR = {
8
+ type: "optional<dyn>",
9
+ label: "HTTP \u54CD\u5E94\u4F53",
10
+ description: "HTTP \u54CD\u5E94\u4F53\uFF08\u914D\u7F6E\u4E86\u300C\u8BF7\u6C42\u300D\u65F6\u4E3A\u5DF2\u89E3\u6790 JSON\uFF0C\u5426\u5219\u4E3A none\uFF09"
11
+ };
12
+ export function registerJsonIfAbsent(env) {
13
+ const declared = env.getDefinitions().variables.some((v) => v.name === "json");
14
+ if (declared) return;
15
+ env.registerVariable("json", JSON_VAR.type, {
16
+ label: JSON_VAR.label,
17
+ description: JSON_VAR.description
18
+ });
19
+ }
20
+ export function partySchema(configure, role) {
21
+ const CelText = Expression({ configure, resultType: textResult });
22
+ return Schema.Struct({
23
+ name: CelText.annotations({
24
+ title: "\u540D\u79F0",
25
+ description: `\u8FD4\u56DE${role}\u540D\u79F0\uFF08\`string\`\uFF09\u7684 CEL \u8868\u8FBE\u5F0F`
26
+ }),
27
+ taxId: CelText.annotations({
28
+ title: "\u7EDF\u4E00\u793E\u4F1A\u4FE1\u7528\u4EE3\u7801/\u7EB3\u7A0E\u4EBA\u8BC6\u522B\u53F7",
29
+ description: `\u8FD4\u56DE${role}\u7EDF\u4E00\u793E\u4F1A\u4FE1\u7528\u4EE3\u7801\u6216\u7EB3\u7A0E\u4EBA\u8BC6\u522B\u53F7\uFF08\`string\`\uFF09\u7684 CEL \u8868\u8FBE\u5F0F`
30
+ })
31
+ }).annotations({
32
+ title: `${role}\u4FE1\u606F`,
33
+ description: `${role}\u7684\u540D\u79F0\u4E0E\u7EB3\u7A0E\u4EBA\u8BC6\u522B\u53F7\uFF1B\u5F00\u6237\u884C / \u8D26\u53F7\u8BF7\u5199\u5165\u300C\u5907\u6CE8\u300D`
34
+ });
35
+ }
@@ -14,6 +14,16 @@ const props = defineProps({
14
14
  const emit = defineEmits(["update:slotValue"]);
15
15
  const inherited = injectCELContext();
16
16
  const $cel = (expression, context) => _rawCel(expression, { ...celBindings(inherited), ...context });
17
+ function isHidden(block) {
18
+ const expr = block.hidden;
19
+ if (typeof expr !== "string" || expr.length === 0) return false;
20
+ try {
21
+ return Effect.runSync($cel(expr));
22
+ } catch (err) {
23
+ console.error(`[shwfed-slot-renderer] failed to evaluate hidden for ${block.id}:`, err);
24
+ return false;
25
+ }
26
+ }
17
27
  function pickLayout(sets) {
18
28
  if (sets.length === 0) return void 0;
19
29
  for (const set of sets) {
@@ -40,6 +50,7 @@ const placedBlocks = computed(() => {
40
50
  const block = b;
41
51
  const placement = layout.placements[block.id];
42
52
  if (!placement) continue;
53
+ if (isHidden(block)) continue;
43
54
  const entry = props.findEntry(block.type, block.compatibilityDate);
44
55
  if (!entry) {
45
56
  const key = `${block.type}@${block.compatibilityDate}`;
@@ -53,37 +64,83 @@ const placedBlocks = computed(() => {
53
64
  }
54
65
  return out;
55
66
  });
67
+ const authoredAreas = computed(() => {
68
+ const layout = activeLayout.value?.layout;
69
+ if (!layout) return [];
70
+ const out = [];
71
+ for (const b of props.slotValue.blocks) {
72
+ const block = b;
73
+ const placement = layout.placements[block.id];
74
+ if (!placement) continue;
75
+ if (!props.findEntry(block.type, block.compatibilityDate)) continue;
76
+ out.push(placement.area);
77
+ }
78
+ return out;
79
+ });
80
+ function buildRemap(count, keep) {
81
+ const line = Array.from({ length: count + 2 });
82
+ const keptFlags = [];
83
+ let acc = 1;
84
+ for (let L = 1; L <= count + 1; L++) {
85
+ line[L] = acc;
86
+ if (L <= count) {
87
+ const k = keep(L);
88
+ keptFlags.push(k);
89
+ if (k) acc++;
90
+ }
91
+ }
92
+ return { line, keptFlags, kept: acc - 1 };
93
+ }
94
+ const compaction = computed(() => {
95
+ const l = activeLayout.value?.layout;
96
+ if (!l) return null;
97
+ const authored = authoredAreas.value;
98
+ const visible = placedBlocks.value.map((p) => p.placement.area);
99
+ const coversCol = (a, c) => a[0][0] <= c && c < a[1][0];
100
+ const coversRow = (a, r) => a[0][1] <= r && r < a[1][1];
101
+ const rowCount = l.rows ?? authored.reduce((m, a) => Math.max(m, a[1][1] - 1), 0);
102
+ const grows = Array.from({ length: rowCount }, () => false);
103
+ for (const p of placedBlocks.value) {
104
+ if (!p.entry.metadata?.h?.grow) continue;
105
+ const [[, y1], [, y2]] = p.placement.area;
106
+ const lo = Math.max(1, y1);
107
+ const hi = Math.min(rowCount, y2 - 1);
108
+ for (let r = lo; r <= hi; r++) grows[r - 1] = true;
109
+ }
110
+ const col = buildRemap(l.columns, (c) => visible.some((a) => coversCol(a, c)) || !authored.some((a) => coversCol(a, c)));
111
+ const row = buildRemap(rowCount, (r) => visible.some((a) => coversRow(a, r)) || !authored.some((a) => coversRow(a, r)));
112
+ const rowTemplate = l.rows ? row.keptFlags.map((keep, i) => keep ? grows[i] ? "auto" : "minmax(auto, 1fr)" : null).filter(Boolean).join(" ") : "";
113
+ return {
114
+ colLine: col.line,
115
+ rowLine: row.line,
116
+ keptColumns: Math.max(1, col.kept),
117
+ rowTemplate
118
+ };
119
+ });
56
120
  const gridStyle = computed(() => {
57
121
  const l = activeLayout.value?.layout;
58
- if (!l) return "";
59
- const tracks = (n, min) => `repeat(${n}, minmax(${min}, 1fr))`;
122
+ const comp = compaction.value;
123
+ if (!l || !comp) return "";
124
+ const colTemplate = `repeat(${comp.keptColumns}, minmax(0, 1fr))`;
60
125
  const gap = `calc(${l.gap ?? DEFAULT_GAP} * 0.25rem)`;
61
- const colGap = l.columns > 1 ? `min(${gap}, calc(100% / ${l.columns - 1}))` : gap;
62
- let rowTemplate = "";
63
- if (l.rows) {
64
- const grows = Array.from({ length: l.rows }, () => false);
65
- for (const p of placedBlocks.value) {
66
- if (!p.entry.metadata?.h?.grow) continue;
67
- const [[, y1], [, y2]] = p.placement.area;
68
- const lo = Math.max(1, y1);
69
- const hi = Math.min(l.rows, y2 - 1);
70
- for (let r = lo; r <= hi; r++) grows[r - 1] = true;
71
- }
72
- rowTemplate = grows.map((g) => g ? "auto" : "minmax(auto, 1fr)").join(" ");
73
- }
126
+ const colGap = comp.keptColumns > 1 ? `min(${gap}, calc(100% / ${comp.keptColumns - 1}))` : gap;
74
127
  const parts = [
75
128
  "display: grid",
76
- `grid-template-columns: ${tracks(l.columns, "0")}`,
77
- rowTemplate ? `grid-template-rows: ${rowTemplate}` : "",
129
+ `grid-template-columns: ${colTemplate}`,
130
+ comp.rowTemplate ? `grid-template-rows: ${comp.rowTemplate}` : "",
78
131
  `column-gap: ${colGap}`,
79
132
  `row-gap: ${gap}`,
80
133
  l.style ?? ""
81
134
  ].filter(Boolean);
82
135
  return parts.join("; ");
83
136
  });
84
- function cellStyle(placement) {
137
+ function cellStyle(placement, comp) {
85
138
  const [[x1, y1], [x2, y2]] = placement.area;
86
- const parts = [`grid-column: ${x1} / ${x2}`, `grid-row: ${y1} / ${y2}`, "min-width: 0"];
139
+ const parts = [
140
+ `grid-column: ${comp.colLine[x1]} / ${comp.colLine[x2]}`,
141
+ `grid-row: ${comp.rowLine[y1]} / ${comp.rowLine[y2]}`,
142
+ "min-width: 0"
143
+ ];
87
144
  const h = placement.h && placement.h !== "stretch" ? placement.h : null;
88
145
  const v = placement.v && placement.v !== "stretch" ? placement.v : null;
89
146
  if (h || v) {
@@ -94,14 +151,16 @@ function cellStyle(placement) {
94
151
  }
95
152
  return parts.join("; ");
96
153
  }
97
- const rendered = computed(
98
- () => placedBlocks.value.map((p) => ({
154
+ const rendered = computed(() => {
155
+ const comp = compaction.value;
156
+ if (!comp) return [];
157
+ return placedBlocks.value.map((p) => ({
99
158
  key: p.block.id,
100
- style: cellStyle(p.placement),
159
+ style: cellStyle(p.placement, comp),
101
160
  component: p.entry.runtime,
102
161
  block: p.block
103
- }))
104
- );
162
+ }));
163
+ });
105
164
  function updateBlock(id, next) {
106
165
  const blocks = props.slotValue.blocks.map((b) => b.id === id ? next : b);
107
166
  emit("update:slotValue", { ...props.slotValue, blocks });
@@ -20,11 +20,13 @@ A `list.slice(number)` / `list.slice(number, number)` receiver pair has been add
20
20
 
21
21
  A `string.replace(string, string): string` / `string.replace(string, string, number): string` receiver pair has been added (in `functions.ts` beside `string.trim()`), matching the CEL strings-extension `replace`. The two-arg form delegates to JS `String.prototype.replaceAll` (so all non-overlapping occurrences are replaced; an empty `from` interleaves `to`). The three-arg form takes a limit and mirrors Go's `strings.Replace` via the `replaceLimited` helper: `limit < 0` replaces all, `0` replaces none, otherwise the first `limit` occurrences (`"foo".replace("", "_", 2) == "_f_oo"`).
22
22
 
23
+ A `number.format(map): string` receiver method has been added (not from upstream, in `functions.ts` beside the `string(number)` conversion) — renders a number as a display string via `Intl.NumberFormat`. The map argument is passed through verbatim as the second (`options`) argument of `new Intl.NumberFormat('en-US', options)` — the same options object `Number.prototype.toLocaleString(locales, options)` accepts (`minimumFractionDigits`, `maximumFractionDigits`, `useGrouping`, `style`, `currency`, …); the locale is hardcoded to `'en-US'` so grouping renders as a comma and results are deterministic. Two precision-critical details: (1) the value is handed to `.format()` as a **string** (`Decimal.toString()`), not a JS number — `Intl` parses the decimal string at full precision, whereas the `.toNumber()` round-trip would clip past the safe-integer range (this matters for money); the `format` type only admits `number | bigint`, so the call is cast (`fmt.format as (value: string) => string`). (2) The option *values* arrive as `Decimal` (a CEL map literal's `2` is a `Decimal`) and must be unwrapped to JS numbers before `Intl` sees them — `intlOptionsFromMap` walks the map (plain object or host `Map`) and `.toNumber()`s any `Decimal`/`bigint` value, leaving strings/bools; these are small config knobs so their float round-trip is precision-safe. An impossible option combination (e.g. `maximumFractionDigits < minimumFractionDigits`) throws a `RangeError` from the `Intl` constructor, surfaced as `invalid_format_options`. A `dyn` receiver that is a number at runtime resolves too (overload resolution is by runtime type). Tested in `__tests__/number-format.test.ts`.
24
+
23
25
  `evaluate` now returns `Effect<T, ParseError | EvaluationError | TypeError>` instead of a plain value. The entire evaluation pipeline is Effect-based — every AST node's `evaluate` function returns an Effect, and sub-expressions are composed via `Effect.gen` / `yield*`. Promise-based async handling has been removed. User-registered functions may return `Effect` values, which are automatically flatMapped into the evaluation pipeline. Import `Effect` from `effect` and use `Effect.runSync` or `Effect.runPromise` to execute the returned Effect.
24
26
 
25
27
  The `homogeneousAggregateLiterals` and `enableOptionalTypes` environment options have been removed. Aggregate data structures (lists, maps) are always heterogeneous — mixed-type literals are inferred as `list<dyn>` or `map<dyn, dyn>`. Optional types (`.?`, `[?]`, `optional.*` helpers) are always enabled.
26
28
 
27
- The custom `Optional` class has been replaced with Effect's `Option` type (`import { Option } from 'effect'`). Internal helpers `optionalOf(value)` and `optionalValue(opt)` in `optional.js` handle the CEL-specific semantics (treating `undefined` as `None`, throwing `EvaluationError` on missing value access). The optional surface is `optional.of(A): optional<A>` (the lift / `pure` — `of(null)` is `Some(null)`), `optional.ofNullable(A): optional<A>` (the null-tolerant lift, Java-style — `null` becomes `None`), `optional.none()`, `optional.hasValue()`, `optional.value()`, `optional.or(optional)`, `optional.orValue(value)`, plus `optional<A>.flatMap(var, body): optional<B>` (monadic bind), `optional<A>.map(var, body): optional<B>` (the Functor `fmap`), `optional<A>.zip(other, x, y, body): optional<C>` (the applicative `liftA2` / `zipWith`), `optional<A>.filter(var, predicate): optional<A>` (Rust `Option::filter` / Haskell `mfilter`), and the folds `optional<A>.exists(var, predicate): bool` (predicate on `Some`, `false` on `None` — Rust `is_some_and`) and `optional<A>.all(var, predicate): bool` (predicate on `Some`, vacuously `true` on `None` — Scala `forall`) — all local additions (not from upstream). All are variable-binding macros (registered with `ast` arg types so the unbound args stay unevaluated): on `Some` they bind the value(s) and evaluate `body`; on `None` they short-circuit without touching `body`. `flatMap`'s body must itself yield an `optional`; `map`'s body is a plain value that gets re-wrapped in `Some`; `filter` / `exists` / `all` predicates must be `bool` (enforced at type-check when static, at runtime via `invalid_condition_type` otherwise). `map` is the derived `o.flatMap(v, optional.of(body))` promoted to a first-class macro. `zip` is the flattened `a.flatMap(x, b.map(y, body))`: it type-checks `other` in the outer ctx, binds `x` then `y` under two chained overlays, re-wraps the plain body in `Some`, and short-circuits to `None` if either side is `None` (a `None` receiver skips evaluating `other`). Because CEL has no tuple type the transform is fused (there is no useful `optional<pair>` to return), so `zip` is `zipWith`, not Rust/Scala's pair-building `Option.zip`. The short-circuit makes `zip` observationally monadic — harmless for `optional` (a `None` carries no payload) but it would not generalise to an error-accumulating applicative. An `optional.none()` receiver/argument carries a `param` element type; the shared `bindOptionalValueType` helper (exported from `optional.ts`) binds it as `dyn` so a boolean body still type-checks — every binding macro (`map` / `flatMap` / `zip` / `filter` / `exists` / `all`) goes through it (leaking the `param` into the iteration variable was a real bug in `map` / `flatMap`, regression-tested in `optional-extras.test.ts`). `zip` lives in `optional.ts` beside `flatMap`; the two-variable binding is why it is not folded into the shared `map` expander. NOTE: `map`, `filter`, `all`, and `exists` are **registered in `macros.ts`**, not here — the parser resolves a macro by name before types are known, so the optional variants and the list-comprehension variants must share one expander per name that dispatches on the receiver type at type-check (an `optional` receiver → optional semantics; `list`/`map` → comprehension; a `dyn` receiver is deferred to a runtime `Option.isOption` check). `exists_one` and `find` stay list-only.
29
+ The custom `Optional` class has been replaced with Effect's `Option` type (`import { Option } from 'effect'`). Internal helpers `optionalOf(value)` and `optionalValue(opt)` in `optional.js` handle the CEL-specific semantics (treating `undefined` as `None`, throwing `EvaluationError` on missing value access). **Optional field access diverges from CEL**: `#getOptionalField` in `registry.ts` collapses a present-but-`null` field to `None`, where upstream (and cel-go) yield `Some(null)` — so `row.?foo` and `row.items[?0]` are absent-tolerant against backend JSON, in which `null` is overwhelmingly "absent" (without it `row.?foo.orValue([])` returns `null` and a downstream `size()` fails the overload check — a designer-facing footgun, since `.?` reads as the null-safe path). The collapse is **scoped to field access on purpose**: `optionalOf` itself keeps CEL semantics, so `optional.of(null)` is still `Some(null)` and `find` can still match a null element (its `Option` is the only channel for "did it match", so collapsing there would make a hit indistinguishable from a miss — unlike field access, where `has(row.foo)` and plain `row.foo` remain as escape hatches). `URL.query` is unaffected either way: it null-coalesces (`?? undefined`) before `optionalOf`. Pinned in `optional-extras.test.ts`. The optional surface is `optional.of(A): optional<A>` (the lift / `pure` — `of(null)` is `Some(null)`), `optional.ofNullable(A): optional<A>` (the null-tolerant lift, Java-style — `null` becomes `None`), `optional.none()`, `optional.hasValue()`, `optional.value()`, `optional.or(optional)`, `optional.orValue(value)`, plus `optional<A>.flatMap(var, body): optional<B>` (monadic bind), `optional<A>.map(var, body): optional<B>` (the Functor `fmap`), `optional<A>.zip(other, x, y, body): optional<C>` (the applicative `liftA2` / `zipWith`), `optional<A>.filter(var, predicate): optional<A>` (Rust `Option::filter` / Haskell `mfilter`), and the folds `optional<A>.exists(var, predicate): bool` (predicate on `Some`, `false` on `None` — Rust `is_some_and`) and `optional<A>.all(var, predicate): bool` (predicate on `Some`, vacuously `true` on `None` — Scala `forall`) — all local additions (not from upstream). All are variable-binding macros (registered with `ast` arg types so the unbound args stay unevaluated): on `Some` they bind the value(s) and evaluate `body`; on `None` they short-circuit without touching `body`. `flatMap`'s body must itself yield an `optional`; `map`'s body is a plain value that gets re-wrapped in `Some`; `filter` / `exists` / `all` predicates must be `bool` (enforced at type-check when static, at runtime via `invalid_condition_type` otherwise). `map` is the derived `o.flatMap(v, optional.of(body))` promoted to a first-class macro. `zip` is the flattened `a.flatMap(x, b.map(y, body))`: it type-checks `other` in the outer ctx, binds `x` then `y` under two chained overlays, re-wraps the plain body in `Some`, and short-circuits to `None` if either side is `None` (a `None` receiver skips evaluating `other`). Because CEL has no tuple type the transform is fused (there is no useful `optional<pair>` to return), so `zip` is `zipWith`, not Rust/Scala's pair-building `Option.zip`. The short-circuit makes `zip` observationally monadic — harmless for `optional` (a `None` carries no payload) but it would not generalise to an error-accumulating applicative. An `optional.none()` receiver/argument carries a `param` element type; the shared `bindOptionalValueType` helper (exported from `optional.ts`) binds it as `dyn` so a boolean body still type-checks — every binding macro (`map` / `flatMap` / `zip` / `filter` / `exists` / `all`) goes through it (leaking the `param` into the iteration variable was a real bug in `map` / `flatMap`, regression-tested in `optional-extras.test.ts`). `zip` lives in `optional.ts` beside `flatMap`; the two-variable binding is why it is not folded into the shared `map` expander. NOTE: `map`, `filter`, `all`, and `exists` are **registered in `macros.ts`**, not here — the parser resolves a macro by name before types are known, so the optional variants and the list-comprehension variants must share one expander per name that dispatches on the receiver type at type-check (an `optional` receiver → optional semantics; `list`/`map` → comprehension; a `dyn` receiver is deferred to a runtime `Option.isOption` check). `exists_one` and `find` stay list-only.
28
30
 
29
31
  A `list.filterMap(var, body): list<dyn>` macro has been added (not from upstream, in `macros.ts` beside `find`) — Rust's `Iterator::filter_map`. The body evaluates to an `optional<B>` per element; `Some` values are kept (unwrapped), `None`s dropped, yielding `list<B>`. It fuses the map-then-strip-absent pattern (which would otherwise need `.map(...)` to build a `list<optional>` plus a second pass to drop the `None`s) and is the optional-gated complement to the 3-arg `map(var, filter, transform)`, whose gate is a bool predicate. NOTE: it is **not** a comprehension desugar — the accu/ternary primitives gate on a bool, not on `Option` presence — so unlike the 2/3-arg `map` and `filter` it runs a manual fold like `find` (eval receiver → coerce Set/Map/object to an array → per-element `ev.eval(body)` → `ensureOptional` → push the `Some`s). Type-check: the receiver element type comes from list `valueType` / map `keyType` / `dyn` (same `invalid_comprehension_range` contract as `find`); the body must be `optional<B>` when statically typed (via `ensureOptionalType`), and a `dyn` body defers to the runtime `ensureOptional` (matching `flatMap`); the result is `registry.getListType(bindOptionalValueType(bodyType))`. Because the body already returns an `optional`, `filterMap` needs no receiver-type dispatch (there is no `optional<A>.filterMap` — that role is `flatMap`), so it is a single list-only expander. Tested in `__tests__/filter-map-macro.test.ts`.
30
32
 
@@ -91,8 +91,8 @@ time when statically known, at runtime when the source is `dyn`.
91
91
  ```cel
92
92
  obj.field // access field on map/object
93
93
  obj["field"] // bracket access (equivalent)
94
- obj.?field // optional field access (returns optional)
95
- obj[?0] // optional index access (returns optional)
94
+ obj.?field // optional field access (returns optional; null counts as absent)
95
+ obj[?0] // optional index access (returns optional; null counts as absent)
96
96
  ```
97
97
 
98
98
  ## Built-in Functions
@@ -116,6 +116,26 @@ There is no `int()`, `uint()`, or `double()` — `number` is the only numeric
116
116
  type. To produce an integer, round explicitly with `math.trunc` / `math.floor`
117
117
  / `math.ceil` / `math.round`.
118
118
 
119
+ `string(number)` renders a plain number (no grouping). For a **formatted**
120
+ display string — thousands separators, fixed decimal places, currency, percent —
121
+ use `number.format(options)`:
122
+ ```cel
123
+ (1234567).format({}) // "1,234,567" (comma grouping on by default)
124
+ (1234567.5).format({"minimumFractionDigits": 2}) // "1,234,567.50"
125
+ (3.14159).format({"maximumFractionDigits": 2}) // "3.14"
126
+ (1234.5).format({"minimumFractionDigits": 2,
127
+ "maximumFractionDigits": 2}) // "1,234.50" (fixed 2 decimals)
128
+ (1234567).format({"useGrouping": false}) // "1234567" (no separators)
129
+ (1234.5).format({"style": "currency", "currency": "CNY"}) // "CN¥1,234.50"
130
+ (0.42).format({"style": "percent"}) // "42%"
131
+ ```
132
+ The map is exactly the `options` object of JavaScript's
133
+ `Number.prototype.toLocaleString(locales, options)` / `Intl.NumberFormat` — the
134
+ locale is fixed to `en-US` (so grouping renders as a comma). The number is
135
+ formatted at full precision (large integers past the float safe range keep every
136
+ digit). An impossible combination (e.g. `minimumFractionDigits` above
137
+ `maximumFractionDigits`) is an error.
138
+
119
139
  ### Math
120
140
  ```cel
121
141
  math.trunc(3.7) // 3 — drop the fraction (toward zero)
@@ -414,12 +434,21 @@ An `optional<T>` is either present (`Some`) or absent (`None`). They come from
414
434
  (the lift — wraps a value as present), `optional.ofNullable(x)` (like `of`, but
415
435
  `null` lifts to absent), and `optional.none()`.
416
436
 
437
+ `.?` / `[?]` treat a `null` field as absent, so `row.?foo` is `None` whether
438
+ `foo` is missing, `null`, or `row` itself is `null` — reach for `orValue` and
439
+ trust it. (This differs from standard CEL, where a null field is present.) Only
440
+ `null` collapses; `""`, `0` and `false` are ordinary present values.
441
+
417
442
  ```cel
418
443
  optional.of(5) // present, holds 5
419
444
  optional.of(null) // present, holds null (of never inspects the value)
420
445
  optional.ofNullable(row.middleName) // absent when the field is null
421
446
  optional.none() // absent
422
447
 
448
+ row.?foo.orValue([]) // [] when foo is missing OR null
449
+ row.?foo.orValue("-") // "-" when absent; "" stays "" (only null collapses)
450
+ has(row.foo) // true even when foo is null — tells null from missing
451
+
423
452
  o.hasValue() // bool — is it present?
424
453
  o.value() // unwrap — ERRORS if absent
425
454
  o.orValue(default) // unwrap, or `default` when absent
@@ -3,7 +3,7 @@ import { Decimal } from "./decimal.js";
3
3
  import { TYPES, Type } from "./registry.js";
4
4
  import { Option } from "effect";
5
5
  import { register as registerOptional, optionalOf } from "./optional.js";
6
- import { objKeys, arrayFrom } from "./globals.js";
6
+ import { objKeys, arrayFrom, isArray } from "./globals.js";
7
7
  import { TZDate } from "@date-fns/tz";
8
8
  import { startOfMinute, startOfHour, startOfDay, startOfWeek, startOfMonth, startOfYear, endOfMinute, endOfHour, endOfDay, endOfWeek, endOfMonth, endOfYear, addMilliseconds, addSeconds, addMinutes, addHours, addDays, addWeeks, addMonths, addYears, setMilliseconds, setSeconds, setMinutes, setHours, setDate as setDayOfMonth, setMonth, setYear, format as formatDate, differenceInMilliseconds, differenceInSeconds, differenceInMinutes, differenceInHours, differenceInDays, differenceInWeeks, differenceInMonths, differenceInYears } from "date-fns";
9
9
  function toNum(v) {
@@ -87,6 +87,18 @@ export function registerFunctions(registry) {
87
87
  functionOverload("string(bool): string", (v) => `${v}`);
88
88
  functionOverload("string(number): string", (v) => Decimal.from(v).toString());
89
89
  functionOverload("string(bytes): string", (v) => ByteOpts.toUtf8(v));
90
+ functionOverload("number.format(map): string", (v, options) => {
91
+ const intlOptions = intlOptionsFromMap(options);
92
+ let fmt;
93
+ try {
94
+ fmt = new Intl.NumberFormat("en-US", intlOptions);
95
+ } catch (e) {
96
+ throw evaluationError("invalid_format_options", `number.format(): ${e.message}`);
97
+ }
98
+ return fmt.format(
99
+ Decimal.from(v).toString()
100
+ );
101
+ });
90
102
  functionOverload("string.startsWith(string): bool", (a, b) => a.startsWith(b));
91
103
  functionOverload("string.endsWith(string): bool", (a, b) => a.endsWith(b));
92
104
  functionOverload("string.contains(string): bool", (a, b) => a.includes(b));
@@ -440,6 +452,21 @@ export function registerFunctions(registry) {
440
452
  functionOverload("string(URL): string", (v) => v.href);
441
453
  registerOptional(registry);
442
454
  }
455
+ function intlOptionsFromMap(m) {
456
+ const out = {};
457
+ const put = (k, val) => {
458
+ out[String(k)] = val instanceof Decimal ? val.toNumber() : typeof val === "bigint" ? Number(val) : val;
459
+ };
460
+ if (m instanceof Map) {
461
+ for (const [k, val] of m) put(k, val);
462
+ } else if (m !== null && typeof m === "object" && !isArray(m)) {
463
+ const obj = m;
464
+ for (const k of objKeys(obj)) put(k, obj[k]);
465
+ } else {
466
+ throw evaluationError("invalid_argument", "number.format(): options must be a map");
467
+ }
468
+ return out;
469
+ }
443
470
  function stringSize(str) {
444
471
  let count = 0;
445
472
  for (const _ of str) count++;
@@ -180,7 +180,8 @@ export class TypeDeclaration {
180
180
  if (unwrapped === void 0) return Option.none();
181
181
  const type = ev.debugType(unwrapped);
182
182
  try {
183
- return optionalOf(type.fieldLazy(unwrapped, key, ast, ev));
183
+ const value = type.fieldLazy(unwrapped, key, ast, ev);
184
+ return value === null ? Option.none() : optionalOf(value);
184
185
  } catch (e) {
185
186
  if (e instanceof EvaluationError) return Option.none();
186
187
  throw e;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shwfed/config",
3
- "version": "3.0.12",
3
+ "version": "3.0.14",
4
4
  "description": "Configurable UI for SHWFED",
5
5
  "type": "module",
6
6
  "publishConfig": {