@shwfed/config 2.12.14 → 2.12.16

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 (47) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/actions/components/group.vue +1 -0
  3. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/config.d.vue.ts +68 -68
  4. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/config.vue.d.ts +68 -68
  5. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/runtime.d.vue.ts +68 -68
  6. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/runtime.vue.d.ts +68 -68
  7. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/schema.d.ts +34 -34
  8. package/dist/runtime/components/config/use-editor.js +16 -0
  9. package/dist/runtime/components/config/utils/linkage.d.ts +29 -0
  10. package/dist/runtime/components/config/utils/linkage.js +57 -0
  11. package/dist/runtime/components/config/utils/validation-error.d.ts +1 -0
  12. package/dist/runtime/components/config/utils/validation-error.js +1 -1
  13. package/dist/runtime/components/form/FormUnitRenderer.vue +6 -1
  14. package/dist/runtime/components/form/fields/2026-04-22/com.shwfed.form.field.textarea/config.d.vue.ts +2 -2
  15. package/dist/runtime/components/form/fields/2026-04-22/com.shwfed.form.field.textarea/config.vue.d.ts +2 -2
  16. package/dist/runtime/components/sidebar/use-editor.js +13 -0
  17. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +2 -2
  18. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +2 -2
  19. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +2 -2
  20. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +2 -2
  21. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +2 -2
  22. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +2 -2
  23. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +2 -2
  24. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +2 -2
  25. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +2 -2
  26. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +2 -2
  27. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +2 -2
  28. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +2 -2
  29. package/dist/runtime/components/table/config.vue +10 -4
  30. package/dist/runtime/components/table/index.vue +6 -1
  31. package/dist/runtime/components/table/schema.d.ts +68 -68
  32. package/dist/runtime/components/ui/expression-editor/ExpressionEditor.vue +21 -3
  33. package/dist/runtime/components/ui/expression-editor/index.d.ts +1 -0
  34. package/dist/runtime/components/ui/expression-editor/index.js +5 -0
  35. package/dist/runtime/components/ui/expression-editor/validity.d.ts +12 -0
  36. package/dist/runtime/components/ui/expression-editor/validity.js +27 -0
  37. package/dist/runtime/share/layout.js +2 -2
  38. package/dist/runtime/share/slot-renderer.vue +6 -1
  39. package/dist/runtime/share/viewport.d.ts +16 -0
  40. package/dist/runtime/share/viewport.js +11 -0
  41. package/dist/runtime/utils/cel-context.d.ts +15 -0
  42. package/dist/runtime/utils/cel-context.js +18 -1
  43. package/dist/runtime/utils/cel.js +6 -1
  44. package/dist/runtime/vendor/cel-js/CLAUDE.md +1 -1
  45. package/dist/runtime/vendor/cel-js/PROMPT.md +15 -0
  46. package/dist/runtime/vendor/cel-js/lib/functions.js +28 -0
  47. package/package.json +1 -1
@@ -45,10 +45,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
45
45
  readonly data: string;
46
46
  readonly request?: string | undefined;
47
47
  };
48
+ readonly enableSorting?: boolean | undefined;
48
49
  readonly nodeKey: string;
49
50
  readonly nodeChildren: string;
50
51
  readonly nodeLabel: string;
51
- readonly enableSorting?: boolean | undefined;
52
52
  readonly nodeTooltip?: readonly [{
53
53
  readonly locale: "zh";
54
54
  readonly message: string;
@@ -103,10 +103,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
103
103
  readonly data: string;
104
104
  readonly request?: string | undefined;
105
105
  };
106
+ readonly enableSorting?: boolean | undefined;
106
107
  readonly nodeKey: string;
107
108
  readonly nodeChildren: string;
108
109
  readonly nodeLabel: string;
109
- readonly enableSorting?: boolean | undefined;
110
110
  readonly nodeTooltip?: readonly [{
111
111
  readonly locale: "zh";
112
112
  readonly message: string;
@@ -45,10 +45,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
45
45
  readonly data: string;
46
46
  readonly request?: string | undefined;
47
47
  };
48
+ readonly enableSorting?: boolean | undefined;
48
49
  readonly nodeKey: string;
49
50
  readonly nodeChildren: string;
50
51
  readonly nodeLabel: string;
51
- readonly enableSorting?: boolean | undefined;
52
52
  readonly nodeTooltip?: readonly [{
53
53
  readonly locale: "zh";
54
54
  readonly message: string;
@@ -103,10 +103,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
103
103
  readonly data: string;
104
104
  readonly request?: string | undefined;
105
105
  };
106
+ readonly enableSorting?: boolean | undefined;
106
107
  readonly nodeKey: string;
107
108
  readonly nodeChildren: string;
108
109
  readonly nodeLabel: string;
109
- readonly enableSorting?: boolean | undefined;
110
110
  readonly nodeTooltip?: readonly [{
111
111
  readonly locale: "zh";
112
112
  readonly message: string;
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
  import { computed, defineComponent, inject, onBeforeUnmount, provide, ref, watch } from "vue";
3
- import { celBindings, injectCELContext, provideCELContext, provideScopeAncestor, provideSelectionRoster, SELECTIONS_VAR } from "../../utils/cel-context";
3
+ import { celBindings, injectCELContext, provideCELContext, provideRowUnreachableScope, provideScopeAncestor, provideSelectionRoster, SELECTIONS_VAR } from "../../utils/cel-context";
4
4
  import { BREADCRUMB_EXTENSION_KEY } from "../config/breadcrumb-extension";
5
5
  import { TABLE_COLUMN_LAYOUT_KEY } from "./column-layout";
6
6
  import { Icon } from "@iconify/vue";
@@ -152,6 +152,7 @@ const TableActionsCELContext = defineComponent({
152
152
  value: []
153
153
  }
154
154
  });
155
+ provideRowUnreachableScope();
155
156
  return () => slots.default?.();
156
157
  }
157
158
  });
@@ -305,14 +306,19 @@ const tableScopeAncestor = computed(() => {
305
306
  typeIcon: tableMetadata.icon,
306
307
  members: [
307
308
  { key: "row", label: "\u5F53\u524D\u884C", type: "dyn", description: "\u8BE5\u8868\u683C\u5F53\u524D\u884C\u7684\u6574\u884C\u6570\u636E" },
308
- { key: "index", label: "\u884C\u5E8F\u53F7", type: "number", description: "\u8BE5\u8868\u683C\u5F53\u524D\u884C\u7684\u5E8F\u53F7\uFF0C\u4ECE `0` \u5F00\u59CB" }
309
+ { key: "index", label: "\u884C\u5E8F\u53F7", type: "number", description: "\u8BE5\u8868\u683C\u5F53\u524D\u884C\u7684\u5E8F\u53F7\uFF0C\u4ECE `0` \u5F00\u59CB" },
310
+ // Registered at the table HOST frame (index.vue), so it is reachable from
311
+ // table-level actions (top toolbar / bottom bar) — where the relative
312
+ // `selected` is shadowed by an inner table — as well as from cells.
313
+ { key: "selected", label: "\u5DF2\u9009\u884C", type: "list<dyn>", description: "\u8BE5\u8868\u683C\u5F53\u524D\u52FE\u9009\u7684\u6240\u6709\u884C" }
309
314
  ],
310
315
  // Each row-provider rebinds the relative `selections` to its own per-row
311
316
  // registry (see row-provider.vue), so a table row shadows an enclosing
312
317
  // form's selection options even though the table exposes no `selections`
313
318
  // member of its own — list it here so that form's options stay offerable to
314
- // a cell expression inside this table.
315
- shadows: ["row", "index", SELECTIONS_VAR]
319
+ // a cell expression inside this table. `selected` likewise: the table host
320
+ // rebinds it, shadowing an enclosing table's selection for everything below.
321
+ shadows: ["row", "index", "selected", SELECTIONS_VAR]
316
322
  };
317
323
  });
318
324
  provideScopeAncestor(tableScopeAncestor);
@@ -17,7 +17,7 @@ import { Pagination, ScrollArea } from "reka-ui/namespaced";
17
17
  import { computed, h, onMounted, ref, toRaw, watch } from "vue";
18
18
  import { asRequest, fetchJsonOption } from "../../share/request";
19
19
  import { useI18n } from "vue-i18n";
20
- import { celBindings, celScope, provideCELContext, injectCELContext } from "../../utils/cel-context";
20
+ import { celBindings, celScope, provideCELContext, injectCELContext, provideScopeAddress } from "../../utils/cel-context";
21
21
  import TableRowProvider from "./row-provider.vue";
22
22
  import * as addRowOp from "../operations/2026-07-06/com.shwfed.operation.table.add-row/schema";
23
23
  import { getLocalizedText } from "../../share/locale";
@@ -353,6 +353,11 @@ provideCELContext({
353
353
  }
354
354
  }
355
355
  });
356
+ if (config.value?.id) {
357
+ provideScopeAddress(config.value.id, {
358
+ selected: () => tableApi.getSelectedRowModel().rows.map((r) => r.original)
359
+ });
360
+ }
356
361
  provideSharedFetchLayer();
357
362
  const hasDragHandleColumn = computed(() => (config.value?.columns ?? []).some((c) => c.type === DRAG_HANDLE_COLUMN_TYPE));
358
363
  const reorderDisabledReason = computed(() => {
@@ -158,40 +158,6 @@ export declare function TableConfig(configure: (env: Environment) => void): Sche
158
158
  readonly message: string;
159
159
  }[]] | undefined;
160
160
  }[];
161
- readonly query?: {
162
- readonly style?: string | undefined;
163
- readonly id?: string | undefined;
164
- readonly initial?: {
165
- readonly data: string;
166
- readonly request?: string | undefined;
167
- } | undefined;
168
- readonly displayName?: string | undefined;
169
- readonly kind: "shwfed.component.form";
170
- readonly fields: readonly any[];
171
- readonly layouts: readonly {
172
- readonly name: string;
173
- readonly layout: {
174
- readonly style?: string | undefined;
175
- readonly columns: number;
176
- readonly gap?: number | undefined;
177
- readonly rows?: number | undefined;
178
- readonly placements: {
179
- readonly [x: string]: {
180
- readonly area: readonly [readonly [number, number], readonly [number, number]];
181
- readonly v?: "stretch" | "center" | "end" | "start" | undefined;
182
- readonly h?: "stretch" | "center" | "end" | "start" | undefined;
183
- };
184
- };
185
- };
186
- readonly media?: string | undefined;
187
- }[];
188
- readonly readonly?: string | undefined;
189
- } | undefined;
190
- readonly dataSource?: {
191
- readonly data: string;
192
- readonly total?: string | undefined;
193
- readonly request?: string | undefined;
194
- } | undefined;
195
161
  readonly actions?: {
196
162
  readonly size: "default" | "sm" | "xs";
197
163
  readonly style?: string | undefined;
@@ -259,6 +225,40 @@ export declare function TableConfig(configure: (env: Environment) => void): Sche
259
225
  readonly variant?: "default" | "link" | "destructive" | "primary" | "ghost" | undefined;
260
226
  }[];
261
227
  } | undefined;
228
+ readonly query?: {
229
+ readonly style?: string | undefined;
230
+ readonly id?: string | undefined;
231
+ readonly initial?: {
232
+ readonly data: string;
233
+ readonly request?: string | undefined;
234
+ } | undefined;
235
+ readonly displayName?: string | undefined;
236
+ readonly kind: "shwfed.component.form";
237
+ readonly fields: readonly any[];
238
+ readonly layouts: readonly {
239
+ readonly name: string;
240
+ readonly layout: {
241
+ readonly style?: string | undefined;
242
+ readonly columns: number;
243
+ readonly gap?: number | undefined;
244
+ readonly rows?: number | undefined;
245
+ readonly placements: {
246
+ readonly [x: string]: {
247
+ readonly area: readonly [readonly [number, number], readonly [number, number]];
248
+ readonly v?: "stretch" | "center" | "end" | "start" | undefined;
249
+ readonly h?: "stretch" | "center" | "end" | "start" | undefined;
250
+ };
251
+ };
252
+ };
253
+ readonly media?: string | undefined;
254
+ }[];
255
+ readonly readonly?: string | undefined;
256
+ } | undefined;
257
+ readonly dataSource?: {
258
+ readonly data: string;
259
+ readonly total?: string | undefined;
260
+ readonly request?: string | undefined;
261
+ } | undefined;
262
262
  readonly bottomActions?: {
263
263
  readonly size: "default" | "sm" | "xs";
264
264
  readonly style?: string | undefined;
@@ -827,40 +827,6 @@ export declare function createTableConfig(body: Omit<Schema.Schema.Type<ReturnTy
827
827
  readonly message: string;
828
828
  }[]] | undefined;
829
829
  }[];
830
- query?: {
831
- readonly style?: string | undefined;
832
- readonly id?: string | undefined;
833
- readonly initial?: {
834
- readonly data: string;
835
- readonly request?: string | undefined;
836
- } | undefined;
837
- readonly displayName?: string | undefined;
838
- readonly kind: "shwfed.component.form";
839
- readonly fields: readonly any[];
840
- readonly layouts: readonly {
841
- readonly name: string;
842
- readonly layout: {
843
- readonly style?: string | undefined;
844
- readonly columns: number;
845
- readonly gap?: number | undefined;
846
- readonly rows?: number | undefined;
847
- readonly placements: {
848
- readonly [x: string]: {
849
- readonly area: readonly [readonly [number, number], readonly [number, number]];
850
- readonly v?: "stretch" | "center" | "end" | "start" | undefined;
851
- readonly h?: "stretch" | "center" | "end" | "start" | undefined;
852
- };
853
- };
854
- };
855
- readonly media?: string | undefined;
856
- }[];
857
- readonly readonly?: string | undefined;
858
- } | undefined;
859
- dataSource?: {
860
- readonly data: string;
861
- readonly total?: string | undefined;
862
- readonly request?: string | undefined;
863
- } | undefined;
864
830
  actions?: {
865
831
  readonly size: "default" | "sm" | "xs";
866
832
  readonly style?: string | undefined;
@@ -928,6 +894,40 @@ export declare function createTableConfig(body: Omit<Schema.Schema.Type<ReturnTy
928
894
  readonly variant?: "default" | "link" | "destructive" | "primary" | "ghost" | undefined;
929
895
  }[];
930
896
  } | undefined;
897
+ query?: {
898
+ readonly style?: string | undefined;
899
+ readonly id?: string | undefined;
900
+ readonly initial?: {
901
+ readonly data: string;
902
+ readonly request?: string | undefined;
903
+ } | undefined;
904
+ readonly displayName?: string | undefined;
905
+ readonly kind: "shwfed.component.form";
906
+ readonly fields: readonly any[];
907
+ readonly layouts: readonly {
908
+ readonly name: string;
909
+ readonly layout: {
910
+ readonly style?: string | undefined;
911
+ readonly columns: number;
912
+ readonly gap?: number | undefined;
913
+ readonly rows?: number | undefined;
914
+ readonly placements: {
915
+ readonly [x: string]: {
916
+ readonly area: readonly [readonly [number, number], readonly [number, number]];
917
+ readonly v?: "stretch" | "center" | "end" | "start" | undefined;
918
+ readonly h?: "stretch" | "center" | "end" | "start" | undefined;
919
+ };
920
+ };
921
+ };
922
+ readonly media?: string | undefined;
923
+ }[];
924
+ readonly readonly?: string | undefined;
925
+ } | undefined;
926
+ dataSource?: {
927
+ readonly data: string;
928
+ readonly total?: string | undefined;
929
+ readonly request?: string | undefined;
930
+ } | undefined;
931
931
  bottomActions?: {
932
932
  readonly size: "default" | "sm" | "xs";
933
933
  readonly style?: string | undefined;
@@ -1,11 +1,12 @@
1
1
  <script setup>
2
2
  import { Icon } from "@iconify/vue";
3
- import { computed, ref, useSlots, useTemplateRef, watch } from "vue";
3
+ import { computed, onScopeDispose, ref, useSlots, useTemplateRef, watch } from "vue";
4
4
  import { buildCheckEnvironment, evaluateExpression } from "../../../share/expression";
5
5
  import { OPTIONS_VAR, SELECTIONS_VAR, injectCELContext, useScopeAncestry, useSelectionRoster, useStepRoster } from "../../../utils/cel-context";
6
6
  import { Markdown } from "../markdown";
7
7
  import CodeMirrorInput from "./CodeMirrorInput.vue";
8
- import { buildScopeLookup } from "./scope-refs";
8
+ import { buildScopeLookup, scanScopeRefs } from "./scope-refs";
9
+ import { useExpressionValidity } from "./validity";
9
10
  import { buildSelectionLookup } from "./selection-refs";
10
11
  import { buildStepLookup } from "./step-refs";
11
12
  import { buildOptionEntries, buildScopeEntries, buildSelectionEntries, buildStepEntries, buildVarEntries, selectShadowedScopes } from "./picker-entries";
@@ -62,11 +63,28 @@ const checkEnvironment = computed(() => {
62
63
  { unlistedVariablesAreDyn: props.unlistedVarsAreDyn }
63
64
  );
64
65
  });
66
+ const danglingScopeError = computed(() => {
67
+ const value = props.modelValue ?? props.defaultValue ?? "";
68
+ if (value.trim() === "") return null;
69
+ const lookup = scopeLookup.value;
70
+ for (const ref2 of scanScopeRefs(value)) {
71
+ const info = lookup.get(ref2.id);
72
+ if (!info || !info.members.has(ref2.member)) {
73
+ return "\u8868\u8FBE\u5F0F\u5F15\u7528\u4E86\u65E0\u6548\u7684\u8DE8\u5C42\u4F5C\u7528\u57DF\uFF08\u6765\u6E90\u5DF2\u4E0D\u5728\u53EF\u5BFB\u5740\u7684\u4E0A\u5C42\u4F5C\u7528\u57DF\u4E2D\uFF09";
74
+ }
75
+ }
76
+ return null;
77
+ });
65
78
  const validationError = computed(() => {
66
79
  const value = props.modelValue ?? props.defaultValue ?? "";
67
80
  if (value.trim() === "") return null;
68
- return evaluateExpression(checkEnvironment.value, value, props.resultType);
81
+ return danglingScopeError.value ?? evaluateExpression(checkEnvironment.value, value, props.resultType);
69
82
  });
83
+ const validityRegistry = useExpressionValidity();
84
+ if (validityRegistry) {
85
+ const unregister = validityRegistry.register(() => danglingScopeError.value === null);
86
+ onScopeDispose(unregister);
87
+ }
70
88
  const open = ref(false);
71
89
  const entries = ref([]);
72
90
  const shownVars = computed(() => entries.value.filter((e) => e.group === "var"));
@@ -1 +1,2 @@
1
1
  export { default as ExpressionEditor } from './ExpressionEditor.vue.js';
2
+ export { createExpressionValidityRegistry, provideExpressionValidity, useExpressionValidity, type ExpressionValidityRegistry, } from './validity.js';
@@ -1 +1,6 @@
1
1
  export { default as ExpressionEditor } from "./ExpressionEditor.vue";
2
+ export {
3
+ createExpressionValidityRegistry,
4
+ provideExpressionValidity,
5
+ useExpressionValidity
6
+ } from "./validity.js";
@@ -0,0 +1,12 @@
1
+ export type ExpressionValidityRegistry = {
2
+ /**
3
+ * Register a mounted editor's validity thunk (`true` ⇒ no ancestry error).
4
+ * Returns an unregister fn to call from `onScopeDispose`.
5
+ */
6
+ register: (isValid: () => boolean) => () => void;
7
+ /** `true` when every currently-registered editor reports valid. */
8
+ allValid: () => boolean;
9
+ };
10
+ export declare function createExpressionValidityRegistry(): ExpressionValidityRegistry;
11
+ export declare function provideExpressionValidity(registry: ExpressionValidityRegistry): void;
12
+ export declare function useExpressionValidity(): ExpressionValidityRegistry | undefined;
@@ -0,0 +1,27 @@
1
+ import { getCurrentInstance, inject, provide } from "vue";
2
+ const KEY = Symbol("shwfed-expression-validity");
3
+ export function createExpressionValidityRegistry() {
4
+ const entries = /* @__PURE__ */ new Map();
5
+ return {
6
+ register(isValid) {
7
+ const token = Symbol("expr");
8
+ entries.set(token, isValid);
9
+ return () => {
10
+ entries.delete(token);
11
+ };
12
+ },
13
+ allValid() {
14
+ for (const isValid of entries.values()) {
15
+ if (!isValid()) return false;
16
+ }
17
+ return true;
18
+ }
19
+ };
20
+ }
21
+ export function provideExpressionValidity(registry) {
22
+ if (!getCurrentInstance()) return;
23
+ provide(KEY, registry);
24
+ }
25
+ export function useExpressionValidity() {
26
+ return inject(KEY, void 0);
27
+ }
@@ -59,7 +59,7 @@ function configureMedia(configure) {
59
59
  return (env) => {
60
60
  env.registerFunction(
61
61
  "media(string): bool",
62
- (_self, q) => typeof window !== "undefined" && window.matchMedia(q).matches,
62
+ (q) => typeof window !== "undefined" && window.matchMedia(q).matches,
63
63
  { description: "\u5339\u914D CSS \u5A92\u4F53\u67E5\u8BE2\uFF0C\u7B49\u4EF7\u4E8E `window.matchMedia(q).matches`\uFF1BSSR \u4E0A\u4E0B\u6587\u8FD4\u56DE `false`" }
64
64
  );
65
65
  configure(env);
@@ -76,7 +76,7 @@ export function LayoutSet(configure) {
76
76
  resultType: "bool"
77
77
  }).annotations({
78
78
  title: "\u5339\u914D\u6761\u4EF6",
79
- description: "\u8FD4\u56DE `bool` \u7684 CEL \u8868\u8FBE\u5F0F\uFF0C\u6302\u8F7D\u65F6\u4E00\u6B21\u6027\u6C42\u503C\uFF1B\u7A7A\u8868\u793A\u65E0\u6761\u4EF6\u5339\u914D\u3002\u53EF\u7528 `media('(min-width: 1024px)')` \u5305\u88C5 CSS \u5A92\u4F53\u67E5\u8BE2\u3002"
79
+ description: "\u8FD4\u56DE `bool` \u7684 CEL \u8868\u8FBE\u5F0F\uFF1B\u968F\u89C6\u53E3\u53D8\u5316\u4E0E\u6240\u5F15\u7528\u7684\u54CD\u5E94\u5F0F\u6570\u636E\u81EA\u52A8\u91CD\u65B0\u6C42\u503C\uFF0C\u7A7A\u8868\u793A\u65E0\u6761\u4EF6\u5339\u914D\u3002\u53EF\u7528 `media('(min-width: 1024px)')` \u5305\u88C5 CSS \u5A92\u4F53\u67E5\u8BE2\u3002"
80
80
  })),
81
81
  layout: Layout
82
82
  }).annotations({
@@ -4,6 +4,7 @@ import { computed } from "vue";
4
4
  import { cel as _rawCel } from "../utils/cel";
5
5
  import { celBindings, injectCELContext } from "../utils/cel-context";
6
6
  import { DEFAULT_GAP } from "./layout";
7
+ import { useViewportRevision } from "./viewport";
7
8
  defineOptions({ name: "ShwfedSlotRenderer" });
8
9
  const props = defineProps({
9
10
  slotValue: { type: Object, required: true },
@@ -25,7 +26,11 @@ function pickLayout(sets) {
25
26
  }
26
27
  return sets[sets.length - 1];
27
28
  }
28
- const activeLayout = computed(() => pickLayout(props.slotValue.layouts));
29
+ const viewportRevision = useViewportRevision();
30
+ const activeLayout = computed(() => {
31
+ void viewportRevision.value;
32
+ return pickLayout(props.slotValue.layouts);
33
+ });
29
34
  const warnedMissing = /* @__PURE__ */ new Set();
30
35
  const placedBlocks = computed(() => {
31
36
  const layout = activeLayout.value?.layout;
@@ -0,0 +1,16 @@
1
+ import type { Ref } from 'vue';
2
+ /**
3
+ * A reactive counter that increments on any viewport change (resize /
4
+ * orientation). Read its `.value` inside a `computed` to make that computed
5
+ * re-evaluate when the viewport changes.
6
+ *
7
+ * This is the reactive dependency a `media('…')` CEL query otherwise lacks:
8
+ * `window.matchMedia(q).matches` is a one-shot, untracked read, so a layout
9
+ * gated on `media('(max-width: 900px)')` would be frozen at its mount-time
10
+ * value and never re-select when the window is resized (or DevTools docked).
11
+ * Touching this ref inside the selection `computed` closes that gap.
12
+ *
13
+ * SSR-safe and self-cleaning: `useEventListener` no-ops without a `window` and
14
+ * detaches automatically when the owning component's effect scope disposes.
15
+ */
16
+ export declare function useViewportRevision(): Ref<number>;
@@ -0,0 +1,11 @@
1
+ import { ref } from "vue";
2
+ import { useEventListener } from "@vueuse/core";
3
+ export function useViewportRevision() {
4
+ const revision = ref(0);
5
+ const bump = () => {
6
+ revision.value++;
7
+ };
8
+ useEventListener("resize", bump, { passive: true });
9
+ useEventListener("orientationchange", bump, { passive: true });
10
+ return revision;
11
+ }
@@ -67,6 +67,21 @@ export declare function provideScopeAncestor(ancestor: Ref<ScopeAncestor | null>
67
67
  * first. Empty when nothing up the config tree exposes an addressable scope.
68
68
  */
69
69
  export declare function useScopeAncestry(): Ref<ReadonlyArray<ScopeAncestor>>;
70
+ /**
71
+ * Re-provide the inherited scope ancestry with the NEAREST frame's per-row
72
+ * members (`row` / `index`) dropped — its `selected` (and any other member)
73
+ * survives. Called by a table-LEVEL action editor (top toolbar / bottom bar):
74
+ * such an action renders OUTSIDE any row, so at runtime the owning table's
75
+ * `__scopes__[id]` there carries only `selected` (registered at the table host
76
+ * frame), never `row`/`index` (registered per row). Everything the action opens
77
+ * — nested modals and their tables — inherits this downgraded chain, so a
78
+ * cross-layer picker below never offers the owning table's 当前行 / 行序号, which
79
+ * would throw `No such key: row` at evaluation. Outer frames are untouched: each
80
+ * table masks only its own here, and an outer table reached via a genuine row
81
+ * action keeps its row/index reachable. `shadows` is left intact — this frame
82
+ * still masks the relative `row`/`index` for descendants at runtime.
83
+ */
84
+ export declare function provideRowUnreachableScope(): void;
70
85
  /** One selection-like field in the editing form, as the picker/chip surfaces it. */
71
86
  export type SelectionFieldRef = Readonly<{
72
87
  /** Field uuid keyed into `selections["<id>"]` — the registry/publish key. */
@@ -30,7 +30,8 @@ export function provideScopeAddress(uuid, accessor) {
30
30
  // bracket access throws a located `No such key` at the offending uuid.
31
31
  value: () => {
32
32
  const base = parentScopes ? typeof parentScopes.value === "function" ? parentScopes.value() : parentScopes.value : {};
33
- return { ...base, [uuid]: resolveScopeAccessor(accessor) };
33
+ const prior = base[uuid];
34
+ return { ...base, [uuid]: { ...prior, ...resolveScopeAccessor(accessor) } };
34
35
  }
35
36
  }
36
37
  });
@@ -49,6 +50,22 @@ export function provideScopeAncestor(ancestor) {
49
50
  export function useScopeAncestry() {
50
51
  return inject(SCOPE_ANCESTRY_KEY, void 0) ?? shallowRef([]);
51
52
  }
53
+ export function provideRowUnreachableScope() {
54
+ const parent = inject(SCOPE_ANCESTRY_KEY, void 0);
55
+ provide(
56
+ SCOPE_ANCESTRY_KEY,
57
+ computed(() => {
58
+ const chain = parent?.value ?? [];
59
+ if (chain.length === 0) return chain;
60
+ const [nearest, ...rest] = chain;
61
+ const members = nearest.members.filter((m) => {
62
+ const shortcut = m.shortcut ?? m.key;
63
+ return shortcut !== "row" && shortcut !== "index";
64
+ });
65
+ return [{ ...nearest, members }, ...rest];
66
+ })
67
+ );
68
+ }
52
69
  const SELECTION_ROSTER_KEY = Symbol("cel-selection-roster");
53
70
  export function provideSelectionRoster(roster) {
54
71
  provide(SELECTION_ROSTER_KEY, roster);
@@ -22,7 +22,12 @@ function unwrapCelValue(value) {
22
22
  const env = new Environment({ unlistedVariablesAreDyn: true });
23
23
  env.registerFunction(
24
24
  "media(string): bool",
25
- (_self, q) => typeof window !== "undefined" && window.matchMedia(q).matches
25
+ // Global (non-method) function: the evaluator passes the CEL argument as the
26
+ // first parameter (no receiver/`self` prepended — that shape is method-only,
27
+ // as in `list<number>.sum()` below). A leading `_self` here would bind the
28
+ // query string to `_self` and the AST node to `q`, so `matchMedia` would
29
+ // stringify the AST and never match — the "media always false" bug.
30
+ (q) => typeof window !== "undefined" && window.matchMedia(q).matches
26
31
  );
27
32
  env.registerFunction(
28
33
  "list<number>.sum(): number",
@@ -85,7 +85,7 @@ Internal-only (not re-exported from `index.js`): `parse`, standalone `evaluate`/
85
85
 
86
86
  Primitives: `string`, `bool`, `number`, `bytes`, `null_type`, `type`
87
87
  Composites: `list<T>`, `map<K, V>`, `optional<T>`, `dyn`, `message`
88
- Custom: `Date` (backed by `TZDate` from `@date-fns/tz`), `URL` (backed by the native `URL`; method `URL.query(string): optional<string>` reads a search param — `None` when absent, distinguishing it from a present-but-empty value), `File` (native), `FormData` (native — see `form-builtins.ts`), `http` / `HttpRequest` (local addition — see `http-builtins.ts`)
88
+ Custom: `Date` (backed by `TZDate` from `@date-fns/tz`), `URL` (backed by the native `URL`; host-supplied only — no CEL constructor. Reader `URL.query(string): optional<string>` returns the *first* value of a search param — `None` when absent, distinguishing it from a present-but-empty value. Writers return a **copy**, never mutating the receiver (a `URL` is host-injected shared state, unlike a fresh-per-expression `HttpRequestBuilder`): the set/replace `URL.query(string, string | number | optional<string> | optional<number>): URL` collapses repeated entries and on `None` *deletes* the key (the result mirrors the optional's state — deliberately unlike `HttpRequest.query`, whose `None` is a drop/no-op), while `URL.appendQuery(...)` with the same four value overloads appends an entry and no-ops on `None`. Number values render Decimal-exact, like `string(number)`. `string(URL)` is the full `href`.), `File` (native), `FormData` (native — see `form-builtins.ts`), `http` / `HttpRequest` (local addition — see `http-builtins.ts`)
89
89
 
90
90
  ## Function Signature Format
91
91
 
@@ -105,6 +105,7 @@ number(d) // Date → epoch-millisecond timestamp
105
105
  string(42) // "42"
106
106
  string(true) // "true"
107
107
  string(d) // Date → ISO 8601 string
108
+ string(location) // URL → the full href string
108
109
  bool("true") // true (also accepts "t", "1", "TRUE", "True")
109
110
  bool("false") // false (also accepts "f", "0", "FALSE", "False")
110
111
  bytes("text") // UTF-8 encode to bytes
@@ -310,6 +311,20 @@ location.query("missing").hasValue() // false — absent params are None
310
311
  location.query("id").orValue("") // first value when a param repeats
311
312
  ```
312
313
 
314
+ `query(name, value)` **sets** a search param and returns a new `URL` — the
315
+ original is never modified. The value may be a `string`, a `number`, or an
316
+ optional of either: `Some(v)` sets the param, `None` **removes** it, so the
317
+ result always mirrors the optional's state. `appendQuery(name, value)` takes
318
+ the same value types but **adds** an entry alongside existing ones (`None`
319
+ appends nothing). Convert the result with `string(...)` when the host expects
320
+ a URL string.
321
+ ```cel
322
+ location.query("page", 2) // ?page=1 → ?page=2 (replaced)
323
+ location.query("page", q.?page) // sets ?page= from the field, or removes it when absent
324
+ location.appendQuery("tag", "a") // ?tag=x → ?tag=x&tag=a (kept both)
325
+ string(location.query("page", 2)) // "https://…?page=2" — e.g. a navigation target
326
+ ```
327
+
313
328
  ## HTTP
314
329
 
315
330
  Build HTTP requests using the `http` builder. A CEL expression only *describes* a request — it always evaluates to an `HttpRequest`, never issues it. The host decides when and how to send it (downloading a file, reading a JSON response, etc.). There is no way to perform the request or read its response from CEL.
@@ -1,6 +1,7 @@
1
1
  import { evaluationError } from "./errors.js";
2
2
  import { Decimal } from "./decimal.js";
3
3
  import { TYPES, Type } from "./registry.js";
4
+ import { Option } from "effect";
4
5
  import { register as registerOptional, optionalOf } from "./optional.js";
5
6
  import { objKeys, arrayFrom } from "./globals.js";
6
7
  import { TZDate } from "@date-fns/tz";
@@ -410,6 +411,33 @@ export function registerFunctions(registry) {
410
411
  registry.registerType("File", File);
411
412
  registry.registerType("URL", URL);
412
413
  functionOverload("URL.query(string): optional<string>", (url, key) => optionalOf(url.searchParams.get(key) ?? void 0));
414
+ const withSearchParams = (u, write) => {
415
+ const url = new URL(u.href);
416
+ write(url.searchParams);
417
+ return url;
418
+ };
419
+ const numToParam = (v) => Decimal.from(v).toString();
420
+ functionOverload("URL.query(string, string): URL", (u, k, v) => withSearchParams(u, (p) => p.set(k, v)));
421
+ functionOverload("URL.query(string, number): URL", (u, k, v) => withSearchParams(u, (p) => p.set(k, numToParam(v))));
422
+ functionOverload("URL.query(string, optional<string>): URL", (u, k, v) => {
423
+ const value = v;
424
+ return withSearchParams(u, (p) => Option.isSome(value) ? p.set(k, value.value) : p.delete(k));
425
+ });
426
+ functionOverload("URL.query(string, optional<number>): URL", (u, k, v) => {
427
+ const value = v;
428
+ return withSearchParams(u, (p) => Option.isSome(value) ? p.set(k, numToParam(value.value)) : p.delete(k));
429
+ });
430
+ functionOverload("URL.appendQuery(string, string): URL", (u, k, v) => withSearchParams(u, (p) => p.append(k, v)));
431
+ functionOverload("URL.appendQuery(string, number): URL", (u, k, v) => withSearchParams(u, (p) => p.append(k, numToParam(v))));
432
+ functionOverload("URL.appendQuery(string, optional<string>): URL", (u, k, v) => {
433
+ const value = v;
434
+ return Option.isSome(value) ? withSearchParams(u, (p) => p.append(k, value.value)) : u;
435
+ });
436
+ functionOverload("URL.appendQuery(string, optional<number>): URL", (u, k, v) => {
437
+ const value = v;
438
+ return Option.isSome(value) ? withSearchParams(u, (p) => p.append(k, numToParam(value.value))) : u;
439
+ });
440
+ functionOverload("string(URL): string", (v) => v.href);
413
441
  registerOptional(registry);
414
442
  }
415
443
  function stringSize(str) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shwfed/config",
3
- "version": "2.12.14",
3
+ "version": "2.12.16",
4
4
  "description": "Configurable UI for SHWFED",
5
5
  "type": "module",
6
6
  "publishConfig": {