@shwfed/config 2.9.6 → 2.9.8

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 (83) hide show
  1. package/dist/mcp.mjs +1054 -855
  2. package/dist/module.json +1 -1
  3. package/dist/preview/assets/{FieldGroup.vue_vue_type_script_setup_true_lang-B-Cwm94m.js → FieldGroup.vue_vue_type_script_setup_true_lang-ByKzTJkK.js} +1 -1
  4. package/dist/preview/assets/{badge-nn3uvUrR.js → badge-Dq4c6LC9.js} +1 -1
  5. package/dist/preview/assets/{config-DO77XC7j.js → config-B8qH4_4x.js} +1 -1
  6. package/dist/preview/assets/{config-e8Li9jh4.js → config-BBGDw3Em.js} +1 -1
  7. package/dist/preview/assets/{config-CUkEZF8G.js → config-C32gE_z0.js} +1 -1
  8. package/dist/preview/assets/{config-CGhV-PCy.js → config-COfUny1G.js} +1 -1
  9. package/dist/preview/assets/{config-d8ZhbBPg.js → config-CVPTjqGi.js} +1 -1
  10. package/dist/preview/assets/{config-Dvq_Xh7I.js → config-CgdFXkLJ.js} +1 -1
  11. package/dist/preview/assets/{config-C6LousLT.js → config-DNr8rSLw.js} +1 -1
  12. package/dist/preview/assets/{config-Bg95dd9K.js → config-DVEOLj6d.js} +1 -1
  13. package/dist/preview/assets/{config-BNQXbhtM.js → config-DpRJqJ1d.js} +1 -1
  14. package/dist/preview/assets/{config-DiD-8mMw.js → config-Dvqs3X3R.js} +1 -1
  15. package/dist/preview/assets/{config-DbjXscRV.js → config-DyqhZSFe.js} +1 -1
  16. package/dist/preview/assets/{definition.vue_vue_type_script_setup_true_lang-DuO-EwWA.js → definition.vue_vue_type_script_setup_true_lang-B0MHEyHj.js} +1 -1
  17. package/dist/preview/assets/index-Br3FtCu2.js +1 -0
  18. package/dist/preview/assets/index-C1h9lV52.css +1 -0
  19. package/dist/preview/assets/index-C1wWjgLL.js +717 -0
  20. package/dist/preview/assets/{index-DZyYLnIs.js → index-CWJ_W3Gk.js} +1 -1
  21. package/dist/preview/assets/{item-BqnYkxMr.js → item-Bp81qizC.js} +1 -1
  22. package/dist/preview/assets/runtime-AHwg8SCY.js +1 -0
  23. package/dist/preview/assets/{runtime-D6blej8e.js → runtime-BHWdj0nM.js} +1 -1
  24. package/dist/preview/assets/runtime-Be6zYys6.js +1 -0
  25. package/dist/preview/assets/{runtime-CqoGHW0U.js → runtime-CKj3_07G.js} +1 -1
  26. package/dist/preview/assets/runtime-CVIZYrHD.js +1 -0
  27. package/dist/preview/assets/runtime-C_F9tcxh.js +1 -0
  28. package/dist/preview/assets/runtime-DNHuKamx.js +1 -0
  29. package/dist/preview/assets/{runtime-DrnADq2i.js → runtime-DambLtyb.js} +1 -1
  30. package/dist/preview/assets/{runtime-CfdnDil3.js → runtime-DqkDOQp2.js} +1 -1
  31. package/dist/preview/assets/runtime-UVi5ssie.js +1 -0
  32. package/dist/preview/index.html +2 -2
  33. package/dist/runtime/components/actions/buttons/2026-04-18/com.shwfed.actions.button.http.request.json/runtime.vue +1 -1
  34. package/dist/runtime/components/actions/buttons/2026-04-18/com.shwfed.actions.button.http.request.json/schema.d.ts +1 -1
  35. package/dist/runtime/components/actions/buttons/2026-04-18/com.shwfed.actions.button.http.request.json/schema.js +2 -2
  36. package/dist/runtime/components/actions/buttons/2026-04-18/com.shwfed.actions.button.http.request.json.confirm/runtime.vue +1 -1
  37. package/dist/runtime/components/actions/buttons/2026-05-15/com.shwfed.actions.button.event.dispatch/runtime.vue +5 -1
  38. package/dist/runtime/components/actions/buttons/2026-05-21/com.shwfed.actions.button.http.download/runtime.vue +2 -2
  39. package/dist/runtime/components/actions/buttons/2026-05-24/com.shwfed.actions.button.state.write/runtime.vue +1 -1
  40. package/dist/runtime/components/actions/buttons/2026-06-08/com.shwfed.actions.button.http.request.json.batch/runtime.vue +1 -1
  41. package/dist/runtime/components/actions/buttons/2026-06-08/com.shwfed.actions.button.http.request.json.batch/schema.d.ts +1 -1
  42. package/dist/runtime/components/actions/buttons/2026-06-08/com.shwfed.actions.button.http.request.json.batch/schema.js +1 -1
  43. package/dist/runtime/components/actions/utils/resolve.js +1 -1
  44. package/dist/runtime/components/block-layout-editor/index.vue +98 -3
  45. package/dist/runtime/components/config/footer.vue +22 -20
  46. package/dist/runtime/components/config/use-editor.js +5 -9
  47. package/dist/runtime/components/config/utils/validation-error.d.ts +1 -0
  48. package/dist/runtime/components/config/utils/validation-error.js +34 -0
  49. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/config.d.vue.ts +155 -0
  50. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/config.vue +918 -0
  51. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/config.vue.d.ts +155 -0
  52. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/runtime.d.vue.ts +8 -0
  53. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/runtime.vue +482 -0
  54. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/runtime.vue.d.ts +8 -0
  55. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/schema.d.ts +126 -0
  56. package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/schema.js +178 -0
  57. package/dist/runtime/components/operations/utils/resolve.d.ts +7 -0
  58. package/dist/runtime/components/operations/utils/resolve.js +1 -1
  59. package/dist/runtime/components/table/columns/2026-05-13/com.shwfed.table.column.switch/runtime.vue +2 -1
  60. package/dist/runtime/components/table/columns/2026-05-13/com.shwfed.table.column.switch.remote/runtime.vue +2 -1
  61. package/dist/runtime/components/table/columns/2026-05-24/com.shwfed.table.column.combobox-single.remote.options-remote/runtime.vue +1 -1
  62. package/dist/runtime/components/table/columns/2026-05-24/com.shwfed.table.column.combobox-single.remote.options-static/runtime.vue +1 -1
  63. package/dist/runtime/components/table/columns/2026-05-25/com.shwfed.table.column.combobox-multi.remote.options-remote/runtime.vue +1 -1
  64. package/dist/runtime/components/table/columns/2026-05-25/com.shwfed.table.column.combobox-multi.remote.options-static/runtime.vue +1 -1
  65. package/dist/runtime/components/table/columns/2026-05-26/com.shwfed.table.column.combobox-multi.remote/runtime.vue +1 -1
  66. package/dist/runtime/components/table/columns/2026-05-26/com.shwfed.table.column.combobox-single.remote/runtime.vue +1 -1
  67. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.combobox-multi/runtime.vue +1 -1
  68. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.combobox-single/runtime.vue +1 -1
  69. package/dist/runtime/share/event-bus.d.ts +20 -2
  70. package/dist/runtime/share/event-bus.js +4 -3
  71. package/dist/runtime/vendor/cel-js/CLAUDE.md +1 -1
  72. package/dist/runtime/vendor/cel-js/PROMPT.md +2 -2
  73. package/dist/runtime/vendor/cel-js/lib/http-builder.js +13 -3
  74. package/package.json +1 -1
  75. package/dist/preview/assets/index-Bpap7h0Q.js +0 -717
  76. package/dist/preview/assets/index-jHSCIEVi.js +0 -1
  77. package/dist/preview/assets/index-vPcvbp7e.css +0 -1
  78. package/dist/preview/assets/runtime-BFAxoqEC.js +0 -1
  79. package/dist/preview/assets/runtime-BntIEgRD.js +0 -1
  80. package/dist/preview/assets/runtime-Bu6BEaxq.js +0 -1
  81. package/dist/preview/assets/runtime-DEsY0ihh.js +0 -1
  82. package/dist/preview/assets/runtime-JW4LSnRF.js +0 -1
  83. package/dist/preview/assets/runtime-U7gyBbWI.js +0 -1
@@ -0,0 +1,126 @@
1
+ import { Effect, Schema } from 'effect';
2
+ import type { Environment } from '../../../../../vendor/cel-js/lib/index.js';
3
+ export declare const type: "com.shwfed.form.field.upload";
4
+ export declare const compatibilityDate: "2026-06-09";
5
+ export declare const metadata: {
6
+ readonly name: "文件上传";
7
+ readonly icon: "fluent:cloud-arrow-up-20-regular";
8
+ readonly w: {
9
+ readonly initial: 8;
10
+ readonly min: 8;
11
+ readonly max: number;
12
+ };
13
+ readonly h: {
14
+ readonly initial: 6;
15
+ readonly min: 6;
16
+ readonly max: number;
17
+ readonly grow: true;
18
+ };
19
+ };
20
+ export declare const JSON_VAR: {
21
+ readonly type: "dyn";
22
+ readonly label: "HTTP 响应体";
23
+ readonly description: "第一步 `请求` 的响应体(已解析 JSON);用于构造第二步下载请求";
24
+ };
25
+ export declare const FILES_VAR: {
26
+ readonly type: "list<dyn>";
27
+ readonly label: "已选文件";
28
+ readonly description: "本次选择的文件列表(`File` 数组);通常通过 `form({...})` 构造 multipart 请求体";
29
+ };
30
+ export declare const UPLOAD_JSON_VAR: {
31
+ readonly type: "dyn";
32
+ readonly label: "上传响应";
33
+ readonly description: "上传 `请求` 的响应体(已解析 JSON);用于提取已上传文件项";
34
+ };
35
+ export declare const FILE_VAR: {
36
+ readonly type: "dyn";
37
+ readonly label: "文件项";
38
+ readonly description: "表单状态中的单个文件项(服务端回填或上传响应映射所得);返回其展示文件名";
39
+ };
40
+ export declare function schema(configure: (env: Environment) => void): Schema.Struct<{
41
+ label: Schema.optional<Schema.TupleType<readonly [Schema.Struct<{
42
+ locale: Schema.Literal<["zh"]>;
43
+ message: Schema.SchemaClass<string, string, never>;
44
+ }>], [Schema.Struct<{
45
+ locale: Schema.Literal<["ja", "en", "ko"]>;
46
+ message: Schema.SchemaClass<string, string, never>;
47
+ }>]>>;
48
+ description: Schema.optional<Schema.refine<readonly [{
49
+ readonly locale: "zh";
50
+ readonly message: string;
51
+ }, ...{
52
+ readonly locale: "en" | "ja" | "ko";
53
+ readonly message: string;
54
+ }[]], Schema.TupleType<readonly [Schema.Struct<{
55
+ locale: Schema.Literal<["zh"]>;
56
+ message: Schema.SchemaClass<string, string, never>;
57
+ }>], [Schema.Struct<{
58
+ locale: Schema.Literal<["ja", "en", "ko"]>;
59
+ message: Schema.SchemaClass<string, string, never>;
60
+ }>]>>>;
61
+ placeholder: Schema.optional<Schema.TupleType<readonly [Schema.Struct<{
62
+ locale: Schema.Literal<["zh"]>;
63
+ message: Schema.SchemaClass<string, string, never>;
64
+ }>], [Schema.Struct<{
65
+ locale: Schema.Literal<["ja", "en", "ko"]>;
66
+ message: Schema.SchemaClass<string, string, never>;
67
+ }>]>>;
68
+ tooltip: Schema.optional<Schema.TupleType<readonly [Schema.Struct<{
69
+ locale: Schema.Literal<["zh"]>;
70
+ message: Schema.SchemaClass<string, string, never>;
71
+ }>], [Schema.Struct<{
72
+ locale: Schema.Literal<["ja", "en", "ko"]>;
73
+ message: Schema.SchemaClass<string, string, never>;
74
+ }>]>>;
75
+ orientation: Schema.optional<Schema.Literal<["vertical", "floating"]>>;
76
+ binding: Schema.optional<Schema.refine<string, typeof Schema.String>>;
77
+ disabled: Schema.optional<Schema.Schema<string, string, never>>;
78
+ readonly: Schema.optional<Schema.Schema<string, string, never>>;
79
+ multiple: Schema.optional<Schema.SchemaClass<boolean, boolean, never>>;
80
+ accept: Schema.optional<Schema.Array$<typeof Schema.String>>;
81
+ maxFileSize: Schema.optional<Schema.refine<number, typeof Schema.Number>>;
82
+ maxTotalSize: Schema.optional<Schema.refine<number, typeof Schema.Number>>;
83
+ maxFiles: Schema.optional<Schema.refine<number, Schema.filter<typeof Schema.Number>>>;
84
+ upload: Schema.optional<Schema.Struct<{
85
+ request: Schema.Schema<string, string, never>;
86
+ handle: Schema.Schema<string, string, never>;
87
+ filename: Schema.optional<Schema.Schema<string, string, never>>;
88
+ }>>;
89
+ templates: Schema.optional<Schema.Array$<Schema.Struct<{
90
+ request: Schema.Schema<string, string, never>;
91
+ download: Schema.optional<Schema.Schema<string, string, never>>;
92
+ icon: Schema.optional<Schema.refine<string, typeof Schema.String>>;
93
+ label: Schema.optional<Schema.TupleType<readonly [Schema.Struct<{
94
+ locale: Schema.Literal<["zh"]>;
95
+ message: Schema.SchemaClass<string, string, never>;
96
+ }>], [Schema.Struct<{
97
+ locale: Schema.Literal<["ja", "en", "ko"]>;
98
+ message: Schema.SchemaClass<string, string, never>;
99
+ }>]>>;
100
+ }>>>;
101
+ id: Schema.refine<string, typeof Schema.String>;
102
+ displayName: Schema.optional<Schema.SchemaClass<string, string, never>>;
103
+ hidden: Schema.optional<Schema.Schema<string, string, never>>;
104
+ required: Schema.optional<Schema.Schema<string, string, never>>;
105
+ validations: Schema.optional<Schema.Array$<Schema.Struct<{
106
+ when: Schema.Schema<string, string, never>;
107
+ warning: Schema.optional<Schema.SchemaClass<boolean, boolean, never>>;
108
+ message: Schema.refine<readonly [{
109
+ readonly locale: "zh";
110
+ readonly message: string;
111
+ }, ...{
112
+ readonly locale: "en" | "ja" | "ko";
113
+ readonly message: string;
114
+ }[]], Schema.TupleType<readonly [Schema.Struct<{
115
+ locale: Schema.Literal<["zh"]>;
116
+ message: Schema.SchemaClass<string, string, never>;
117
+ }>], [Schema.Struct<{
118
+ locale: Schema.Literal<["ja", "en", "ko"]>;
119
+ message: Schema.SchemaClass<string, string, never>;
120
+ }>]>>;
121
+ }>>>;
122
+ type: Schema.Literal<["com.shwfed.form.field.upload"]>;
123
+ compatibilityDate: Schema.Literal<["2026-06-09"]>;
124
+ }>;
125
+ export type Value = Schema.Schema.Type<ReturnType<typeof schema>>;
126
+ export declare function migrate(prev: unknown): Effect.Effect<unknown, Error>;
@@ -0,0 +1,178 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { Expression, LocaleMarkdown } from "../../../../../share/expression.js";
3
+ import { Locale } from "../../../../../share/locale.js";
4
+ import { commonFieldFields, FieldOrientationSchema } from "../../../utils/common.js";
5
+ export const type = "com.shwfed.form.field.upload";
6
+ export const compatibilityDate = "2026-06-09";
7
+ export const metadata = {
8
+ name: "\u6587\u4EF6\u4E0A\u4F20",
9
+ icon: "fluent:cloud-arrow-up-20-regular",
10
+ w: { initial: 8, min: 8, max: Infinity },
11
+ h: { initial: 6, min: 6, max: Infinity, grow: true }
12
+ };
13
+ export const JSON_VAR = {
14
+ type: "dyn",
15
+ label: "HTTP \u54CD\u5E94\u4F53",
16
+ description: "\u7B2C\u4E00\u6B65 `\u8BF7\u6C42` \u7684\u54CD\u5E94\u4F53\uFF08\u5DF2\u89E3\u6790 JSON\uFF09\uFF1B\u7528\u4E8E\u6784\u9020\u7B2C\u4E8C\u6B65\u4E0B\u8F7D\u8BF7\u6C42"
17
+ };
18
+ export const FILES_VAR = {
19
+ type: "list<dyn>",
20
+ label: "\u5DF2\u9009\u6587\u4EF6",
21
+ description: "\u672C\u6B21\u9009\u62E9\u7684\u6587\u4EF6\u5217\u8868\uFF08`File` \u6570\u7EC4\uFF09\uFF1B\u901A\u5E38\u901A\u8FC7 `form({...})` \u6784\u9020 multipart \u8BF7\u6C42\u4F53"
22
+ };
23
+ export const UPLOAD_JSON_VAR = {
24
+ type: "dyn",
25
+ label: "\u4E0A\u4F20\u54CD\u5E94",
26
+ description: "\u4E0A\u4F20 `\u8BF7\u6C42` \u7684\u54CD\u5E94\u4F53\uFF08\u5DF2\u89E3\u6790 JSON\uFF09\uFF1B\u7528\u4E8E\u63D0\u53D6\u5DF2\u4E0A\u4F20\u6587\u4EF6\u9879"
27
+ };
28
+ export const FILE_VAR = {
29
+ type: "dyn",
30
+ label: "\u6587\u4EF6\u9879",
31
+ description: "\u8868\u5355\u72B6\u6001\u4E2D\u7684\u5355\u4E2A\u6587\u4EF6\u9879\uFF08\u670D\u52A1\u7AEF\u56DE\u586B\u6216\u4E0A\u4F20\u54CD\u5E94\u6620\u5C04\u6240\u5F97\uFF09\uFF1B\u8FD4\u56DE\u5176\u5C55\u793A\u6587\u4EF6\u540D"
32
+ };
33
+ function withVar(configure, name, spec) {
34
+ return (env) => {
35
+ configure(env);
36
+ env.registerVariable(name, spec.type, { description: spec.description });
37
+ };
38
+ }
39
+ export function schema(configure) {
40
+ const CelBool = Expression({ configure, resultType: "bool" });
41
+ const CelHttpRequest = Expression({ configure, resultType: "HttpRequest" });
42
+ const CelDownloadRequest = Expression({ configure: withVar(configure, "json", JSON_VAR), resultType: "HttpRequest" });
43
+ const CelUploadRequest = Expression({ configure: withVar(configure, "files", FILES_VAR), resultType: "HttpRequest" });
44
+ const CelUploadHandle = Expression({ configure: withVar(configure, "json", UPLOAD_JSON_VAR), resultType: "list" });
45
+ const CelItemName = Expression({ configure: withVar(configure, "file", FILE_VAR), resultType: "string" });
46
+ const LocaleMd = LocaleMarkdown({ configure });
47
+ return Schema.Struct({
48
+ type: Schema.Literal(type),
49
+ compatibilityDate: Schema.Literal(compatibilityDate),
50
+ ...commonFieldFields(configure),
51
+ label: Schema.optional(Locale.annotations({
52
+ title: "\u6807\u7B7E",
53
+ description: "\u5B57\u6BB5\u524D\u5C55\u793A\u7684\u6587\u672C\uFF1B\u7559\u7A7A\u5219\u4E0D\u6E32\u67D3\u6807\u7B7E"
54
+ })),
55
+ description: Schema.optional(LocaleMd.annotations({
56
+ title: "\u8BF4\u660E",
57
+ description: "\u5C55\u793A\u5728\u4E0A\u4F20\u533A\u57DF\u4E0A\u65B9\u7684\u591A\u8BED\u8A00\u8BF4\u660E\uFF0C\u652F\u6301 Markdown \u4E0E `{{ form.foo }}` \u63D2\u503C"
58
+ })),
59
+ placeholder: Schema.optional(Locale.annotations({
60
+ title: "\u5360\u4F4D\u7B26",
61
+ description: "\u4E0A\u4F20\u533A\u57DF\u4E3A\u7A7A\u65F6\u7684\u63D0\u793A\u6587\u672C"
62
+ })),
63
+ tooltip: Schema.optional(Locale.annotations({
64
+ title: "\u63D0\u793A",
65
+ description: "\u9F20\u6807\u60AC\u505C\u5728\u6807\u7B7E\u4E0A\u65F6\u5C55\u793A\u7684\u8BF4\u660E"
66
+ })),
67
+ orientation: Schema.optional(FieldOrientationSchema),
68
+ binding: Schema.optional(Schema.String.pipe(Schema.minLength(1)).annotations({
69
+ title: "\u7ED1\u5B9A\u8DEF\u5F84",
70
+ description: "\u5199\u5165\u8868\u5355\u72B6\u6001\u7684 `dot-prop` \u8DEF\u5F84\uFF0C\u4F8B\u5982 `attachments.5du4fbuv`\uFF1B\u7559\u7A7A\u5219\u4E3A\u975E\u53D7\u63A7\u5B57\u6BB5"
71
+ })),
72
+ disabled: Schema.optional(CelBool.annotations({
73
+ title: "\u7981\u7528\u6761\u4EF6",
74
+ description: "\u8FD4\u56DE `true` \u65F6\u4E0A\u4F20\u533A\u57DF\u4ECD\u7136\u6E32\u67D3\u4F46\u4E0D\u53EF\u64CD\u4F5C"
75
+ })),
76
+ readonly: Schema.optional(CelBool.annotations({
77
+ title: "\u53EA\u8BFB\u6761\u4EF6",
78
+ description: "\u8FD4\u56DE `true` \u65F6\u4EC5\u4EE5\u6587\u4EF6\u5217\u8868\u5C55\u793A\u5F53\u524D\u503C\uFF0C\u4E0D\u5141\u8BB8\u589E\u5220"
79
+ })),
80
+ multiple: Schema.optional(Schema.Boolean.annotations({
81
+ title: "\u5141\u8BB8\u591A\u6587\u4EF6",
82
+ description: "\u5F00\u542F\u540E\u53EF\u4E00\u6B21\u9009\u62E9/\u62D6\u5165\u591A\u4E2A\u6587\u4EF6\uFF1B\u5173\u95ED\u65F6\u65B0\u6587\u4EF6\u4F1A\u66FF\u6362\u5DF2\u9009\u6587\u4EF6"
83
+ })),
84
+ accept: Schema.optional(Schema.Array(Schema.String).annotations({
85
+ title: "\u5141\u8BB8\u7684\u6587\u4EF6\u7C7B\u578B",
86
+ description: "\u5141\u8BB8\u4E0A\u4F20\u7684 MIME \u7C7B\u578B\u5217\u8868\uFF1B\u7559\u7A7A\u8868\u793A\u4E0D\u9650\u5236"
87
+ })),
88
+ maxFileSize: Schema.optional(Schema.Number.pipe(Schema.positive()).annotations({
89
+ title: "\u5355\u6587\u4EF6\u5927\u5C0F\u9650\u5236",
90
+ description: "\u5355\u4E2A\u6587\u4EF6\u5141\u8BB8\u7684\u6700\u5927\u5B57\u8282\u6570\uFF1B\u8D85\u8FC7\u6B64\u9650\u5236\u7684\u6587\u4EF6\u4F1A\u88AB\u62D2\u7EDD"
91
+ })),
92
+ maxTotalSize: Schema.optional(Schema.Number.pipe(Schema.positive()).annotations({
93
+ title: "\u603B\u5927\u5C0F\u9650\u5236",
94
+ description: "\u6240\u6709\u5DF2\u9009\u6587\u4EF6\u7D2F\u8BA1\u7684\u6700\u5927\u5B57\u8282\u6570\uFF1B\u65B0\u589E\u5BFC\u81F4\u8D85\u51FA\u65F6\u4F1A\u88AB\u62D2\u7EDD"
95
+ })),
96
+ maxFiles: Schema.optional(Schema.Number.pipe(Schema.int(), Schema.positive()).annotations({
97
+ title: "\u6587\u4EF6\u6570\u91CF\u9650\u5236",
98
+ description: "\u5141\u8BB8\u540C\u65F6\u4E0A\u4F20\u7684\u6700\u5927\u6587\u4EF6\u6570\uFF1B\u8FBE\u5230\u4E0A\u9650\u540E\u65B0\u589E\u6587\u4EF6\u4F1A\u88AB\u62D2\u7EDD"
99
+ })),
100
+ // Immediate upload. Its presence enables "upload on pick": each picked file
101
+ // is POSTed right away (`request`), the response is mapped into the stored
102
+ // items (`handle`), and each stored item's display name is resolved via
103
+ // `filename`. Absent → files are held as raw `File[]` and submitted with the
104
+ // form (deferred mode). The canonical→backend write mapping lives in the
105
+ // submit action, not here, so the field stays backend-agnostic.
106
+ upload: Schema.optional(Schema.Struct({
107
+ request: CelUploadRequest.annotations({
108
+ title: "\u4E0A\u4F20\u8BF7\u6C42",
109
+ description: '\u8FD4\u56DE `HttpRequest` \u7684 CEL \u8868\u8FBE\u5F0F\uFF1B\u53EF\u901A\u8FC7 `files` \u5F15\u7528\u672C\u6B21\u9009\u62E9\u7684\u6587\u4EF6\uFF0C\u4F8B\u5982 `http.post(url).body(form({"files": files}))`\u3002\u4EE5 `.json()` \u53D1\u8D77'
110
+ }),
111
+ handle: CelUploadHandle.annotations({
112
+ title: "\u5904\u7406\u54CD\u5E94",
113
+ description: '\u8FD4\u56DE**\u5217\u8868**\u7684 CEL \u8868\u8FBE\u5F0F\uFF0C\u53EF\u901A\u8FC7 `json` \u5F15\u7528\u4E0A\u4F20\u54CD\u5E94\uFF1B\u6620\u5C04\u4E3A\u8FFD\u52A0\u5230\u7ED1\u5B9A\u503C\u7684\u6587\u4EF6\u9879\uFF0C\u4F8B\u5982 `json.data.map(d, {"cacheKey": d.key, "filename": d.fileOriginalName})`'
114
+ }),
115
+ filename: Schema.optional(CelItemName.annotations({
116
+ title: "\u6587\u4EF6\u540D",
117
+ description: "\u8FD4\u56DE `string` \u7684 CEL \u8868\u8FBE\u5F0F\uFF0C\u53EF\u901A\u8FC7 `file` \u5F15\u7528\u5355\u4E2A\u6587\u4EF6\u9879\uFF1B\u7528\u4E8E\u5C55\u793A\u6587\u4EF6\u540D\uFF0C\u4F8B\u5982 `file.filename`\u3002\u7559\u7A7A\u65F6\u56DE\u9000\u5230 `filename` / `name` \u5B57\u6BB5"
118
+ }))
119
+ }).annotations({
120
+ title: "\u5373\u65F6\u4E0A\u4F20",
121
+ description: "\u914D\u7F6E\u540E\u6587\u4EF6\u5728\u9009\u62E9\u65F6\u7ACB\u5373\u4E0A\u4F20\uFF0C\u7ED1\u5B9A\u503C\u5B58\u50A8\u6620\u5C04\u540E\u7684\u6587\u4EF6\u9879\uFF08\u800C\u975E\u539F\u59CB `File`\uFF09\uFF1B\u4E0D\u914D\u7F6E\u5219\u4FDD\u7559\u6587\u4EF6\u968F\u8868\u5355\u4E00\u5E76\u63D0\u4EA4"
122
+ })),
123
+ // Download templates — each entry renders one button next to the upload
124
+ // zone. `request` is the (possibly only) step; `download` is an optional
125
+ // second step that may reference the first response via `json`.
126
+ templates: Schema.optional(Schema.Array(Schema.Struct({
127
+ request: CelHttpRequest.annotations({
128
+ title: "\u8BF7\u6C42",
129
+ description: "\u8FD4\u56DE `HttpRequest` \u7684 CEL \u8868\u8FBE\u5F0F\u3002\u672A\u914D\u7F6E\u300C\u4E0B\u8F7D\u8BF7\u6C42\u300D\u65F6\u76F4\u63A5\u53D1\u8D77\u5E76\u4E0B\u8F7D\u5176\u54CD\u5E94\uFF1B\u914D\u7F6E\u4E86\u300C\u4E0B\u8F7D\u8BF7\u6C42\u300D\u65F6\u4F5C\u4E3A\u83B7\u53D6\u4E0B\u8F7D\u51ED\u636E\u7684\u7B2C\u4E00\u6B65\u8BF7\u6C42"
130
+ }),
131
+ download: Schema.optional(CelDownloadRequest.annotations({
132
+ title: "\u4E0B\u8F7D\u8BF7\u6C42",
133
+ description: "\u53EF\u9009\u7684\u7B2C\u4E8C\u6B65\uFF1A\u8FD4\u56DE `HttpRequest` \u7684 CEL \u8868\u8FBE\u5F0F\uFF0C\u53EF\u901A\u8FC7 `json` \u5F15\u7528\u7B2C\u4E00\u6B65\u7684\u54CD\u5E94\u4F53\uFF0C\u4F8B\u5982\u7528 `json.data.key` \u6784\u9020\u771F\u6B63\u7684\u4E0B\u8F7D\u8BF7\u6C42"
134
+ })),
135
+ icon: Schema.optional(Schema.String.pipe(Schema.minLength(1)).annotations({
136
+ title: "\u6A21\u677F\u6309\u94AE\u56FE\u6807",
137
+ description: "Iconify \u56FE\u6807\u6807\u8BC6\u7B26\uFF0C\u4F8B\u5982 `fluent:arrow-download-20-regular`\uFF1B\u7559\u7A7A\u65F6\u4F7F\u7528\u9ED8\u8BA4\u56FE\u6807"
138
+ })),
139
+ label: Schema.optional(Locale.annotations({
140
+ title: "\u6A21\u677F\u6309\u94AE\u6587\u672C",
141
+ description: "\u4E0B\u8F7D\u6A21\u677F\u6309\u94AE\u4E0A\u7684\u672C\u5730\u5316\u6587\u672C\uFF1B\u7559\u7A7A\u65F6\u4F7F\u7528\u300C\u4E0B\u8F7D\u6A21\u677F\u300D"
142
+ }))
143
+ })).annotations({
144
+ title: "\u4E0B\u8F7D\u6A21\u677F",
145
+ description: "\u4E0B\u8F7D\u6A21\u677F\u5217\u8868\uFF1B\u6BCF\u4E00\u9879\u5728\u4E0A\u4F20\u533A\u57DF\u65C1\u6E32\u67D3\u4E00\u4E2A\u4E0B\u8F7D\u6309\u94AE"
146
+ }))
147
+ }).annotations({
148
+ title: "UploadField",
149
+ description: "\u6587\u4EF6\u4E0A\u4F20\u5B57\u6BB5"
150
+ });
151
+ }
152
+ export function migrate(prev) {
153
+ return Effect.try({
154
+ try: () => {
155
+ if (typeof prev !== "object" || prev === null) {
156
+ throw new Error("\u4E0A\u4F20\u5B57\u6BB5\u8FC1\u79FB\u5931\u8D25\uFF1A\u539F\u914D\u7F6E\u4E0D\u662F\u5BF9\u8C61");
157
+ }
158
+ const {
159
+ template,
160
+ templateIcon,
161
+ templateLabel,
162
+ compatibilityDate: _drop,
163
+ ...rest
164
+ } = prev;
165
+ const next = { ...rest, compatibilityDate };
166
+ if (template != null && typeof template === "object" && typeof template.request === "string") {
167
+ const t = template;
168
+ const entry = { request: t.request };
169
+ if (typeof t.download === "string") entry.download = t.download;
170
+ if (typeof templateIcon === "string") entry.icon = templateIcon;
171
+ if (templateLabel != null) entry.label = templateLabel;
172
+ next.templates = [entry];
173
+ }
174
+ return next;
175
+ },
176
+ catch: (error) => error instanceof Error ? error : new Error(String(error))
177
+ });
178
+ }
@@ -40,6 +40,13 @@ export declare function findOperation(type: string, compatibilityDate: string):
40
40
  * the request's `params` to the registered `handler` with `ctx`. Unknown
41
41
  * refs are skipped. Hosts merge the result with any bespoke param-less
42
42
  * handlers (e.g. the page's `close`).
43
+ *
44
+ * When the request carries an emit-site evaluator (`request.cel`, the
45
+ * dispatching button's own scope), it overrides `ctx.cel` for that one call —
46
+ * so a handler's CEL-bearing params resolve where they were authored (the
47
+ * button, with `row` live) instead of against the host. Handlers read `ctx.cel`
48
+ * unchanged; the swap is invisible to them. Falls back to the host's evaluator
49
+ * when the emitter supplied none.
43
50
  */
44
51
  export declare function operationHandlers(exposed: ReadonlyArray<{
45
52
  type: string;
@@ -70,7 +70,7 @@ export function operationHandlers(exposed, ctx) {
70
70
  for (const { type, compatibilityDate } of exposed) {
71
71
  const entry = findOperation(type, compatibilityDate);
72
72
  if (!entry) continue;
73
- handlers[type] = (params) => entry.handler(params, ctx);
73
+ handlers[type] = (params, cel) => entry.handler(params, cel ? { ...ctx, cel } : ctx);
74
74
  }
75
75
  return handlers;
76
76
  }
@@ -15,6 +15,7 @@ const props = defineProps({
15
15
  });
16
16
  const celContext = injectCELContext();
17
17
  const eventChannel = useEventChannel();
18
+ const triggerCel = (expression, context) => $cel(expression, { ...celBindings(celContext), ...context });
18
19
  const rawValue = computed(() => props.ctx.cell.getValue());
19
20
  const isMissing = computed(() => rawValue.value === void 0 || rawValue.value === null);
20
21
  const checked = computed(() => rawValue.value === true);
@@ -54,7 +55,7 @@ async function onUpdate(next) {
54
55
  console.error("[shwfed-table] switch successMessage failed", e);
55
56
  }
56
57
  }
57
- await Effect.runPromise(dispatchTriggers(eventChannel, props.column.triggers));
58
+ await Effect.runPromise(dispatchTriggers(eventChannel, props.column.triggers, triggerCel));
58
59
  } catch (e) {
59
60
  console.error("[shwfed-table] switch onChange failed", e);
60
61
  toast.error("\u8BF7\u6C42\u5931\u8D25");
@@ -15,6 +15,7 @@ const props = defineProps({
15
15
  });
16
16
  const celContext = injectCELContext();
17
17
  const eventChannel = useEventChannel();
18
+ const triggerCel = (expression, context) => $cel(expression, { ...celBindings(celContext), ...context });
18
19
  const rawValue = computed(() => props.ctx.cell.getValue());
19
20
  const isMissing = computed(() => rawValue.value === void 0 || rawValue.value === null);
20
21
  const checked = computed(() => rawValue.value === true);
@@ -54,7 +55,7 @@ async function onUpdate(next) {
54
55
  console.error("[shwfed-table] switch successMessage failed", e);
55
56
  }
56
57
  }
57
- await Effect.runPromise(dispatchTriggers(eventChannel, props.column.triggers));
58
+ await Effect.runPromise(dispatchTriggers(eventChannel, props.column.triggers, triggerCel));
58
59
  } catch (e) {
59
60
  console.error("[shwfed-table] switch onChange failed", e);
60
61
  toast.error("\u8BF7\u6C42\u5931\u8D25");
@@ -204,7 +204,7 @@ async function submit(next) {
204
204
  console.error("[shwfed-table] combobox-single.remote.options-remote successMessage failed", e);
205
205
  }
206
206
  }
207
- await Effect.runPromise(dispatchTriggers(eventChannel, props.column.triggers));
207
+ await Effect.runPromise(dispatchTriggers(eventChannel, props.column.triggers, $cel));
208
208
  } catch (e) {
209
209
  console.error("[shwfed-table] combobox-single.remote.options-remote onChange failed", e);
210
210
  toast.error("\u8BF7\u6C42\u5931\u8D25");
@@ -126,7 +126,7 @@ async function submit(next) {
126
126
  console.error("[shwfed-table] combobox-single.remote.options-static successMessage failed", e);
127
127
  }
128
128
  }
129
- await Effect.runPromise(dispatchTriggers(eventChannel, props.column.triggers));
129
+ await Effect.runPromise(dispatchTriggers(eventChannel, props.column.triggers, $cel));
130
130
  } catch (e) {
131
131
  console.error("[shwfed-table] combobox-single.remote.options-static onChange failed", e);
132
132
  toast.error("\u8BF7\u6C42\u5931\u8D25");
@@ -218,7 +218,7 @@ async function submit(next) {
218
218
  console.error("[shwfed-table] combobox-multi.remote.options-remote successMessage failed", e);
219
219
  }
220
220
  }
221
- await Effect.runPromise(dispatchTriggers(eventChannel, props.column.triggers));
221
+ await Effect.runPromise(dispatchTriggers(eventChannel, props.column.triggers, $cel));
222
222
  } catch (e) {
223
223
  console.error("[shwfed-table] combobox-multi.remote.options-remote onChange failed", e);
224
224
  toast.error("\u8BF7\u6C42\u5931\u8D25");
@@ -140,7 +140,7 @@ async function submit(next) {
140
140
  console.error("[shwfed-table] combobox-multi.remote.options-static successMessage failed", e);
141
141
  }
142
142
  }
143
- await Effect.runPromise(dispatchTriggers(eventChannel, props.column.triggers));
143
+ await Effect.runPromise(dispatchTriggers(eventChannel, props.column.triggers, $cel));
144
144
  } catch (e) {
145
145
  console.error("[shwfed-table] combobox-multi.remote.options-static onChange failed", e);
146
146
  toast.error("\u8BF7\u6C42\u5931\u8D25");
@@ -253,7 +253,7 @@ async function submit(next) {
253
253
  console.error("[shwfed-table] combobox-multi.remote successMessage failed", e);
254
254
  }
255
255
  }
256
- await Effect.runPromise(dispatchTriggers(eventChannel, props.column.triggers));
256
+ await Effect.runPromise(dispatchTriggers(eventChannel, props.column.triggers, $cel));
257
257
  } catch (e) {
258
258
  console.error("[shwfed-table] combobox-multi.remote onChange failed", e);
259
259
  toast.error("\u8BF7\u6C42\u5931\u8D25");
@@ -233,7 +233,7 @@ async function submit(next) {
233
233
  console.error("[shwfed-table] combobox-single.remote successMessage failed", e);
234
234
  }
235
235
  }
236
- await Effect.runPromise(dispatchTriggers(eventChannel, props.column.triggers));
236
+ await Effect.runPromise(dispatchTriggers(eventChannel, props.column.triggers, $cel));
237
237
  } catch (e) {
238
238
  console.error("[shwfed-table] combobox-single.remote onChange failed", e);
239
239
  toast.error("\u8BF7\u6C42\u5931\u8D25");
@@ -292,7 +292,7 @@ async function submit(next) {
292
292
  console.error("[shwfed-table] combobox-multi successMessage failed", e);
293
293
  }
294
294
  }
295
- await Effect.runPromise(dispatchTriggers(eventChannel, triggers));
295
+ await Effect.runPromise(dispatchTriggers(eventChannel, triggers, $cel));
296
296
  } catch (e) {
297
297
  console.error("[shwfed-table] combobox-multi onChange failed", e);
298
298
  toast.error("\u8BF7\u6C42\u5931\u8D25");
@@ -271,7 +271,7 @@ async function submit(next) {
271
271
  console.error("[shwfed-table] combobox-single successMessage failed", e);
272
272
  }
273
273
  }
274
- await Effect.runPromise(dispatchTriggers(eventChannel, triggers));
274
+ await Effect.runPromise(dispatchTriggers(eventChannel, triggers, $cel));
275
275
  } catch (e) {
276
276
  console.error("[shwfed-table] combobox-single onChange failed", e);
277
277
  toast.error("\u8BF7\u6C42\u5931\u8D25");
@@ -1,5 +1,6 @@
1
1
  import { Effect, Schema } from 'effect';
2
2
  import type { InjectionKey, Ref } from 'vue';
3
+ import type { CelEvaluator } from './interpolate.js';
3
4
  /** A descendant button asking an ancestor instance to run an operation. */
4
5
  export type OpRequest = Readonly<{
5
6
  /** Instance id of the ancestor that should handle this request. */
@@ -13,6 +14,17 @@ export type OpRequest = Readonly<{
13
14
  * CEL/locale in them resolves, is the receiving operation's own concern.
14
15
  */
15
16
  params?: unknown;
17
+ /**
18
+ * The *emit-site* CEL evaluator — the dispatching button's own context, where
19
+ * descendant bindings like a table row's `row` are live. Params are authored
20
+ * on the trigger row that sits on the button, so any CEL in them reads most
21
+ * naturally in this scope, not the handling ancestor's. Carried (not the
22
+ * pre-rendered string) so interpolation stays in the receiving operation,
23
+ * which alone knows which param fields are CEL — the bus stays param-opaque.
24
+ * Absent when the emitter has no CEL context; the handler then falls back to
25
+ * its host's own evaluator.
26
+ */
27
+ cel?: CelEvaluator;
16
28
  }>;
17
29
  /**
18
30
  * The persisted form of a trigger — one stored row addressing an ancestor
@@ -79,7 +91,7 @@ export type EventChannel = Readonly<{
79
91
  * pre-request trigger list, so validation is explicit and configured rather
80
92
  * than an implicit host gate.
81
93
  */
82
- export type OperationHandlers = Readonly<Record<string, (params?: unknown) => Effect.Effect<void>>>;
94
+ export type OperationHandlers = Readonly<Record<string, (params?: unknown, cel?: CelEvaluator) => Effect.Effect<void>>>;
83
95
  export declare const EVENT_CHANNEL_KEY: InjectionKey<EventChannel>;
84
96
  /**
85
97
  * Build a channel node for an operation-exposing instance. `dispatch` runs
@@ -109,8 +121,14 @@ export declare function useEventChannel(): EventChannel | undefined;
109
121
  *
110
122
  * A button that fires triggers composes this into its runtime effect at the
111
123
  * lifecycle point the triggers should fire — the bus no longer runs them.
124
+ *
125
+ * `cel` is the emitter's own evaluator, stamped onto every request so a
126
+ * parameterized operation can resolve CEL in its params against the *emit-site*
127
+ * scope (e.g. a table row's `row`) rather than the handling ancestor's. Pass it
128
+ * whenever the emitter has a CEL context; omit it and ops fall back to their
129
+ * host evaluator. See `OpRequest.cel`.
112
130
  */
113
- export declare function dispatchTriggers(channel: EventChannel | undefined, triggers: ReadonlyArray<TriggerValue> | undefined): Effect.Effect<void>;
131
+ export declare function dispatchTriggers(channel: EventChannel | undefined, triggers: ReadonlyArray<TriggerValue> | undefined, cel?: CelEvaluator): Effect.Effect<void>;
114
132
  /**
115
133
  * One operation a component type exposes. `id` is what the trigger stores;
116
134
  * `name`/`icon` are display only.
@@ -33,7 +33,7 @@ export const EVENT_CHANNEL_KEY = Symbol("shwfed/event-channel");
33
33
  export function createEventChannel(instanceId, operations, parent) {
34
34
  return {
35
35
  dispatch(request) {
36
- const handled = request.target === instanceId ? operations[request.operation]?.(request.params) ?? Effect.void : Effect.void;
36
+ const handled = request.target === instanceId ? operations[request.operation]?.(request.params, request.cel) ?? Effect.void : Effect.void;
37
37
  return parent ? Effect.andThen(handled, parent.dispatch(request)) : handled;
38
38
  }
39
39
  };
@@ -45,14 +45,15 @@ export function provideEventTarget(instanceId, operations) {
45
45
  export function useEventChannel() {
46
46
  return inject(EVENT_CHANNEL_KEY, void 0);
47
47
  }
48
- export function dispatchTriggers(channel, triggers) {
48
+ export function dispatchTriggers(channel, triggers, cel) {
49
49
  if (!channel || !triggers || triggers.length === 0) return Effect.void;
50
50
  return Effect.forEach(
51
51
  triggers,
52
52
  (trigger) => channel.dispatch({
53
53
  target: trigger.target,
54
54
  operation: trigger.operation,
55
- params: trigger.params
55
+ params: trigger.params,
56
+ cel
56
57
  }),
57
58
  { discard: true }
58
59
  );
@@ -26,7 +26,7 @@ The custom `Optional` class has been replaced with Effect's `Option` type (`impo
26
26
 
27
27
  An `http` built-in has been added (`http-builtins.ts`, `http-builder.ts`) — not from upstream. It registers the `http` constant and the `http` / `HttpRequest` types on `globalRegistry`, so `http.get(url).header(...).body(...)` expressions type-check and evaluate in every `Environment`. A CEL expression only builds an `HttpRequestBuilder` — a pure description of a request; it never issues one. Both terminal methods, `.json()` and `.file()`, are dispatched by the host on the returned builder (neither is a CEL method), so expression evaluation stays free of IO. Endpoints must be absolute URLs — there is no base-URL resolution. Depends on `fx-fetch`. The host can register process-wide default headers via `HttpRequestBuilder.setDefaultHeader(name, valueOrGetter)` / `clearDefaultHeader(name)`; they are merged in at `#buildRequest()` time, with case-insensitive precedence to an explicit `.header(...)` on the builder. A getter that returns `null` / `undefined` / `''` skips the header for that request, so the host can source values from live refs (e.g. the active i18n locale for `Accept-Language`) without baking in stale snapshots.
28
28
 
29
- A `form(dyn): FormData` built-in has been added (`form-builtins.ts`) — not from upstream. It registers the `FormData` type on `globalRegistry` and a global `form` function that turns a CEL map into a native `FormData`, so authors can write `http.post(url).body(form({"file": myFile, "name": "Alice"}))` (`.body()` already passes `FormData` through verbatim and `fetch` sets the multipart boundary). Value coercion mirrors `.query()`'s flat-record semantics: `null` / `undefined` / `Option.None` skip the key; `Option.Some(x)` recurses on `x`; `File` / `Blob` pass through (preserves the `File` filename); `Decimal` → `.toString()` (preserves precision past safe-integer); `Date` (`TZDate`) → `.toISOString()`; `bool` → `'true'` / `'false'`; arrays append one entry per element; nested objects throw (`multipart/form-data` is flat — same rule as `.query(dyn)`). Top-level input must be a map literal / record (Map or plain object); passing an array or scalar throws.
29
+ A `form(dyn): FormData` built-in has been added (`form-builtins.ts`) — not from upstream. It registers the `FormData` type on `globalRegistry` and a global `form` function that turns a CEL map into a native `FormData`, so authors can write `http.post(url).body(form({"file": myFile, "name": "Alice"}))` (`.body()` passes `FormData` through verbatim; `#buildRequest()` serializes it). NOTE: `#buildRequest()` must serialize a `FormData` body itself rather than hand it to fx-fetch — fx-fetch clones bodies via `new Response(body).blob()`, and the File API ASCII-lowercases `Blob#type`, so in Chrome the multipart boundary in the derived `Content-Type` loses its case while the body bytes keep it (boundaries are case-sensitive → every parser sees zero parts). The builder lifts the case-preserved `Content-Type` off the `Response` *header* (header values are never case-normalized), adds it as an explicit header (unless the author set their own `Content-Type`), and passes the bytes as a `Promise<Blob>` (fx-fetch resolves promises verbatim). Value coercion mirrors `.query()`'s flat-record semantics: `null` / `undefined` / `Option.None` skip the key; `Option.Some(x)` recurses on `x`; `File` / `Blob` pass through (preserves the `File` filename); `Decimal` → `.toString()` (preserves precision past safe-integer); `Date` (`TZDate`) → `.toISOString()`; `bool` → `'true'` / `'false'`; arrays append one entry per element; nested objects throw (`multipart/form-data` is flat — same rule as `.query(dyn)`). Top-level input must be a map literal / record (Map or plain object); passing an array or scalar throws.
30
30
  The `__proto__` / `constructor` / `prototype` keys are skipped on the top-level record, mirroring `safeFromEntries`.
31
31
 
32
32
  A `JSON` built-in namespace has been added (`json-builtins.ts`) — not from upstream. It registers a `JSON` brand class and a same-named constant on `globalRegistry`, so `JSON.parse(string): dyn` and `JSON.stringify(dyn): string` resolve in every `Environment`. The CEL-side surface mirrors the JS globals deliberately. `JSON.parse` is a thin wrapper over `globalThis.JSON.parse` — numbers come back as plain JS `number` (the evaluator coerces via `Decimal.from` at use time). `JSON.stringify` pre-walks the value via `normalizeForJSON` before handing it to `globalThis.JSON.stringify`: `Decimal` → JS number (via `.toNumber()` — loses precision past safe-integer range, deliberate trade-off for idiomatic JSON output), `Option.Some(x)` → `x`, `Option.None` → `null`, `Map` → plain object, `Date`/`TZDate` passes through so its built-in `toJSON` emits ISO 8601. The pre-walk is required because Effect's `Option` defines its own `toJSON()` that runs before any replacer would see it.
@@ -198,8 +198,8 @@ http.post("https://api.example.com/files")
198
198
  .body(form({"file": myFile, "name": "Alice", "tags": ["a", "b"]}))
199
199
  ```
200
200
  `form(map): FormData` turns a flat record into a native `FormData`. Pass it
201
- straight to `.body(...)` — `fetch` sets the multipart boundary, so you don't
202
- need a `Content-Type` header.
201
+ straight to `.body(...)` — the builder derives the multipart `Content-Type`
202
+ (boundary included), so you don't need a `Content-Type` header.
203
203
 
204
204
  Value coercion is flat (same rules as `.query(...)`):
205
205
  - `null` / `undefined` / absent `.?` values → key is skipped
@@ -134,6 +134,19 @@ export class HttpRequestBuilder {
134
134
  const explicit = new Set(this.#headers.map(([k]) => k.toLowerCase()));
135
135
  const headers = {};
136
136
  for (const [k, v] of this.#headers) headers[k] = v;
137
+ if (this.#body !== void 0) {
138
+ if (this.#body instanceof FormData) {
139
+ const serialized = new Response(this.#body);
140
+ const contentType = serialized.headers.get("content-type");
141
+ if (contentType !== null && !explicit.has("content-type")) {
142
+ headers["Content-Type"] = contentType;
143
+ explicit.add("content-type");
144
+ }
145
+ parts.body = serialized.blob();
146
+ } else {
147
+ parts.body = this.#body;
148
+ }
149
+ }
137
150
  for (const [key, { name, get }] of HttpRequestBuilder.#defaultHeaders) {
138
151
  if (explicit.has(key)) continue;
139
152
  const v = get();
@@ -143,9 +156,6 @@ export class HttpRequestBuilder {
143
156
  if (Object.keys(headers).length > 0) {
144
157
  parts.headers = headers;
145
158
  }
146
- if (this.#body !== void 0) {
147
- parts.body = this.#body;
148
- }
149
159
  let request = FxRequest.unsafeMake(parts);
150
160
  for (const [k, v] of this.#queries) {
151
161
  request = FxRequest.appendUrlSearchParam(request, k, v);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shwfed/config",
3
- "version": "2.9.6",
3
+ "version": "2.9.8",
4
4
  "description": "Configurable UI for SHWFED",
5
5
  "type": "module",
6
6
  "publishConfig": {