@shwfed/config 3.0.8 → 3.0.10

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 (48) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/actions/ai-l1.md +1 -1
  3. package/dist/runtime/components/actions/buttons/2026-04-21/com.shwfed.actions.button.navigation/runtime.vue +3 -2
  4. package/dist/runtime/components/config/ai-generate.system.md +2 -1
  5. package/dist/runtime/components/config/blocks/2026-06-17/com.shwfed.block.tree.single/config.d.vue.ts +18 -18
  6. package/dist/runtime/components/config/blocks/2026-06-17/com.shwfed.block.tree.single/config.vue.d.ts +18 -18
  7. package/dist/runtime/components/config/blocks/2026-06-17/com.shwfed.block.tree.single/runtime.d.vue.ts +18 -18
  8. package/dist/runtime/components/config/blocks/2026-06-17/com.shwfed.block.tree.single/runtime.vue.d.ts +18 -18
  9. package/dist/runtime/components/config/index.d.vue.ts +2 -2
  10. package/dist/runtime/components/config/index.vue.d.ts +2 -2
  11. package/dist/runtime/components/config/utils/ai-cursor.js +7 -1
  12. package/dist/runtime/components/form/fields/2026-05-23/com.shwfed.form.field.tree.multi/config.d.vue.ts +8 -8
  13. package/dist/runtime/components/form/fields/2026-05-23/com.shwfed.form.field.tree.multi/config.vue.d.ts +8 -8
  14. package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.multi/config.d.vue.ts +8 -8
  15. package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.multi/config.vue.d.ts +8 -8
  16. package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.single/config.d.vue.ts +8 -8
  17. package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.single/config.vue.d.ts +8 -8
  18. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.multi/config.d.vue.ts +8 -8
  19. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.multi/config.vue.d.ts +8 -8
  20. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.single/config.d.vue.ts +8 -8
  21. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.single/config.vue.d.ts +8 -8
  22. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.multi/config.d.vue.ts +8 -8
  23. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.multi/config.vue.d.ts +8 -8
  24. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.single/config.d.vue.ts +8 -8
  25. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.single/config.vue.d.ts +8 -8
  26. package/dist/runtime/components/operations/2026-06-15/com.shwfed.operation.navigation/runtime.js +3 -2
  27. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +8 -8
  28. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +8 -8
  29. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +8 -8
  30. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +8 -8
  31. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +8 -8
  32. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +8 -8
  33. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +8 -8
  34. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +8 -8
  35. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +8 -8
  36. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +8 -8
  37. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +8 -8
  38. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +8 -8
  39. package/dist/runtime/components/tabs/index.vue +10 -2
  40. package/dist/runtime/components/ui/chat/ChatMessages.vue +30 -6
  41. package/dist/runtime/share/navigation.d.ts +23 -0
  42. package/dist/runtime/share/navigation.js +15 -0
  43. package/dist/runtime/utils/disabled-date.d.ts +8 -2
  44. package/dist/runtime/utils/disabled-date.js +2 -1
  45. package/dist/runtime/vendor/cel-js/CLAUDE.md +3 -1
  46. package/dist/runtime/vendor/cel-js/PROMPT.md +13 -0
  47. package/dist/runtime/vendor/cel-js/lib/macros.js +64 -1
  48. package/package.json +2 -1
@@ -36,15 +36,15 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
36
36
  }[]] | undefined;
37
37
  readonly keywords?: string | undefined;
38
38
  readonly readonly?: string | undefined;
39
- readonly dataSource: {
40
- readonly data: string;
41
- readonly request?: string | undefined;
42
- };
43
39
  readonly binding: string;
44
40
  readonly derived?: {
45
41
  readonly mode: "formula" | "prefill";
46
42
  readonly expression: string;
47
43
  } | undefined;
44
+ readonly dataSource: {
45
+ readonly data: string;
46
+ readonly request?: string | undefined;
47
+ };
48
48
  readonly nodeKey: string;
49
49
  readonly nodeChildren: string;
50
50
  readonly nodeLabel: string;
@@ -95,15 +95,15 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
95
95
  }[]] | undefined;
96
96
  readonly keywords?: string | undefined;
97
97
  readonly readonly?: string | undefined;
98
- readonly dataSource: {
99
- readonly data: string;
100
- readonly request?: string | undefined;
101
- };
102
98
  readonly binding: string;
103
99
  readonly derived?: {
104
100
  readonly mode: "formula" | "prefill";
105
101
  readonly expression: string;
106
102
  } | undefined;
103
+ readonly dataSource: {
104
+ readonly data: string;
105
+ readonly request?: string | undefined;
106
+ };
107
107
  readonly nodeKey: string;
108
108
  readonly nodeChildren: string;
109
109
  readonly nodeLabel: string;
@@ -36,15 +36,15 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
36
36
  }[]] | undefined;
37
37
  readonly keywords?: string | undefined;
38
38
  readonly readonly?: string | undefined;
39
- readonly dataSource: {
40
- readonly data: string;
41
- readonly request?: string | undefined;
42
- };
43
39
  readonly binding: string;
44
40
  readonly derived?: {
45
41
  readonly mode: "formula" | "prefill";
46
42
  readonly expression: string;
47
43
  } | undefined;
44
+ readonly dataSource: {
45
+ readonly data: string;
46
+ readonly request?: string | undefined;
47
+ };
48
48
  readonly nodeKey: string;
49
49
  readonly nodeChildren: string;
50
50
  readonly nodeLabel: string;
@@ -94,15 +94,15 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
94
94
  }[]] | undefined;
95
95
  readonly keywords?: string | undefined;
96
96
  readonly readonly?: string | undefined;
97
- readonly dataSource: {
98
- readonly data: string;
99
- readonly request?: string | undefined;
100
- };
101
97
  readonly binding: string;
102
98
  readonly derived?: {
103
99
  readonly mode: "formula" | "prefill";
104
100
  readonly expression: string;
105
101
  } | undefined;
102
+ readonly dataSource: {
103
+ readonly data: string;
104
+ readonly request?: string | undefined;
105
+ };
106
106
  readonly nodeKey: string;
107
107
  readonly nodeChildren: string;
108
108
  readonly nodeLabel: string;
@@ -36,15 +36,15 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
36
36
  }[]] | undefined;
37
37
  readonly keywords?: string | undefined;
38
38
  readonly readonly?: string | undefined;
39
- readonly dataSource: {
40
- readonly data: string;
41
- readonly request?: string | undefined;
42
- };
43
39
  readonly binding: string;
44
40
  readonly derived?: {
45
41
  readonly mode: "formula" | "prefill";
46
42
  readonly expression: string;
47
43
  } | undefined;
44
+ readonly dataSource: {
45
+ readonly data: string;
46
+ readonly request?: string | undefined;
47
+ };
48
48
  readonly nodeKey: string;
49
49
  readonly nodeChildren: string;
50
50
  readonly nodeLabel: string;
@@ -94,15 +94,15 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
94
94
  }[]] | undefined;
95
95
  readonly keywords?: string | undefined;
96
96
  readonly readonly?: string | undefined;
97
- readonly dataSource: {
98
- readonly data: string;
99
- readonly request?: string | undefined;
100
- };
101
97
  readonly binding: string;
102
98
  readonly derived?: {
103
99
  readonly mode: "formula" | "prefill";
104
100
  readonly expression: string;
105
101
  } | undefined;
102
+ readonly dataSource: {
103
+ readonly data: string;
104
+ readonly request?: string | undefined;
105
+ };
106
106
  readonly nodeKey: string;
107
107
  readonly nodeChildren: string;
108
108
  readonly nodeLabel: string;
@@ -22,11 +22,19 @@ useHead(() => {
22
22
  const tab = tabs.value.find((t) => t.path === activePath.value);
23
23
  return tab ? { title: store.resolveTitle(tab, locale.value) } : {};
24
24
  });
25
+ const homePath = props.home.split(/[?#]/, 1)[0];
26
+ function track(path, fullPath) {
27
+ if (path === homePath) {
28
+ store.activePath.value = null;
29
+ return;
30
+ }
31
+ store.openOrFocus(path, fullPath);
32
+ }
25
33
  let stopAfterEach;
26
34
  onMounted(() => {
27
- store.openOrFocus(route.path, route.fullPath);
35
+ track(route.path, route.fullPath);
28
36
  stopAfterEach = router.afterEach((to) => {
29
- store.openOrFocus(to.path, to.fullPath);
37
+ track(to.path, to.fullPath);
30
38
  });
31
39
  });
32
40
  onBeforeUnmount(() => stopAfterEach?.());
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { nextTick, ref, watch } from "vue";
2
+ import { nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
3
3
  import { Icon } from "@iconify/vue";
4
4
  import { ScrollArea } from "../scroll-area";
5
5
  import ChatMessage from "./ChatMessage.vue";
@@ -9,15 +9,35 @@ const props = defineProps({
9
9
  pending: { type: Boolean, required: false }
10
10
  });
11
11
  const root = ref(null);
12
- function scrollToBottom() {
13
- const viewport = root.value?.$el?.querySelector?.(
12
+ function viewportEl() {
13
+ return root.value?.$el?.querySelector?.(
14
14
  '[data-slot="scroll-area-viewport"]'
15
- );
16
- if (viewport) viewport.scrollTop = viewport.scrollHeight;
15
+ ) ?? null;
16
+ }
17
+ const STICK_SLOP = 24;
18
+ const stick = ref(true);
19
+ function onScroll() {
20
+ const v = viewportEl();
21
+ if (!v) return;
22
+ stick.value = v.scrollHeight - v.scrollTop - v.clientHeight <= STICK_SLOP;
23
+ }
24
+ function scrollToBottom() {
25
+ const v = viewportEl();
26
+ if (v) v.scrollTop = v.scrollHeight;
17
27
  }
28
+ onMounted(() => {
29
+ const v = viewportEl();
30
+ v?.addEventListener("scroll", onScroll, { passive: true });
31
+ nextTick(scrollToBottom);
32
+ });
33
+ onBeforeUnmount(() => {
34
+ viewportEl()?.removeEventListener("scroll", onScroll);
35
+ });
18
36
  watch(
19
37
  () => [props.messages, props.pending],
20
- () => nextTick(scrollToBottom),
38
+ () => nextTick(() => {
39
+ if (stick.value) scrollToBottom();
40
+ }),
21
41
  { flush: "post" }
22
42
  );
23
43
  </script>
@@ -61,3 +81,7 @@ watch(
61
81
  </div>
62
82
  </div>
63
83
  </template>
84
+
85
+ <style scoped>
86
+ .relative :deep([data-slot=scroll-area-viewport]>div){display:block!important;width:100%}
87
+ </style>
@@ -1 +1,24 @@
1
1
  export declare function isAbsoluteUrl(url: string): boolean;
2
+ /**
3
+ * Demote an absolute href that names OUR OWN origin to a router-ready in-app
4
+ * path, so `_self` navigation to it is an SPA push rather than a full document
5
+ * reload. Returns `null` for any other origin — and for opaque-origin schemes
6
+ * like `mailto:` / `tel:` / custom app schemes — which stay full-page
7
+ * navigations governed by `isAbsoluteUrl`.
8
+ *
9
+ * Why it exists: a CEL author who writes `location.query('tab', 'x')` gets a
10
+ * `URL` back, and `string(URL)` is the full `.href` — origin and all. Reloading
11
+ * our own page just to flip a query param is almost never the intent, so a
12
+ * same-origin absolute is routed internally instead.
13
+ *
14
+ * Two subtleties make this more than an `=== origin` check — and are exactly
15
+ * why it cannot fold back into `isAbsoluteUrl`:
16
+ * - The router prepends its history base (`app.baseURL`) to whatever path it
17
+ * routes, and a full pathname ALREADY carries that base. Handing it back
18
+ * unstripped double-prepends (`/base/x` → `/base/base/x`), so the base is
19
+ * peeled off here. Vue Router's `history.base` may or may not carry a
20
+ * trailing slash; both normalize.
21
+ * - A protocol-relative `//host/x` has no scheme of its own, so it resolves
22
+ * against `origin` to inherit ours before the comparison.
23
+ */
24
+ export declare function sameOriginInternalPath(href: string, origin: string, base: string): string | null;
@@ -2,3 +2,18 @@ const ABSOLUTE = /^(?:[a-z][a-z0-9+\-.]*:|\/\/)/i;
2
2
  export function isAbsoluteUrl(url) {
3
3
  return ABSOLUTE.test(url.trim());
4
4
  }
5
+ export function sameOriginInternalPath(href, origin, base) {
6
+ let url;
7
+ try {
8
+ url = new URL(href.trim(), origin);
9
+ } catch {
10
+ return null;
11
+ }
12
+ if (url.origin !== origin) return null;
13
+ const path = `${url.pathname}${url.search}${url.hash}`;
14
+ const prefix = base.replace(/\/+$/, "");
15
+ if (prefix && (path === prefix || path.startsWith(`${prefix}/`))) {
16
+ return path.slice(prefix.length) || "/";
17
+ }
18
+ return path;
19
+ }
@@ -1,12 +1,18 @@
1
1
  import type { DateValue } from 'reka-ui';
2
2
  /**
3
3
  * Convert an `@internationalized/date` `DateValue` (what the picker's
4
- * `disabledDate` callback receives) to a native JS `Date`, so user expressions
5
- * see a plain `input: Date`. Mirrors `pickerValueToNative` in
4
+ * `disabledDate` callback receives) to the CEL `Date` type, so user expressions
5
+ * see `input: Date`. Mirrors `pickerValueToNative` in
6
6
  * `ui/date-picker/DatePicker.vue` (minus the `Time` branch — the per-date
7
7
  * predicate never receives a `Time`). For a `CalendarDate` (date/month/year
8
8
  * panels) the time fields are absent and default to `0`; for `month` panels the
9
9
  * candidate is the first of the month (`day` is `1`).
10
+ *
11
+ * MUST be a `TZDate`, not a plain `new Date()`: CEL's registry maps its `Date`
12
+ * type to the `TZDate` constructor exactly (`registerType('Date', TZDate)`), so
13
+ * a plain `Date`'s constructor lookup misses and the evaluator throws
14
+ * `Unsupported type: Date`. Same requirement as the `now` variable in
15
+ * `form/index.vue`.
10
16
  */
11
17
  export declare function dateValueToNative(d: DateValue): Date;
12
18
  /**
@@ -1,5 +1,6 @@
1
+ import { TZDate } from "@date-fns/tz";
1
2
  export function dateValueToNative(d) {
2
- return new Date(
3
+ return new TZDate(
3
4
  d.year,
4
5
  d.month - 1,
5
6
  "day" in d ? d.day : 1,
@@ -26,6 +26,8 @@ The `homogeneousAggregateLiterals` and `enableOptionalTypes` environment options
26
26
 
27
27
  The custom `Optional` class has been replaced with Effect's `Option` type (`import { Option } from 'effect'`). Internal helpers `optionalOf(value)` and `optionalValue(opt)` in `optional.js` handle the CEL-specific semantics (treating `undefined` as `None`, throwing `EvaluationError` on missing value access). The optional surface is `optional.of(A): optional<A>` (the lift / `pure` — `of(null)` is `Some(null)`), `optional.ofNullable(A): optional<A>` (the null-tolerant lift, Java-style — `null` becomes `None`), `optional.none()`, `optional.hasValue()`, `optional.value()`, `optional.or(optional)`, `optional.orValue(value)`, plus `optional<A>.flatMap(var, body): optional<B>` (monadic bind), `optional<A>.map(var, body): optional<B>` (the Functor `fmap`), `optional<A>.zip(other, x, y, body): optional<C>` (the applicative `liftA2` / `zipWith`), `optional<A>.filter(var, predicate): optional<A>` (Rust `Option::filter` / Haskell `mfilter`), and the folds `optional<A>.exists(var, predicate): bool` (predicate on `Some`, `false` on `None` — Rust `is_some_and`) and `optional<A>.all(var, predicate): bool` (predicate on `Some`, vacuously `true` on `None` — Scala `forall`) — all local additions (not from upstream). All are variable-binding macros (registered with `ast` arg types so the unbound args stay unevaluated): on `Some` they bind the value(s) and evaluate `body`; on `None` they short-circuit without touching `body`. `flatMap`'s body must itself yield an `optional`; `map`'s body is a plain value that gets re-wrapped in `Some`; `filter` / `exists` / `all` predicates must be `bool` (enforced at type-check when static, at runtime via `invalid_condition_type` otherwise). `map` is the derived `o.flatMap(v, optional.of(body))` promoted to a first-class macro. `zip` is the flattened `a.flatMap(x, b.map(y, body))`: it type-checks `other` in the outer ctx, binds `x` then `y` under two chained overlays, re-wraps the plain body in `Some`, and short-circuits to `None` if either side is `None` (a `None` receiver skips evaluating `other`). Because CEL has no tuple type the transform is fused (there is no useful `optional<pair>` to return), so `zip` is `zipWith`, not Rust/Scala's pair-building `Option.zip`. The short-circuit makes `zip` observationally monadic — harmless for `optional` (a `None` carries no payload) but it would not generalise to an error-accumulating applicative. An `optional.none()` receiver/argument carries a `param` element type; the shared `bindOptionalValueType` helper (exported from `optional.ts`) binds it as `dyn` so a boolean body still type-checks — every binding macro (`map` / `flatMap` / `zip` / `filter` / `exists` / `all`) goes through it (leaking the `param` into the iteration variable was a real bug in `map` / `flatMap`, regression-tested in `optional-extras.test.ts`). `zip` lives in `optional.ts` beside `flatMap`; the two-variable binding is why it is not folded into the shared `map` expander. NOTE: `map`, `filter`, `all`, and `exists` are **registered in `macros.ts`**, not here — the parser resolves a macro by name before types are known, so the optional variants and the list-comprehension variants must share one expander per name that dispatches on the receiver type at type-check (an `optional` receiver → optional semantics; `list`/`map` → comprehension; a `dyn` receiver is deferred to a runtime `Option.isOption` check). `exists_one` and `find` stay list-only.
28
28
 
29
+ A `list.filterMap(var, body): list<dyn>` macro has been added (not from upstream, in `macros.ts` beside `find`) — Rust's `Iterator::filter_map`. The body evaluates to an `optional<B>` per element; `Some` values are kept (unwrapped), `None`s dropped, yielding `list<B>`. It fuses the map-then-strip-absent pattern (which would otherwise need `.map(...)` to build a `list<optional>` plus a second pass to drop the `None`s) and is the optional-gated complement to the 3-arg `map(var, filter, transform)`, whose gate is a bool predicate. NOTE: it is **not** a comprehension desugar — the accu/ternary primitives gate on a bool, not on `Option` presence — so unlike the 2/3-arg `map` and `filter` it runs a manual fold like `find` (eval receiver → coerce Set/Map/object to an array → per-element `ev.eval(body)` → `ensureOptional` → push the `Some`s). Type-check: the receiver element type comes from list `valueType` / map `keyType` / `dyn` (same `invalid_comprehension_range` contract as `find`); the body must be `optional<B>` when statically typed (via `ensureOptionalType`), and a `dyn` body defers to the runtime `ensureOptional` (matching `flatMap`); the result is `registry.getListType(bindOptionalValueType(bodyType))`. Because the body already returns an `optional`, `filterMap` needs no receiver-type dispatch (there is no `optional<A>.filterMap` — that role is `flatMap`), so it is a single list-only expander. Tested in `__tests__/filter-map-macro.test.ts`.
30
+
29
31
  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. The terminal methods `.json()`, `.file()`, and `.fileOrJson()` are dispatched by the host on the returned builder (none is a CEL method), so expression evaluation stays free of IO. `.fileOrJson()` branches on the response `Content-Type`: a JSON media type (`application/json` or a `+json` suffix) is parsed and returned as `{ _tag: 'json', json }`; anything else is returned as `{ _tag: 'file', file }`. Note the `Fetch` service itself `ensureOk`s (both `FetchLive` and the mock layer), so a non-2xx fails at `Fetch.fetch` before the branch and this only ever distinguishes 2xx bodies — i.e. the HTTP-200-with-error-envelope case. The download operation uses it so a backend that answers a download with a JSON error envelope (commonly `{ code: -1 }` on a 200) is classified rather than saved as the file. 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. `.query` accepts `(string, string)` / `(string, number)`, the optional-valued `(string, optional<string>)` / `(string, optional<number>)` (a `None`, e.g. an absent `.?` field, drops the param rather than emitting `key=`; `Some` appends it via the string/number coercion), and the record-splat `(dyn)`.
30
32
 
31
33
  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.
@@ -51,7 +53,7 @@ lib/
51
53
  ├── operators.ts — Binary/unary operator implementations
52
54
  ├── overloads.ts — Operator overload definitions
53
55
  ├── functions.ts — Built-in functions + Date type (TZDate)
54
- ├── macros.ts — map / filter / all / exists (each dispatching list comprehension vs optional receiver), exists_one, find, has, cel.bind
56
+ ├── macros.ts — map / filter / all / exists (each dispatching list comprehension vs optional receiver), exists_one, find, filterMap, has, cel.bind
55
57
  ├── http-builtins.ts — `http` constant + http/HttpRequest types (local addition)
56
58
  ├── http-builder.ts — `HttpRequestBuilder` — what an http.* expression returns
57
59
  ├── json-builtins.ts — `JSON` constant + `JSON.parse` / `JSON.stringify` (local addition)
@@ -505,6 +505,19 @@ Macros are special forms — they look like method calls but have special evalua
505
505
  [1, 2, 3, 4, 5].filter(x, x > 3) // [4, 5]
506
506
  ```
507
507
 
508
+ ### filterMap — Transform-and-keep in one pass (Rust `filter_map`)
509
+ The body returns an `optional<B>` per element; the `Some` values are kept
510
+ (unwrapped) and the `None`s dropped, yielding a `list<B>`. Use it when the
511
+ keep/drop decision *is* the transform — computing the mapped value and its
512
+ presence together — rather than a separate bool predicate. (The 3-arg
513
+ `map(x, filter, transform)` is the bool-predicate form; reach for `filterMap`
514
+ when the gate rides on an `optional`, e.g. a `.?` field or `optional.ofNullable`.)
515
+ ```cel
516
+ [1, 2, 3, 4].filterMap(x, x % 2 == 0 ? optional.of(x * 10) : optional.none()) // [20, 40]
517
+ users.filterMap(u, optional.ofNullable(u.email)) // emails of users that have one
518
+ rows.filterMap(r, r.?qty.map(q, q * 2)) // doubled qty, skipping rows without one
519
+ ```
520
+
508
521
  ### all — True if ALL elements match
509
522
  ```cel
510
523
  [1, 2, 3].all(x, x > 0) // true
@@ -1,7 +1,7 @@
1
1
  import { Effect, Option } from "effect";
2
2
  import { evaluationError, parseError } from "./errors.js";
3
3
  import { OPERATORS as OPS } from "./operators.js";
4
- import { optionalOf, ensureOptional, bindOptionalValueType } from "./optional.js";
4
+ import { optionalOf, ensureOptional, ensureOptionalType, bindOptionalValueType } from "./optional.js";
5
5
  const identity = (x) => x;
6
6
  function assertIdentifier(node, message) {
7
7
  if (node.op === "id") return node.args;
@@ -294,6 +294,68 @@ function createFindExpander() {
294
294
  };
295
295
  };
296
296
  }
297
+ function createFilterMapExpander() {
298
+ const functionDesc = "filterMap(var, body)";
299
+ const invalidMsg = `${functionDesc} invalid iteration variable`;
300
+ const invalidBody = `${functionDesc} body`;
301
+ function typeCheck(chk, macro, ctx) {
302
+ const iterType = chk.check(macro.receiver, ctx);
303
+ let elemType;
304
+ if (iterType.kind === "dyn") elemType = iterType;
305
+ else if (iterType.kind === "list") elemType = iterType.valueType;
306
+ else if (iterType.kind === "map") elemType = iterType.keyType;
307
+ else {
308
+ throw chk.createError(
309
+ "invalid_comprehension_range",
310
+ `Expression of type '${iterType}' cannot be range of filterMap (must be list, map, or dynamic).`,
311
+ macro.receiver
312
+ );
313
+ }
314
+ macro.iterCtx = ctx.forkWithVariable(macro.iterVarName, elemType);
315
+ const bodyType = ensureOptionalType(chk, macro.body, macro.iterCtx, invalidBody);
316
+ return chk.registry.getListType(bindOptionalValueType(chk, bodyType));
317
+ }
318
+ function evaluate(ev, macro, ctx) {
319
+ return Effect.gen(function* () {
320
+ let items = yield* ev.eval(macro.receiver, ctx);
321
+ if (!Array.isArray(items)) {
322
+ if (items instanceof Set) items = [...items];
323
+ else if (items instanceof Map) items = [...items.keys()];
324
+ else if (items && typeof items === "object") items = Object.keys(items);
325
+ else {
326
+ return yield* Effect.fail(
327
+ ev.createError(
328
+ "invalid_comprehension_range",
329
+ `Expression of type '${ev.debugType(items)}' cannot be range of filterMap.`,
330
+ macro.receiver
331
+ )
332
+ );
333
+ }
334
+ }
335
+ const iterCtx = macro.iterCtx.reuse(ctx);
336
+ const arr = items;
337
+ const out = [];
338
+ for (let i = 0; i < arr.length; i++) {
339
+ const result = yield* ev.eval(macro.body, iterCtx.setIterValue(arr[i], ev));
340
+ const optional = ensureOptional(result, macro.body, invalidBody);
341
+ if (Option.isSome(optional)) out.push(optional.value);
342
+ }
343
+ return out;
344
+ });
345
+ }
346
+ return (opts) => {
347
+ const { args, receiver, ast } = opts;
348
+ return {
349
+ ast,
350
+ receiver,
351
+ iterVarName: assertIdentifier(args[0], invalidMsg),
352
+ body: args[1],
353
+ iterCtx: void 0,
354
+ evaluate,
355
+ typeCheck
356
+ };
357
+ };
358
+ }
297
359
  function createHasExpander() {
298
360
  const invalidHasArgument = "has() invalid argument";
299
361
  function evaluate(ev, macro, ctx) {
@@ -395,6 +457,7 @@ export function registerMacros(registry) {
395
457
  functionOverload("list.map(ast, ast, ast): list<dyn>", createMapExpander(true));
396
458
  functionOverload("list.filter(ast, ast): list<dyn>", createFilterExpander());
397
459
  functionOverload("list.find(ast, ast): optional<dyn>", createFindExpander());
460
+ functionOverload("list.filterMap(ast, ast): list<dyn>", createFilterMapExpander());
398
461
  class CelNamespace {
399
462
  #kind = "cel";
400
463
  toString() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shwfed/config",
3
- "version": "3.0.8",
3
+ "version": "3.0.10",
4
4
  "description": "Configurable UI for SHWFED",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -36,6 +36,7 @@
36
36
  "lint": "eslint .",
37
37
  "test": "vitest run",
38
38
  "test:watch": "vitest watch",
39
+ "test:ai-eval": "AI_EVAL=1 vitest run --config vitest.eval.config.mjs",
39
40
  "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
40
41
  },
41
42
  "dependencies": {