@shwfed/config 2.1.0 → 2.1.2

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 (67) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/form/ai/fields-button.d.vue.ts +13 -0
  3. package/dist/runtime/components/form/ai/fields-button.vue +458 -0
  4. package/dist/runtime/components/form/ai/fields-button.vue.d.ts +13 -0
  5. package/dist/runtime/components/form/ai/fields-task.md +71 -0
  6. package/dist/runtime/components/form/config.d.vue.ts +1 -1
  7. package/dist/runtime/components/form/config.vue +4 -36
  8. package/dist/runtime/components/form/config.vue.d.ts +1 -1
  9. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.daterange/config.d.vue.ts +18 -18
  10. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.daterange/config.vue.d.ts +18 -18
  11. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.datetime/config.d.vue.ts +4 -4
  12. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.datetime/config.vue.d.ts +4 -4
  13. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.datetimerange/config.d.vue.ts +22 -22
  14. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.datetimerange/config.vue.d.ts +22 -22
  15. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.time/config.d.vue.ts +2 -2
  16. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.time/config.vue.d.ts +2 -2
  17. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.timerange/config.d.vue.ts +4 -4
  18. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.timerange/config.vue.d.ts +4 -4
  19. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.numberrange/config.d.vue.ts +12 -12
  20. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.numberrange/config.vue +87 -11
  21. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.numberrange/config.vue.d.ts +12 -12
  22. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.numberrange/runtime.vue +18 -6
  23. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.numberrange/schema.d.ts +1 -1
  24. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.numberrange/schema.js +11 -4
  25. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.switch/config.d.vue.ts +10 -10
  26. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.switch/config.vue.d.ts +10 -10
  27. package/dist/runtime/components/form/fields/2026-05-12/com.shwfed.form.field.upload/config.d.vue.ts +12 -12
  28. package/dist/runtime/components/form/fields/2026-05-12/com.shwfed.form.field.upload/config.vue.d.ts +12 -12
  29. package/dist/runtime/components/form/fields/2026-05-13/com.shwfed.form.field.list/config.d.vue.ts +16 -2
  30. package/dist/runtime/components/form/fields/2026-05-13/com.shwfed.form.field.list/config.vue +37 -0
  31. package/dist/runtime/components/form/fields/2026-05-13/com.shwfed.form.field.list/config.vue.d.ts +16 -2
  32. package/dist/runtime/components/form/fields/2026-05-13/com.shwfed.form.field.list/row.d.vue.ts +1 -0
  33. package/dist/runtime/components/form/fields/2026-05-13/com.shwfed.form.field.list/row.vue +13 -4
  34. package/dist/runtime/components/form/fields/2026-05-13/com.shwfed.form.field.list/row.vue.d.ts +1 -0
  35. package/dist/runtime/components/form/fields/2026-05-13/com.shwfed.form.field.list/runtime.vue +111 -22
  36. package/dist/runtime/components/form/fields/2026-05-13/com.shwfed.form.field.list/schema.d.ts +15 -1
  37. package/dist/runtime/components/form/fields/2026-05-13/com.shwfed.form.field.list/schema.js +9 -1
  38. package/dist/runtime/components/form/index.vue +5 -4
  39. package/dist/runtime/components/form/unit-config.d.vue.ts +16 -0
  40. package/dist/runtime/components/form/unit-config.vue +30 -3
  41. package/dist/runtime/components/form/unit-config.vue.d.ts +16 -0
  42. package/dist/runtime/components/form/utils/cel-scope.d.ts +13 -0
  43. package/dist/runtime/components/form/utils/cel-scope.js +32 -0
  44. package/dist/runtime/components/form/utils/schema-meta.d.ts +13 -0
  45. package/dist/runtime/components/form/utils/schema-meta.js +15 -0
  46. package/dist/runtime/components/table/ai/columns-task.md +10 -1
  47. package/dist/runtime/components/table/columns/2026-04-14/com.shwfed.table.column.markdown/config.vue +2 -2
  48. package/dist/runtime/components/table/columns/2026-04-14/com.shwfed.table.column.markdown/runtime.vue +14 -4
  49. package/dist/runtime/components/table/columns/2026-04-14/com.shwfed.table.column.markdown/schema.js +3 -2
  50. package/dist/runtime/components/table/columns/2026-04-14/com.shwfed.table.column.text/config.vue +2 -2
  51. package/dist/runtime/components/table/columns/2026-04-14/com.shwfed.table.column.text/runtime.vue +14 -4
  52. package/dist/runtime/components/table/columns/2026-04-14/com.shwfed.table.column.text/schema.js +3 -2
  53. package/dist/runtime/components/table/utils/shared.d.ts +2 -1
  54. package/dist/runtime/components/ui/date-range-picker/DateRangePickerDateTimePanel.d.vue.ts +1 -1
  55. package/dist/runtime/components/ui/date-range-picker/DateRangePickerDateTimePanel.vue.d.ts +1 -1
  56. package/dist/runtime/components/ui/date-range-picker/DateRangePickerTimeInput.d.vue.ts +1 -1
  57. package/dist/runtime/components/ui/date-range-picker/DateRangePickerTimeInput.vue.d.ts +1 -1
  58. package/dist/runtime/share/expression.d.ts +1 -2
  59. package/dist/runtime/share/slot-renderer.vue +7 -6
  60. package/dist/runtime/utils/interpolate.d.ts +1 -0
  61. package/dist/runtime/utils/interpolate.js +7 -0
  62. package/dist/runtime/vendor/cel-js/CLAUDE.md +1 -1
  63. package/dist/runtime/vendor/cel-js/PROMPT.md +6 -1
  64. package/dist/runtime/vendor/cel-js/lib/http-builder.d.ts +3 -2
  65. package/dist/runtime/vendor/cel-js/lib/http-builtins.d.ts +4 -3
  66. package/dist/runtime/vendor/cel-js/lib/http-builtins.js +4 -0
  67. package/package.json +1 -1
@@ -10,7 +10,8 @@ import { provideFormState } from "../../../utils/state";
10
10
  defineOptions({ name: "ShwfedListFieldRow" });
11
11
  const state = defineModel({ type: Object, ...{ required: true } });
12
12
  const props = defineProps({
13
- unit: { type: Object, required: true }
13
+ unit: { type: Object, required: true },
14
+ index: { type: Number, required: true }
14
15
  });
15
16
  const formState = provideFormState(state);
16
17
  provideCELContext({
@@ -19,6 +20,12 @@ provideCELContext({
19
20
  label: "item",
20
21
  description: "\u5F53\u524D\u5217\u8868\u9879",
21
22
  value: () => state.value ?? {}
23
+ },
24
+ index: {
25
+ type: "number",
26
+ label: "index",
27
+ description: "\u5F53\u524D\u5217\u8868\u9879\u7D22\u5F15\uFF0C\u4ECE 0 \u5F00\u59CB",
28
+ value: () => props.index
22
29
  }
23
30
  });
24
31
  const inherited = injectCELContext();
@@ -83,14 +90,16 @@ const gridStyle = computed(() => {
83
90
  const hi = Math.min(l.rows, y2 - 1);
84
91
  for (let r = lo; r <= hi; r++) grows[r - 1] = true;
85
92
  }
86
- rowTemplate = grows.map((g) => g ? "auto" : "minmax(0, 1fr)").join(" ");
93
+ rowTemplate = grows.map((g) => g ? "auto" : "minmax(auto, 1fr)").join(" ");
87
94
  }
95
+ const gap = `calc(${l.gap ?? DEFAULT_GAP} * 0.25rem)`;
96
+ const colGap = l.columns > 1 ? `min(${gap}, calc(100% / ${l.columns - 1}))` : gap;
88
97
  const parts = [
89
98
  "display: grid",
90
99
  `grid-template-columns: ${colTemplate}`,
91
100
  rowTemplate ? `grid-template-rows: ${rowTemplate}` : "",
92
- // Missing `gap` falls back to `DEFAULT_GAP`; see ../../../index.vue.
93
- `gap: calc(${l.gap ?? DEFAULT_GAP} * 0.25rem)`,
101
+ `column-gap: ${colGap}`,
102
+ `row-gap: ${gap}`,
94
103
  l.style ?? ""
95
104
  ].filter(Boolean);
96
105
  return parts.join("; ");
@@ -2,6 +2,7 @@ import type { FormUnitValue } from '../../../schema.js';
2
2
  import { type FormState } from '../../../utils/state.js';
3
3
  type __VLS_Props = {
4
4
  unit: FormUnitValue;
5
+ index: number;
5
6
  };
6
7
  type __VLS_ModelProps = {
7
8
  modelValue: FormState;
@@ -5,6 +5,7 @@ import { computed, ref, watch } from "vue";
5
5
  import { useI18n } from "vue-i18n";
6
6
  import { cel as _rawCel } from "../../../../../utils/cel";
7
7
  import { celBindings, injectCELContext } from "../../../../../utils/cel-context";
8
+ import { interpolate } from "../../../../../utils/interpolate";
8
9
  import { getLocalizedText } from "../../../../../share/locale";
9
10
  import {
10
11
  useTreeDnd
@@ -31,6 +32,8 @@ const tooltipText = computed(
31
32
  const addLabelText = computed(
32
33
  () => getLocalizedText(props.config.addLabel, locale.value) || "\u65B0\u589E"
33
34
  );
35
+ const summaryTemplate = computed(() => getLocalizedText(props.config.locale, locale.value));
36
+ const collapsible = computed(() => !!summaryTemplate.value);
34
37
  function evalBool(expression, label) {
35
38
  if (!expression) return false;
36
39
  try {
@@ -50,6 +53,28 @@ const items = computed(() => {
50
53
  if (!Array.isArray(raw)) return [];
51
54
  return raw;
52
55
  });
56
+ const rowSummaries = computed(() => {
57
+ const template = summaryTemplate.value;
58
+ if (template == null) return [];
59
+ return items.value.map(
60
+ (item, i) => interpolate(template, (expression) => {
61
+ try {
62
+ const out = Effect.runSync($cel(expression, {
63
+ form: state.value ?? {},
64
+ item: item ?? {},
65
+ index: i
66
+ }));
67
+ return out == null ? "" : String(out);
68
+ } catch (err) {
69
+ console.error(
70
+ `[shwfed-form] list field: failed to evaluate summary expression \`${expression}\` for ${props.fieldId}[${i}]:`,
71
+ err
72
+ );
73
+ return "";
74
+ }
75
+ })
76
+ );
77
+ });
53
78
  function writeItems(next) {
54
79
  const path = props.config.binding;
55
80
  if (path == null) {
@@ -74,6 +99,16 @@ watch(
74
99
  },
75
100
  { immediate: true }
76
101
  );
102
+ const expandedKeys = ref(/* @__PURE__ */ new Set());
103
+ function isExpanded(key) {
104
+ return expandedKeys.value.has(key);
105
+ }
106
+ function toggleExpanded(key) {
107
+ const next = new Set(expandedKeys.value);
108
+ if (next.has(key)) next.delete(key);
109
+ else next.add(key);
110
+ expandedKeys.value = next;
111
+ }
77
112
  const atMax = computed(
78
113
  () => props.config.max != null && items.value.length >= props.config.max
79
114
  );
@@ -86,13 +121,23 @@ const canReorder = computed(() => !isDisabled.value && reorderable.value);
86
121
  function append() {
87
122
  if (!canAdd.value) return;
88
123
  writeItems([...items.value, {}]);
89
- rowKeys.value = [...rowKeys.value, makeId()];
124
+ const id = makeId();
125
+ rowKeys.value = [...rowKeys.value, id];
126
+ const next = new Set(expandedKeys.value);
127
+ next.add(id);
128
+ expandedKeys.value = next;
90
129
  }
91
130
  function removeAt(i) {
92
131
  if (!canRemove.value) return;
93
132
  if (i < 0 || i >= items.value.length) return;
133
+ const removedKey = rowKeys.value[i];
94
134
  writeItems(items.value.filter((_, j) => j !== i));
95
135
  rowKeys.value = rowKeys.value.filter((_, j) => j !== i);
136
+ if (removedKey != null && expandedKeys.value.has(removedKey)) {
137
+ const next = new Set(expandedKeys.value);
138
+ next.delete(removedKey);
139
+ expandedKeys.value = next;
140
+ }
96
141
  }
97
142
  function moveRow(from, to) {
98
143
  if (!canReorder.value) return;
@@ -169,37 +214,81 @@ function rowConfig(i) {
169
214
  v-for="(item, i) in items"
170
215
  :key="rowKeys[i]"
171
216
  :ref="dnd.rowRef(rowId(i), rowConfig(i))"
172
- class="relative flex items-center gap-2 rounded border border-zinc-200 bg-zinc-50/40 p-3"
217
+ class="relative rounded border border-zinc-200 bg-zinc-50/40 p-3"
173
218
  data-slot="list-row"
174
219
  :data-instruction="dnd.instructionFor(rowId(i)) ?? void 0"
175
220
  >
176
- <Icon
177
- v-if="reorderable"
178
- icon="fluent:re-order-dots-vertical-20-regular"
179
- class="drag-handle size-4 shrink-0 text-zinc-400"
180
- :class="canReorder ? 'cursor-grab' : 'cursor-not-allowed opacity-50'"
181
- data-slot="list-row-drag-handle"
182
- />
221
+ <div class="flex items-center gap-2">
222
+ <Icon
223
+ v-if="reorderable"
224
+ icon="fluent:re-order-dots-vertical-20-regular"
225
+ class="drag-handle size-4 shrink-0 text-zinc-400"
226
+ :class="canReorder ? 'cursor-grab' : 'cursor-not-allowed opacity-50'"
227
+ data-slot="list-row-drag-handle"
228
+ />
229
+
230
+ <InputGroupButton
231
+ v-if="collapsible"
232
+ size="icon-xs"
233
+ class="shrink-0"
234
+ data-slot="list-row-collapse"
235
+ :aria-expanded="isExpanded(rowId(i))"
236
+ :aria-label="isExpanded(rowId(i)) ? '\u6298\u53E0' : '\u5C55\u5F00'"
237
+ @click="toggleExpanded(rowId(i))"
238
+ >
239
+ <Icon
240
+ :icon="isExpanded(rowId(i)) ? 'fluent:chevron-down-20-regular' : 'fluent:chevron-right-20-regular'"
241
+ />
242
+ </InputGroupButton>
183
243
 
184
- <div class="min-w-0 flex-1">
244
+ <div
245
+ v-if="collapsible"
246
+ class="min-w-0 flex-1"
247
+ data-slot="list-row-summary"
248
+ >
249
+ <Markdown
250
+ :source="rowSummaries[i] ?? ''"
251
+ class="prose prose-xs prose-zinc"
252
+ />
253
+ </div>
254
+ <div
255
+ v-else
256
+ class="min-w-0 flex-1"
257
+ >
258
+ <Row
259
+ :model-value="item"
260
+ :unit="config.unit"
261
+ :index="i"
262
+ @update:model-value="(next) => updateRow(i, next)"
263
+ />
264
+ </div>
265
+
266
+ <InputGroupButton
267
+ variant="destructive"
268
+ size="icon-xs"
269
+ class="shrink-0"
270
+ :disabled="!canRemove"
271
+ data-slot="list-row-remove"
272
+ aria-label="删除"
273
+ @click="removeAt(i)"
274
+ >
275
+ <Icon icon="fluent:delete-20-regular" />
276
+ </InputGroupButton>
277
+ </div>
278
+
279
+ <div
280
+ v-if="collapsible"
281
+ v-show="isExpanded(rowId(i))"
282
+ class="mt-3 min-w-0"
283
+ data-slot="list-row-content"
284
+ >
185
285
  <Row
186
286
  :model-value="item"
187
287
  :unit="config.unit"
288
+ :index="i"
188
289
  @update:model-value="(next) => updateRow(i, next)"
189
290
  />
190
291
  </div>
191
-
192
- <InputGroupButton
193
- variant="destructive"
194
- size="icon-xs"
195
- class="shrink-0"
196
- :disabled="!canRemove"
197
- data-slot="list-row-remove"
198
- aria-label="删除"
199
- @click="removeAt(i)"
200
- >
201
- <Icon icon="fluent:delete-20-regular" />
202
- </InputGroupButton>
203
292
  </div>
204
293
 
205
294
  <Button
@@ -36,15 +36,22 @@ export declare function schema(configure: (env: Environment) => void): Schema.re
36
36
  readonly locale: "en" | "ja" | "ko";
37
37
  readonly message: string;
38
38
  }[]] | undefined;
39
+ readonly locale?: readonly [{
40
+ readonly locale: "zh";
41
+ readonly message: string;
42
+ }, ...{
43
+ readonly locale: "en" | "ja" | "ko";
44
+ readonly message: string;
45
+ }[]] | undefined;
39
46
  readonly displayName?: string | undefined;
40
47
  readonly compatibilityDate: "2026-05-13";
41
48
  readonly min?: number | undefined;
42
49
  readonly max?: number | undefined;
50
+ readonly binding?: string | undefined;
43
51
  readonly unit: Readonly<{
44
52
  fields: ReadonlyArray<import("../../../schema.js").FieldValue>;
45
53
  layouts: ReadonlyArray<import("../../../schema.js").LayoutSetValue>;
46
54
  }>;
47
- readonly binding?: string | undefined;
48
55
  readonly addLabel?: readonly [{
49
56
  readonly locale: "zh";
50
57
  readonly message: string;
@@ -94,6 +101,13 @@ export declare function schema(configure: (env: Environment) => void): Schema.re
94
101
  min: Schema.optional<Schema.refine<number, Schema.filter<typeof Schema.Number>>>;
95
102
  max: Schema.optional<Schema.refine<number, Schema.filter<typeof Schema.Number>>>;
96
103
  disabled: Schema.optional<Schema.Schema<string, string, never>>;
104
+ locale: Schema.optional<Schema.TupleType<readonly [Schema.Struct<{
105
+ locale: Schema.Literal<["zh"]>;
106
+ message: Schema.SchemaClass<string, string, never>;
107
+ }>], [Schema.Struct<{
108
+ locale: Schema.Literal<["ja", "en", "ko"]>;
109
+ message: Schema.SchemaClass<string, string, never>;
110
+ }>]>>;
97
111
  unit: Schema.suspend<Readonly<{
98
112
  fields: ReadonlyArray<import("../../../schema.js").FieldValue>;
99
113
  layouts: ReadonlyArray<import("../../../schema.js").LayoutSetValue>;
@@ -15,9 +15,13 @@ export function schema(configure) {
15
15
  const CelBool = Expression({ configure, resultType: "bool" });
16
16
  const unitConfigure = (env) => {
17
17
  configure(env);
18
- if (!env.getDefinitions().variables.some((v) => v.name === "item")) {
18
+ const declared = new Set(env.getDefinitions().variables.map((v) => v.name));
19
+ if (!declared.has("item")) {
19
20
  env.registerVariable("item", "dyn", { description: "\u5F53\u524D\u5217\u8868\u9879" });
20
21
  }
22
+ if (!declared.has("index")) {
23
+ env.registerVariable("index", "number", { description: "\u5F53\u524D\u5217\u8868\u9879\u7D22\u5F15\uFF0C\u4ECE 0 \u5F00\u59CB" });
24
+ }
21
25
  };
22
26
  const Unit = Schema.suspend(() => FormUnit(unitConfigure));
23
27
  return Schema.Struct({
@@ -60,6 +64,10 @@ export function schema(configure) {
60
64
  title: "\u7981\u7528\u6761\u4EF6",
61
65
  description: "\u8FD4\u56DE `true` \u65F6\u6574\u4E2A\u5217\u8868\u53EA\u8BFB\uFF0C\u4E0D\u80FD\u65B0\u589E/\u5220\u9664/\u91CD\u6392"
62
66
  })),
67
+ locale: Schema.optional(Locale.annotations({
68
+ title: "\u5217\u8868\u9879\u6458\u8981",
69
+ description: "\u914D\u7F6E\u540E\u6BCF\u4E2A\u5217\u8868\u9879\u53EF\u6298\u53E0\uFF0C\u6298\u53E0\u65F6\u5C55\u793A\u6B64\u5904\u6587\u672C\u6E32\u67D3\u7684\u884C\u5185 Markdown \u4F5C\u4E3A\u6458\u8981\u3002\u652F\u6301 `{{ \u8868\u8FBE\u5F0F }}` \u63D2\u503C\uFF0C\u8868\u8FBE\u5F0F\u4E3A CEL\uFF0C\u53EF\u8BBF\u95EE `item`\uFF08\u5F53\u524D\u9879\uFF09\u3001`index`\uFF08\u5E8F\u53F7\uFF0C\u4ECE 0 \u5F00\u59CB\uFF09\u4E0E `form`"
70
+ })),
63
71
  unit: Unit.annotations({
64
72
  title: "\u5217\u8868\u9879\u914D\u7F6E",
65
73
  description: "\u5355\u4E2A\u5217\u8868\u9879\u7684\u5B57\u6BB5\u4E0E\u5E03\u5C40\uFF1B\u6BCF\u4E2A\u5217\u8868\u9879\u90FD\u6309\u6B64\u7ED3\u6784\u6E32\u67D3\u4E00\u4EFD"
@@ -167,15 +167,16 @@ const gridStyle = computed(() => {
167
167
  const hi = Math.min(l.rows, y2 - 1);
168
168
  for (let r = lo; r <= hi; r++) grows[r - 1] = true;
169
169
  }
170
- rowTemplate = grows.map((g) => g ? "auto" : "minmax(0, 1fr)").join(" ");
170
+ rowTemplate = grows.map((g) => g ? "auto" : "minmax(auto, 1fr)").join(" ");
171
171
  }
172
+ const gap = `calc(${l.gap ?? DEFAULT_GAP} * 0.25rem)`;
173
+ const colGap = l.columns > 1 ? `min(${gap}, calc(100% / ${l.columns - 1}))` : gap;
172
174
  const parts = [
173
175
  "display: grid",
174
176
  `grid-template-columns: ${colTemplate}`,
175
177
  rowTemplate ? `grid-template-rows: ${rowTemplate}` : "",
176
- // A missing `gap` resolves to `DEFAULT_GAP`, not 0 — same fallback the
177
- // layout editor shows, so editor and render agree. `gap: 0` stays 0.
178
- `gap: calc(${l.gap ?? DEFAULT_GAP} * 0.25rem)`,
178
+ `column-gap: ${colGap}`,
179
+ `row-gap: ${gap}`,
179
180
  l.style ?? ""
180
181
  ].filter(Boolean);
181
182
  return parts.join("; ");
@@ -1,4 +1,6 @@
1
+ import { type CELContext } from '../../utils/cel-context.js';
1
2
  import type { FieldValue, FormUnitValue } from './schema.js';
3
+ import type { Environment } from '../../vendor/cel-js/lib/index.js';
2
4
  export type UnitExtra = {
3
5
  id: string;
4
6
  label: string;
@@ -17,6 +19,20 @@ type __VLS_Props = {
17
19
  * v-model — hosts read which extra is active via `selection.value`.
18
20
  */
19
21
  extras?: ReadonlyArray<UnitExtra>;
22
+ /**
23
+ * Host-registered CEL variables. Forwarded to the drilled field-list's
24
+ * "AI 生成字段" button so the model can advertise host variables in
25
+ * generated expressions. Absent → only `form` / `now` are in scope.
26
+ */
27
+ configure?: (env: Environment) => void;
28
+ /**
29
+ * Extra CEL variables in scope for the unit's *field* editors — the list
30
+ * field passes `item` (the current row). Advertised to every drilled field
31
+ * config's ExpressionEditor via `provideCELContext` below; the host's own
32
+ * `extras-pane` slot is excluded, since slotted content injects through the
33
+ * host component, above this provide.
34
+ */
35
+ fieldCelScope?: CELContext;
20
36
  };
21
37
  type __VLS_Slots = {
22
38
  /** Pane content for the currently-selected extra. Receives `{ id }`. */
@@ -6,7 +6,8 @@ import { computed, inject, onBeforeUnmount, provide, ref, useTemplateRef, watch
6
6
  import { toast } from "vue-sonner";
7
7
  import {
8
8
  celBindings,
9
- injectCELContext
9
+ injectCELContext,
10
+ provideCELContext
10
11
  } from "../../utils/cel-context";
11
12
  import { findFreePlacement, normalizeLayoutSet } from "../../share/layout";
12
13
  import { getLocalizedText } from "../../share/locale";
@@ -20,13 +21,16 @@ import { Button } from "../ui/button";
20
21
  import { ScrollArea } from "../ui/scroll-area";
21
22
  import { Separator } from "../ui/separator";
22
23
  import { FIELDS, findField } from "./utils/resolve";
24
+ import ShwfedFormAiFieldsButton from "./ai/fields-button.vue";
23
25
  defineOptions({ name: "ShwfedFormUnitConfig" });
24
26
  const unit = defineModel({ type: Object, ...{ required: true } });
25
27
  const selection = defineModel("selection", { type: Object, ...{
26
28
  default: () => ({ kind: "layout" })
27
29
  } });
28
- defineProps({
29
- extras: { type: Array, required: false }
30
+ const props = defineProps({
31
+ extras: { type: Array, required: false },
32
+ configure: { type: Function, required: false },
33
+ fieldCelScope: { type: Object, required: false }
30
34
  });
31
35
  defineSlots();
32
36
  const activeLayoutIndex = ref(0);
@@ -40,6 +44,7 @@ function popOne() {
40
44
  const breadcrumbExt = inject(BREADCRUMB_EXTENSION_KEY, null);
41
45
  const fullPane = ref(false);
42
46
  provide(FORM_FIELD_LAYOUT_KEY, { fullPane });
47
+ provideCELContext(props.fieldCelScope ?? {});
43
48
  const takeover = inject(SIDEBAR_TAKEOVER_KEY, null);
44
49
  const takeoverTarget = computed(() => takeover?.target.value ?? null);
45
50
  const activeFieldId = computed(() => {
@@ -171,6 +176,16 @@ function removeFields(ids) {
171
176
  stack.value = stack.value.slice(0, -1);
172
177
  }
173
178
  }
179
+ function applyAiFields(generated) {
180
+ const valid = generated.filter(
181
+ (f) => !!findField(f.type, f.compatibilityDate)
182
+ );
183
+ if (valid.length === 0) return;
184
+ unit.value = {
185
+ ...unit.value,
186
+ fields: [...unit.value.fields, ...valid]
187
+ };
188
+ }
174
189
  function updateActiveField(next) {
175
190
  const id = activeFieldId.value;
176
191
  if (!id) return;
@@ -311,6 +326,12 @@ function isExtraActive(id) {
311
326
  </button>
312
327
  </div>
313
328
  </ScrollArea>
329
+
330
+ <ShwfedFormAiFieldsButton
331
+ :configure="configure"
332
+ :fields="unit.fields"
333
+ @apply="applyAiFields"
334
+ />
314
335
  </template>
315
336
  </Teleport>
316
337
 
@@ -388,6 +409,12 @@ function isExtraActive(id) {
388
409
  </button>
389
410
  </div>
390
411
  </ScrollArea>
412
+
413
+ <ShwfedFormAiFieldsButton
414
+ :configure="configure"
415
+ :fields="unit.fields"
416
+ @apply="applyAiFields"
417
+ />
391
418
  </template>
392
419
  </div>
393
420
 
@@ -1,4 +1,6 @@
1
+ import { type CELContext } from '../../utils/cel-context.js';
1
2
  import type { FieldValue, FormUnitValue } from './schema.js';
3
+ import type { Environment } from '../../vendor/cel-js/lib/index.js';
2
4
  export type UnitExtra = {
3
5
  id: string;
4
6
  label: string;
@@ -17,6 +19,20 @@ type __VLS_Props = {
17
19
  * v-model — hosts read which extra is active via `selection.value`.
18
20
  */
19
21
  extras?: ReadonlyArray<UnitExtra>;
22
+ /**
23
+ * Host-registered CEL variables. Forwarded to the drilled field-list's
24
+ * "AI 生成字段" button so the model can advertise host variables in
25
+ * generated expressions. Absent → only `form` / `now` are in scope.
26
+ */
27
+ configure?: (env: Environment) => void;
28
+ /**
29
+ * Extra CEL variables in scope for the unit's *field* editors — the list
30
+ * field passes `item` (the current row). Advertised to every drilled field
31
+ * config's ExpressionEditor via `provideCELContext` below; the host's own
32
+ * `extras-pane` slot is excluded, since slotted content injects through the
33
+ * host component, above this provide.
34
+ */
35
+ fieldCelScope?: CELContext;
20
36
  };
21
37
  type __VLS_Slots = {
22
38
  /** Pane content for the currently-selected extra. Receives `{ id }`. */
@@ -0,0 +1,13 @@
1
+ import { Environment } from '../../../vendor/cel-js/lib/index.js';
2
+ import type { CELContext } from '../../../utils/cel-context.js';
3
+ /**
4
+ * Builds the CEL variable scope advertised to a form's expression editors and
5
+ * AI assists. Seeds the form-wide `form` / `now` live variables, then probes a
6
+ * throwaway `Environment` with the host's `configure` to surface any
7
+ * host-registered variables (their type / label / description).
8
+ *
9
+ * Values are left `undefined` — this scope is for designer-time advertising
10
+ * and prompt rendering, never evaluation. Extracted from `form/config.vue` so
11
+ * the field-list AI button can reuse the exact same scope.
12
+ */
13
+ export declare function buildFormCelScope(configure: (env: Environment) => void): CELContext;
@@ -0,0 +1,32 @@
1
+ import { Environment } from "../../../vendor/cel-js/lib/index.js";
2
+ export function buildFormCelScope(configure) {
3
+ const probe = new Environment({ unlistedVariablesAreDyn: false });
4
+ const baseline = new Set(probe.getDefinitions().variables.map((v) => v.name));
5
+ configure(probe);
6
+ const out = {
7
+ form: {
8
+ type: "dyn",
9
+ label: "\u8868\u5355\u503C",
10
+ description: "\u5F53\u524D\u8868\u5355\u72B6\u6001",
11
+ value: void 0
12
+ },
13
+ // Mirrors the runtime `now` binding in `form/index.vue`. Editors and
14
+ // prompts never evaluate, so we leave `value` undefined.
15
+ now: {
16
+ type: "Date",
17
+ label: "now",
18
+ description: "\u5F53\u524D\u65E5\u671F/\u65F6\u95F4",
19
+ value: void 0
20
+ }
21
+ };
22
+ for (const v of probe.getDefinitions().variables) {
23
+ if (baseline.has(v.name)) continue;
24
+ out[v.name] = {
25
+ type: v.type,
26
+ label: v.label ?? v.name,
27
+ description: v.description ?? void 0,
28
+ value: void 0
29
+ };
30
+ }
31
+ return out;
32
+ }
@@ -2,4 +2,17 @@ import { type Schema } from 'effect';
2
2
  type AnySchema = Schema.Schema<any, any, any>;
3
3
  export declare function getStructFieldTitle(schema: AnySchema, fieldName: string): string | undefined;
4
4
  export declare function getStructFieldDescription(schema: AnySchema, fieldName: string): string | undefined;
5
+ export type StructFieldInfo = {
6
+ name: string;
7
+ title?: string;
8
+ description?: string;
9
+ optional: boolean;
10
+ };
11
+ /**
12
+ * Lists a struct schema's property signatures, pairing each with its `@title` /
13
+ * `@description` annotation and optionality. Used to auto-derive the per-field
14
+ * documentation surfaced to the AI field generator — a new field folder becomes
15
+ * discoverable with zero edits there. Returns `[]` for non-struct schemas.
16
+ */
17
+ export declare function listStructFields(schema: AnySchema): StructFieldInfo[];
5
18
  export {};
@@ -46,3 +46,18 @@ export function getStructFieldTitle(schema, fieldName) {
46
46
  export function getStructFieldDescription(schema, fieldName) {
47
47
  return getFieldAnnotation(schema, fieldName, SchemaAST.getDescriptionAnnotation);
48
48
  }
49
+ export function listStructFields(schema) {
50
+ const tl = getTypeLiteral(schema.ast);
51
+ if (!tl) return [];
52
+ return tl.propertySignatures.map((ps) => {
53
+ const name = String(ps.name);
54
+ const title = getStructFieldTitle(schema, name);
55
+ const description = getStructFieldDescription(schema, name);
56
+ return {
57
+ name,
58
+ ...title !== void 0 ? { title } : {},
59
+ ...description !== void 0 ? { description } : {},
60
+ optional: ps.isOptional
61
+ };
62
+ });
63
+ }
@@ -4,7 +4,7 @@ The `columns` array is a **full replacement**: whatever you emit becomes the ent
4
4
 
5
5
  # Hard rules
6
6
 
7
- 1. **NEVER** emit a column with `type === "com.shwfed.table.column.actions"`. Action columns are wired by a separate registry; emitting one will be rejected and you will be retried.
7
+ 1. **NEVER add** a new column with `type === "com.shwfed.table.column.actions"`. Action columns are wired by a separate registry you cannot synthesize one, and a fabricated actions column will be rejected and you will be retried. The **one exception**: if the current configuration already contains an actions column, echo it back verbatim (same `id`, `type`, `compatibilityDate`, and all fields) in its render position. Never invent an actions column the user merely describes when none already exists.
8
8
  2. **ONLY** emit a column with `type === "com.shwfed.table.column.markdown"` when the user **explicitly** asks for markdown / rich content / formatted body / 富文本 / Markdown. Otherwise prefer `com.shwfed.table.column.text`.
9
9
  3. **NEVER** set `enableSorting: true`. Client-side sorting is rarely the right call in practice (paginated/server-driven data, mixed types, etc.) and the user should opt in manually. Omit the field or set it to `false`.
10
10
  4. **NEVER** wrap the `accessor` in type coercions (`string(...)`, `int(...)`, `double(...)`) and **NEVER** supply a default via `.orValue(...)`. Emit the bare optional access — e.g. `row.?name`, `row.?age`. Coercion and defaults make it impossible for the user to distinguish real data from filler, which is actively harmful.
@@ -41,4 +41,13 @@ Always use optional access (`row.?field`) rather than `row.field` to avoid runti
41
41
 
42
42
  If the user gives a sample response, derive accessors directly from its shape. Otherwise, infer field names from the user's prose (e.g. "show name and email" → `row.?name`, `row.?email`).
43
43
 
44
+ # Rendering recipes
45
+
46
+ **Boolean / `0`-`1` values.** When a field is a boolean or a `0`/`1` flag (e.g. `enabled`, `isActive`, a `0/1` status), render it with `com.shwfed.table.column.icon` rather than `text` — a green check / red cross reads far faster than the literal `true` or `1`. Build both the `accessor` and the `color` as CEL ternaries on the condition:
47
+
48
+ - `accessor`: `<condition> ? 'fluent:checkmark-20-filled' : 'fluent:dismiss-20-filled'`
49
+ - `color`: `<condition> ? '#00c950' : '#fb2c36'`
50
+
51
+ where `<condition>` is a bare optional-access test — `row.?enabled == true` for a boolean field, `row.?status == 1` for a `0/1` field. This is a comparison, not a coercion, so hard rule 4 still holds: do not wrap in `string(...)`/`int(...)` or add `.orValue(...)`.
52
+
44
53
  The current configuration (existing data source + columns) is appended to the user prompt under `Current configuration`. Use it only to (a) reuse existing column `id`s where the user's described column clearly corresponds to an existing one, and (b) inform field shapes (e.g. response keys). Do **not** copy columns forward that the user did not describe — the user's prompt is the source of truth for which columns should exist.
@@ -154,8 +154,8 @@ const copyExpressionModel = computed({
154
154
  </FieldLabel>
155
155
  <ExpressionEditor
156
156
  v-model="copyExpressionModel"
157
- placeholder="如 row.name"
158
- result-type="string"
157
+ placeholder="如 row.name 或 row.?name"
158
+ :result-type="['string', 'optional']"
159
159
  class="min-h-10"
160
160
  />
161
161
  </Field>
@@ -24,12 +24,22 @@ const rendered = computed(() => {
24
24
  const isEmpty = computed(() => rendered.value.trim().length === 0);
25
25
  const align = computed(() => isEmpty.value ? "center" : props.column.align ?? "left");
26
26
  const justifyClass = computed(() => JUSTIFY_CLASS[align.value] ?? JUSTIFY_CLASS.left);
27
- const showCopy = computed(() => !!props.column.copyExpression && !isEmpty.value);
27
+ const copyText = computed(() => {
28
+ const { copyExpression } = props.column;
29
+ if (!copyExpression) return void 0;
30
+ try {
31
+ const value = Effect.runSync($cel(copyExpression, celBindings(celContext)));
32
+ return value === void 0 || value === null ? void 0 : String(value);
33
+ } catch (e) {
34
+ console.error("[shwfed-table] markdown copy expression failed", e);
35
+ return void 0;
36
+ }
37
+ });
38
+ const showCopy = computed(() => copyText.value !== void 0 && !isEmpty.value);
28
39
  const onCopy = async () => {
29
- const { column } = props;
30
- if (!column.copyExpression) return;
40
+ const text = copyText.value;
41
+ if (text === void 0) return;
31
42
  try {
32
- const text = String(Effect.runSync($cel(column.copyExpression, celBindings(celContext))));
33
43
  await navigator.clipboard.writeText(text);
34
44
  } catch (e) {
35
45
  console.error("[shwfed-table] markdown copy failed", e);
@@ -7,8 +7,9 @@ export const metadata = {
7
7
  name: "MD",
8
8
  icon: "fluent:markdown-20-regular"
9
9
  };
10
+ const isCopyExpressionType = (t) => t === "string" || t === "dyn" || t.startsWith("optional");
10
11
  export function schema(configure) {
11
- const cel = CelRowAccess(configure, { resultType: "string" });
12
+ const cel = CelRowAccess(configure, { resultType: isCopyExpressionType });
12
13
  const localeMarkdown = LocaleMarkdownWithRow(configure);
13
14
  return Schema.Struct({
14
15
  type: Schema.Literal(type),
@@ -34,7 +35,7 @@ export function schema(configure) {
34
35
  }),
35
36
  copyExpression: Schema.optional(cel.annotations({
36
37
  title: "\u590D\u5236\u8868\u8FBE\u5F0F",
37
- description: "\u590D\u5236\u503C\u7684 CEL \u8868\u8FBE\u5F0F"
38
+ description: "\u590D\u5236\u503C\u7684 CEL \u8868\u8FBE\u5F0F\uFF1B\u53EF\u8FD4\u56DE optional\uFF08\u5982 `row.?field`\uFF09\uFF0C\u4E3A\u7A7A\u65F6\u4E0D\u663E\u793A\u590D\u5236\u6309\u94AE"
38
39
  }))
39
40
  }).annotations({ title: "MarkdownRenderer", description: "MD" });
40
41
  }
@@ -188,8 +188,8 @@ const copyExpressionModel = computed({
188
188
  </FieldLabel>
189
189
  <ExpressionEditor
190
190
  v-model="copyExpressionModel"
191
- placeholder="如 row.name"
192
- result-type="string"
191
+ placeholder="如 row.name 或 row.?name"
192
+ :result-type="['string', 'optional']"
193
193
  class="min-h-10"
194
194
  />
195
195
  </Field>