@colixsystems/widget-sdk 0.284.0 → 0.286.0

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.
package/README.md CHANGED
@@ -89,7 +89,15 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
89
89
 
90
90
  ## Status
91
91
 
92
- `v0.284.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
92
+ `v0.286.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
93
+
94
+ ### What's new in 0.286.0 (contract 1.234.0)
95
+
96
+ **Action scripts only: `previous` and `changedFields`.** `CONTRACT.actionScriptGlobals` lists the two new runner globals a `record_updated` Action script reads: the row before the edit and the columns whose value changed. Nothing changes for widget authors.
97
+
98
+ ### What's new in 0.285.0 (contract 1.233.0)
99
+
100
+ **Host only: container background patterns.** `@colixsystems/widget-sdk/host` exports `normaliseContainerPattern`, `containerPatternTile` and `containerPatternSvg`, the one definition of a layout container's `backgroundPattern` (dots, grid, lines, stripes, grain) that the web Player and the native export both tile. Nothing changes for widget authors.
93
101
 
94
102
  ### What's new in 0.284.0 (contract 1.233.0)
95
103
 
@@ -0,0 +1,196 @@
1
+ // GENERATED from container-pattern.js by scripts/generate-cjs.js — DO NOT EDIT.
2
+ // Change container-pattern.js, then run `npm run generate:cjs` in packages/widget-sdk.
3
+ "use strict";
4
+
5
+ const { isHexColor } = require("./contract.cjs");
6
+
7
+ // sc-14027 — a layout container's background pattern: dot paper, a grid, ruled
8
+ // lines, a diagonal hatch or film grain, tiled over the fill and under the
9
+ // scrim. The ONE definition of the vocabulary and of each tile's geometry, so
10
+ // the web Player (a CSS background tile), the compiled Expo app (a
11
+ // react-native-svg <Pattern>) and Mason's plan validator cannot disagree.
12
+ //
13
+ // Host-integration surface, re-exported from `host.js`.
14
+
15
+ const CONTAINER_PATTERN_KINDS = Object.freeze([
16
+ "dots",
17
+ "grid",
18
+ "lines",
19
+ "stripes",
20
+ "grain",
21
+ ]);
22
+
23
+ // Tile edge in px (grain: the scale of its specks). Opacity is a 0-100
24
+ // percentage, like every other opacity a container style carries.
25
+ const CONTAINER_PATTERN_SIZE = Object.freeze({ min: 4, max: 64, default: 12 });
26
+ const CONTAINER_PATTERN_OPACITY = Object.freeze({ min: 0, max: 100, default: 15 });
27
+
28
+ const FALLBACK_COLOR = "#000000";
29
+ const PATTERN_KEYS = ["kind", "color", "size", "opacity"];
30
+
31
+ function clampNumber(value, { min, max, default: fallback }) {
32
+ if (value === undefined || value === null || value === "") return fallback;
33
+ const n = Number(value);
34
+ if (!Number.isFinite(n)) return fallback;
35
+ return Math.min(Math.max(n, min), max);
36
+ }
37
+
38
+ function inRange(value, { min, max }) {
39
+ return value === undefined || (typeof value === "number" && value >= min && value <= max);
40
+ }
41
+
42
+ // Strict: what a writer (Mason) must send. The hosts clamp instead, so a
43
+ // value only strict mode refuses still paints.
44
+ function isStrictPattern(raw) {
45
+ return (
46
+ Object.keys(raw).every((key) => PATTERN_KEYS.includes(key)) &&
47
+ (raw.color === undefined || raw.color === "" || isHexColor(raw.color)) &&
48
+ inRange(raw.size, CONTAINER_PATTERN_SIZE) &&
49
+ inRange(raw.opacity, CONTAINER_PATTERN_OPACITY)
50
+ );
51
+ }
52
+
53
+ /**
54
+ * Resolve an authored `style.backgroundPattern` into what a host paints.
55
+ *
56
+ * @param {unknown} raw `{ kind, color?, size?, opacity? }`
57
+ * @param {string} [fallbackColor] the ink an unset colour takes, usually the surface's `onSurface`
58
+ * @param {{strict?: boolean}} [options] strict: refuse unknown keys and out-of-range values instead of clamping
59
+ * @returns {{kind: string, color: string, size: number, opacity: number}|null}
60
+ * null when no recognised kind is set, so an unpatterned container paints nothing new.
61
+ * `opacity` is the 0-1 alpha.
62
+ */
63
+ function normaliseContainerPattern(raw, fallbackColor, { strict = false } = {}) {
64
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
65
+ if (!CONTAINER_PATTERN_KINDS.includes(raw.kind)) return null;
66
+ if (strict && !isStrictPattern(raw)) return null;
67
+ // Hex only: the colour is written into SVG markup on the web.
68
+ const color = isHexColor(raw.color)
69
+ ? raw.color
70
+ : isHexColor(fallbackColor)
71
+ ? fallbackColor
72
+ : FALLBACK_COLOR;
73
+ const opacity = clampNumber(raw.opacity, CONTAINER_PATTERN_OPACITY) / 100;
74
+ if (opacity === 0) return null;
75
+ return {
76
+ kind: raw.kind,
77
+ color,
78
+ size: Math.round(clampNumber(raw.size, CONTAINER_PATTERN_SIZE)),
79
+ opacity,
80
+ };
81
+ }
82
+
83
+ const round2 = (n) => Math.round(n * 100) / 100;
84
+
85
+ // The grain tile: a fixed speckle on a GRAIN_CELLS square grid, seeded so every
86
+ // host and every render draws the same noise. Not feTurbulence: native has no
87
+ // filter primitives, and a computed tile is identical on both hosts.
88
+ const GRAIN_CELLS = 48;
89
+ const GRAIN_LEVELS = [0.35, 0.6, 1];
90
+ const GRAIN_SPECKS = (() => {
91
+ let seed = 0x14027;
92
+ const next = () => {
93
+ seed = (seed + 0x6d2b79f5) | 0;
94
+ let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
95
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
96
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
97
+ };
98
+ const byLevel = GRAIN_LEVELS.map(() => []);
99
+ for (let y = 0; y < GRAIN_CELLS; y += 1) {
100
+ for (let x = 0; x < GRAIN_CELLS; x += 1) {
101
+ if (next() < 0.1) byLevel[Math.floor(next() * GRAIN_LEVELS.length)].push([x, y]);
102
+ }
103
+ }
104
+ return byLevel;
105
+ })();
106
+
107
+ function grainTile(size) {
108
+ const cell = size / CONTAINER_PATTERN_SIZE.default;
109
+ const c = round2(cell);
110
+ return {
111
+ size: round2(GRAIN_CELLS * cell),
112
+ paths: GRAIN_SPECKS.map((specks, i) => ({
113
+ d: specks.map(([x, y]) => `M${round2(x * cell)} ${round2(y * cell)}h${c}v${c}h-${c}z`).join(""),
114
+ fill: true,
115
+ alpha: GRAIN_LEVELS[i],
116
+ })),
117
+ };
118
+ }
119
+
120
+ // Tiles depend only on kind and the clamped size, so the cache stays small.
121
+ const TILE_CACHE = new Map();
122
+
123
+ /**
124
+ * The repeating tile for a resolved pattern, as plain SVG path data both hosts
125
+ * draw: the web as an SVG string, native as react-native-svg `<Path>`s.
126
+ *
127
+ * @param {{kind: string, size: number}} pattern output of {@link normaliseContainerPattern}
128
+ * @returns {{size: number, paths: Array<{d: string, fill?: boolean, strokeWidth?: number, alpha?: number}>}}
129
+ * shared and cached: do not mutate it.
130
+ */
131
+ function containerPatternTile(pattern) {
132
+ const key = `${pattern.kind}:${pattern.size}`;
133
+ if (!TILE_CACHE.has(key)) TILE_CACHE.set(key, buildTile(pattern.kind, pattern.size));
134
+ return TILE_CACHE.get(key);
135
+ }
136
+
137
+ function buildTile(kind, s) {
138
+ if (kind === "grain") return grainTile(s);
139
+ if (kind === "dots") {
140
+ const r = round2(Math.max(1, s / 8));
141
+ const c = s / 2;
142
+ return {
143
+ size: s,
144
+ paths: [{ d: `M${round2(c - r)} ${c}a${r} ${r} 0 1 0 ${round2(r * 2)} 0a${r} ${r} 0 1 0 -${round2(r * 2)} 0z`, fill: true }],
145
+ };
146
+ }
147
+ if (kind === "grid") {
148
+ return { size: s, paths: [{ d: `M0 0.5H${s}M0.5 0V${s}`, strokeWidth: 1 }] };
149
+ }
150
+ if (kind === "lines") {
151
+ return { size: s, paths: [{ d: `M0 0.5H${s}`, strokeWidth: 1 }] };
152
+ }
153
+ // stripes: a 45° hatch. The two corner segments complete the stripe the
154
+ // neighbouring tiles cut, so the hatch runs unbroken across tile edges.
155
+ const h = s / 2;
156
+ return {
157
+ size: s,
158
+ paths: [
159
+ {
160
+ d: `M0 ${s}L${s} 0M-${h} ${h}L${h} -${h}M${h} ${s + h}L${s + h} ${h}`,
161
+ strokeWidth: round2(Math.max(1, s / 4)),
162
+ },
163
+ ],
164
+ };
165
+ }
166
+
167
+ /**
168
+ * The tile as standalone SVG markup, for the web host's repeating CSS
169
+ * background. The colour is hex-validated by {@link normaliseContainerPattern}
170
+ * and the path data is numeric, so nothing authored reaches the markup raw.
171
+ *
172
+ * @param {{kind: string, color: string, size: number}} pattern
173
+ * @returns {string}
174
+ */
175
+ function containerPatternSvg(pattern) {
176
+ const tile = containerPatternTile(pattern);
177
+ const paths = tile.paths
178
+ .map((p) => {
179
+ const paint = p.fill
180
+ ? `fill="${pattern.color}"`
181
+ : `fill="none" stroke="${pattern.color}" stroke-width="${p.strokeWidth}"`;
182
+ const alpha = p.alpha === undefined ? "" : ` opacity="${p.alpha}"`;
183
+ return `<path d="${p.d}" ${paint}${alpha}/>`;
184
+ })
185
+ .join("");
186
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${tile.size}" height="${tile.size}">${paths}</svg>`;
187
+ }
188
+
189
+ module.exports = {
190
+ CONTAINER_PATTERN_KINDS,
191
+ CONTAINER_PATTERN_SIZE,
192
+ CONTAINER_PATTERN_OPACITY,
193
+ normaliseContainerPattern,
194
+ containerPatternTile,
195
+ containerPatternSvg,
196
+ };
@@ -0,0 +1,191 @@
1
+ // sc-14027 — a layout container's background pattern: dot paper, a grid, ruled
2
+ // lines, a diagonal hatch or film grain, tiled over the fill and under the
3
+ // scrim. The ONE definition of the vocabulary and of each tile's geometry, so
4
+ // the web Player (a CSS background tile), the compiled Expo app (a
5
+ // react-native-svg <Pattern>) and Mason's plan validator cannot disagree.
6
+ //
7
+ // Host-integration surface, re-exported from `host.js`.
8
+ import { isHexColor } from "./contract.js";
9
+
10
+ const CONTAINER_PATTERN_KINDS = Object.freeze([
11
+ "dots",
12
+ "grid",
13
+ "lines",
14
+ "stripes",
15
+ "grain",
16
+ ]);
17
+
18
+ // Tile edge in px (grain: the scale of its specks). Opacity is a 0-100
19
+ // percentage, like every other opacity a container style carries.
20
+ const CONTAINER_PATTERN_SIZE = Object.freeze({ min: 4, max: 64, default: 12 });
21
+ const CONTAINER_PATTERN_OPACITY = Object.freeze({ min: 0, max: 100, default: 15 });
22
+
23
+ const FALLBACK_COLOR = "#000000";
24
+ const PATTERN_KEYS = ["kind", "color", "size", "opacity"];
25
+
26
+ function clampNumber(value, { min, max, default: fallback }) {
27
+ if (value === undefined || value === null || value === "") return fallback;
28
+ const n = Number(value);
29
+ if (!Number.isFinite(n)) return fallback;
30
+ return Math.min(Math.max(n, min), max);
31
+ }
32
+
33
+ function inRange(value, { min, max }) {
34
+ return value === undefined || (typeof value === "number" && value >= min && value <= max);
35
+ }
36
+
37
+ // Strict: what a writer (Mason) must send. The hosts clamp instead, so a
38
+ // value only strict mode refuses still paints.
39
+ function isStrictPattern(raw) {
40
+ return (
41
+ Object.keys(raw).every((key) => PATTERN_KEYS.includes(key)) &&
42
+ (raw.color === undefined || raw.color === "" || isHexColor(raw.color)) &&
43
+ inRange(raw.size, CONTAINER_PATTERN_SIZE) &&
44
+ inRange(raw.opacity, CONTAINER_PATTERN_OPACITY)
45
+ );
46
+ }
47
+
48
+ /**
49
+ * Resolve an authored `style.backgroundPattern` into what a host paints.
50
+ *
51
+ * @param {unknown} raw `{ kind, color?, size?, opacity? }`
52
+ * @param {string} [fallbackColor] the ink an unset colour takes, usually the surface's `onSurface`
53
+ * @param {{strict?: boolean}} [options] strict: refuse unknown keys and out-of-range values instead of clamping
54
+ * @returns {{kind: string, color: string, size: number, opacity: number}|null}
55
+ * null when no recognised kind is set, so an unpatterned container paints nothing new.
56
+ * `opacity` is the 0-1 alpha.
57
+ */
58
+ function normaliseContainerPattern(raw, fallbackColor, { strict = false } = {}) {
59
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
60
+ if (!CONTAINER_PATTERN_KINDS.includes(raw.kind)) return null;
61
+ if (strict && !isStrictPattern(raw)) return null;
62
+ // Hex only: the colour is written into SVG markup on the web.
63
+ const color = isHexColor(raw.color)
64
+ ? raw.color
65
+ : isHexColor(fallbackColor)
66
+ ? fallbackColor
67
+ : FALLBACK_COLOR;
68
+ const opacity = clampNumber(raw.opacity, CONTAINER_PATTERN_OPACITY) / 100;
69
+ if (opacity === 0) return null;
70
+ return {
71
+ kind: raw.kind,
72
+ color,
73
+ size: Math.round(clampNumber(raw.size, CONTAINER_PATTERN_SIZE)),
74
+ opacity,
75
+ };
76
+ }
77
+
78
+ const round2 = (n) => Math.round(n * 100) / 100;
79
+
80
+ // The grain tile: a fixed speckle on a GRAIN_CELLS square grid, seeded so every
81
+ // host and every render draws the same noise. Not feTurbulence: native has no
82
+ // filter primitives, and a computed tile is identical on both hosts.
83
+ const GRAIN_CELLS = 48;
84
+ const GRAIN_LEVELS = [0.35, 0.6, 1];
85
+ const GRAIN_SPECKS = (() => {
86
+ let seed = 0x14027;
87
+ const next = () => {
88
+ seed = (seed + 0x6d2b79f5) | 0;
89
+ let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
90
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
91
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
92
+ };
93
+ const byLevel = GRAIN_LEVELS.map(() => []);
94
+ for (let y = 0; y < GRAIN_CELLS; y += 1) {
95
+ for (let x = 0; x < GRAIN_CELLS; x += 1) {
96
+ if (next() < 0.1) byLevel[Math.floor(next() * GRAIN_LEVELS.length)].push([x, y]);
97
+ }
98
+ }
99
+ return byLevel;
100
+ })();
101
+
102
+ function grainTile(size) {
103
+ const cell = size / CONTAINER_PATTERN_SIZE.default;
104
+ const c = round2(cell);
105
+ return {
106
+ size: round2(GRAIN_CELLS * cell),
107
+ paths: GRAIN_SPECKS.map((specks, i) => ({
108
+ d: specks.map(([x, y]) => `M${round2(x * cell)} ${round2(y * cell)}h${c}v${c}h-${c}z`).join(""),
109
+ fill: true,
110
+ alpha: GRAIN_LEVELS[i],
111
+ })),
112
+ };
113
+ }
114
+
115
+ // Tiles depend only on kind and the clamped size, so the cache stays small.
116
+ const TILE_CACHE = new Map();
117
+
118
+ /**
119
+ * The repeating tile for a resolved pattern, as plain SVG path data both hosts
120
+ * draw: the web as an SVG string, native as react-native-svg `<Path>`s.
121
+ *
122
+ * @param {{kind: string, size: number}} pattern output of {@link normaliseContainerPattern}
123
+ * @returns {{size: number, paths: Array<{d: string, fill?: boolean, strokeWidth?: number, alpha?: number}>}}
124
+ * shared and cached: do not mutate it.
125
+ */
126
+ function containerPatternTile(pattern) {
127
+ const key = `${pattern.kind}:${pattern.size}`;
128
+ if (!TILE_CACHE.has(key)) TILE_CACHE.set(key, buildTile(pattern.kind, pattern.size));
129
+ return TILE_CACHE.get(key);
130
+ }
131
+
132
+ function buildTile(kind, s) {
133
+ if (kind === "grain") return grainTile(s);
134
+ if (kind === "dots") {
135
+ const r = round2(Math.max(1, s / 8));
136
+ const c = s / 2;
137
+ return {
138
+ size: s,
139
+ paths: [{ d: `M${round2(c - r)} ${c}a${r} ${r} 0 1 0 ${round2(r * 2)} 0a${r} ${r} 0 1 0 -${round2(r * 2)} 0z`, fill: true }],
140
+ };
141
+ }
142
+ if (kind === "grid") {
143
+ return { size: s, paths: [{ d: `M0 0.5H${s}M0.5 0V${s}`, strokeWidth: 1 }] };
144
+ }
145
+ if (kind === "lines") {
146
+ return { size: s, paths: [{ d: `M0 0.5H${s}`, strokeWidth: 1 }] };
147
+ }
148
+ // stripes: a 45° hatch. The two corner segments complete the stripe the
149
+ // neighbouring tiles cut, so the hatch runs unbroken across tile edges.
150
+ const h = s / 2;
151
+ return {
152
+ size: s,
153
+ paths: [
154
+ {
155
+ d: `M0 ${s}L${s} 0M-${h} ${h}L${h} -${h}M${h} ${s + h}L${s + h} ${h}`,
156
+ strokeWidth: round2(Math.max(1, s / 4)),
157
+ },
158
+ ],
159
+ };
160
+ }
161
+
162
+ /**
163
+ * The tile as standalone SVG markup, for the web host's repeating CSS
164
+ * background. The colour is hex-validated by {@link normaliseContainerPattern}
165
+ * and the path data is numeric, so nothing authored reaches the markup raw.
166
+ *
167
+ * @param {{kind: string, color: string, size: number}} pattern
168
+ * @returns {string}
169
+ */
170
+ function containerPatternSvg(pattern) {
171
+ const tile = containerPatternTile(pattern);
172
+ const paths = tile.paths
173
+ .map((p) => {
174
+ const paint = p.fill
175
+ ? `fill="${pattern.color}"`
176
+ : `fill="none" stroke="${pattern.color}" stroke-width="${p.strokeWidth}"`;
177
+ const alpha = p.alpha === undefined ? "" : ` opacity="${p.alpha}"`;
178
+ return `<path d="${p.d}" ${paint}${alpha}/>`;
179
+ })
180
+ .join("");
181
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${tile.size}" height="${tile.size}">${paths}</svg>`;
182
+ }
183
+
184
+ export {
185
+ CONTAINER_PATTERN_KINDS,
186
+ CONTAINER_PATTERN_SIZE,
187
+ CONTAINER_PATTERN_OPACITY,
188
+ normaliseContainerPattern,
189
+ containerPatternTile,
190
+ containerPatternSvg,
191
+ };
package/dist/contract.cjs CHANGED
@@ -3945,6 +3945,9 @@ const ACTION_SCRIPT_GLOBALS = [
3945
3945
  "directory",
3946
3946
  "console",
3947
3947
  "record",
3948
+ // sc-15609 — on record_updated, the row before the edit and what changed.
3949
+ "previous",
3950
+ "changedFields",
3948
3951
  // sc-5366 — `{ body }` on a run fired by the inbound `http_post` trigger,
3949
3952
  // null on every other trigger.
3950
3953
  "request",
@@ -6719,7 +6722,9 @@ const CONTRACT = deepFreeze({
6719
6722
  // "Rows per page" property.
6720
6723
  // 1.233.0: additive (sc-8115) — a `datastoreTemplate` table may declare
6721
6724
  // `deleteWhenLastMemberLeaves: true`.
6722
- version: "1.233.0",
6725
+ // 1.234.0: additive (sc-15609) — ACTION_SCRIPT_GLOBALS gains `previous` and
6726
+ // `changedFields`; no widget hook, primitive or manifest field changed.
6727
+ version: "1.234.0",
6723
6728
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6724
6729
  hooks: HOOKS,
6725
6730
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -3941,6 +3941,9 @@ const ACTION_SCRIPT_GLOBALS = [
3941
3941
  "directory",
3942
3942
  "console",
3943
3943
  "record",
3944
+ // sc-15609 — on record_updated, the row before the edit and what changed.
3945
+ "previous",
3946
+ "changedFields",
3944
3947
  // sc-5366 — `{ body }` on a run fired by the inbound `http_post` trigger,
3945
3948
  // null on every other trigger.
3946
3949
  "request",
@@ -6715,7 +6718,9 @@ const CONTRACT = deepFreeze({
6715
6718
  // "Rows per page" property.
6716
6719
  // 1.233.0: additive (sc-8115) — a `datastoreTemplate` table may declare
6717
6720
  // `deleteWhenLastMemberLeaves: true`.
6718
- version: "1.233.0",
6721
+ // 1.234.0: additive (sc-15609) — ACTION_SCRIPT_GLOBALS gains `previous` and
6722
+ // `changedFields`; no widget hook, primitive or manifest field changed.
6723
+ version: "1.234.0",
6719
6724
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6720
6725
  hooks: HOOKS,
6721
6726
  primitives: PRIMITIVES,
package/dist/host.d.ts CHANGED
@@ -712,3 +712,33 @@ export const DARK_PALETTE_KEYS: readonly string[];
712
712
  * appearance setting stores it: the base light, `dark` the dark palette.
713
713
  */
714
714
  export function themeInBothModes<T>(theme: T, mode: AppearanceMode, twin: unknown): T;
715
+
716
+ /** A container background pattern, resolved for painting. `opacity` is 0-1. */
717
+ export interface ContainerPattern {
718
+ kind: "dots" | "grid" | "lines" | "stripes" | "grain";
719
+ color: string;
720
+ size: number;
721
+ opacity: number;
722
+ }
723
+
724
+ /** One path of a pattern tile: filled, or stroked at `strokeWidth`. */
725
+ export interface ContainerPatternPath {
726
+ d: string;
727
+ fill?: boolean;
728
+ strokeWidth?: number;
729
+ alpha?: number;
730
+ }
731
+
732
+ export const CONTAINER_PATTERN_KINDS: readonly ContainerPattern["kind"][];
733
+ export const CONTAINER_PATTERN_SIZE: { readonly min: number; readonly max: number; readonly default: number };
734
+ export const CONTAINER_PATTERN_OPACITY: { readonly min: number; readonly max: number; readonly default: number };
735
+ /** A container's `style.backgroundPattern`, or null when it names no known kind. */
736
+ export function normaliseContainerPattern(
737
+ raw: unknown,
738
+ fallbackColor?: string,
739
+ options?: { strict?: boolean },
740
+ ): ContainerPattern | null;
741
+ /** The repeating tile both hosts draw. */
742
+ export function containerPatternTile(pattern: ContainerPattern): { size: number; paths: ContainerPatternPath[] };
743
+ /** The tile as standalone SVG markup, for a CSS background. */
744
+ export function containerPatternSvg(pattern: ContainerPattern): string;
package/dist/host.js CHANGED
@@ -173,3 +173,14 @@ export {
173
173
  dateTimeLocalInputToIso,
174
174
  formatDisplayLabel as formatDateTimeLabel,
175
175
  } from "./datetimepicker-format.js";
176
+
177
+ // sc-14027: a container's background pattern — vocabulary and tile geometry,
178
+ // so the Player's CSS tile and the export's <Pattern> draw the same thing.
179
+ export {
180
+ CONTAINER_PATTERN_KINDS,
181
+ CONTAINER_PATTERN_SIZE,
182
+ CONTAINER_PATTERN_OPACITY,
183
+ normaliseContainerPattern,
184
+ containerPatternTile,
185
+ containerPatternSvg,
186
+ } from "./container-pattern.js";
@@ -3,6 +3,7 @@
3
3
  "use strict";
4
4
 
5
5
  const { CONTRACT, isHexColor, contrastRatio, mixHex, readableTextColor } = require("./contract.cjs");
6
+ const { isPlainObject } = require("./plain-object.cjs");
6
7
 
7
8
  // REQ-NAV-STRUCTURE — the host side of an app's navigation SHAPE and the
8
9
  // tokens its footer strip is painted with.
@@ -27,10 +28,6 @@ const { CONTRACT, isHexColor, contrastRatio, mixHex, readableTextColor } = requi
27
28
  // existed renders and compiles byte-identically.
28
29
  const DEFAULT_MENU_TYPE = "sidebar";
29
30
 
30
- function isPlainObject(value) {
31
- return value !== null && typeof value === "object" && !Array.isArray(value);
32
- }
33
-
34
31
  /**
35
32
  * Resolve a `theme_config.navigation` block to the shape both hosts read.
36
33
  *
@@ -17,15 +17,12 @@
17
17
  // diverge.
18
18
 
19
19
  import { CONTRACT, isHexColor, contrastRatio, mixHex, readableTextColor } from "./contract.js";
20
+ import { isPlainObject } from "./plain-object.js";
20
21
 
21
22
  // Absent or unknown resolves here, so every app authored before menu types
22
23
  // existed renders and compiles byte-identically.
23
24
  const DEFAULT_MENU_TYPE = "sidebar";
24
25
 
25
- function isPlainObject(value) {
26
- return value !== null && typeof value === "object" && !Array.isArray(value);
27
- }
28
-
29
26
  /**
30
27
  * Resolve a `theme_config.navigation` block to the shape both hosts read.
31
28
  *
@@ -0,0 +1,11 @@
1
+ // GENERATED from plain-object.js by scripts/generate-cjs.js — DO NOT EDIT.
2
+ // Change plain-object.js, then run `npm run generate:cjs` in packages/widget-sdk.
3
+ "use strict";
4
+
5
+ /** Any non-null, non-array object — a Date, Map or class instance passes. */
6
+ function isPlainObject(value) {
7
+ return value !== null && typeof value === "object" && !Array.isArray(value);
8
+ }
9
+ module.exports = {
10
+ isPlainObject,
11
+ };
@@ -0,0 +1,4 @@
1
+ /** Any non-null, non-array object — a Date, Map or class instance passes. */
2
+ export function isPlainObject(value) {
3
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4
+ }
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { CORNER_RADIUS_KEYS } from "./corner-radius.js";
5
5
  import { SPACING_KEYS } from "./spacing.js";
6
+ import { isPlainObject } from "./plain-object.js";
6
7
 
7
8
  const VALID_TYPES = new Set([
8
9
  "string", "number", "boolean",
@@ -85,10 +86,6 @@ const VALID_TYPES = new Set([
85
86
  /** The propertySchema field types, for documentation generated from this list. */
86
87
  export const PROPERTY_TYPES = Object.freeze([...VALID_TYPES]);
87
88
 
88
- function isPlainObject(v) {
89
- return v !== null && typeof v === "object" && !Array.isArray(v);
90
- }
91
-
92
89
  function validateDef(def, path, errors) {
93
90
  if (!isPlainObject(def)) {
94
91
  errors.push(`${path}: must be an object`);
@@ -2,6 +2,8 @@
2
2
  // a sign order's state, what a refusal says, and how an overview is indexed.
3
3
  // Kept apart from hooks.js so it runs under plain `node --test`.
4
4
 
5
+ import { isPlainObject } from "./plain-object.js";
6
+
5
7
  /** The server reads at most this many file ids per overview call. */
6
8
  export const OVERVIEW_IDS_PER_CALL = 200;
7
9
 
@@ -83,12 +85,11 @@ export function readRefusalDetail(err) {
83
85
  }
84
86
 
85
87
  const nonEmptyString = (value) => typeof value === "string" && value.trim() !== "";
86
- const isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
87
88
 
88
89
  /** One prepare entry as the filestore client takes it, or null when it is neither form. */
89
90
  function signerForPrepare(signer) {
90
91
  if (nonEmptyString(signer)) return signer;
91
- if (!isObject(signer)) return null;
92
+ if (!isPlainObject(signer)) return null;
92
93
  const keys = Object.keys(signer).sort().join(",");
93
94
  if (keys === "app_user_id" && nonEmptyString(signer.app_user_id)) return signer.app_user_id;
94
95
  if (keys === "email,name" && nonEmptyString(signer.name) && nonEmptyString(signer.email)) {
@@ -111,7 +112,7 @@ export function signerListForPrepare(signers) {
111
112
 
112
113
  /** The body of a new link: a corrected `email` when one is given, otherwise nothing. */
113
114
  export function resendLinkOptions(options) {
114
- const email = isObject(options) && typeof options.email === "string" ? options.email.trim() : "";
115
+ const email = isPlainObject(options) && typeof options.email === "string" ? options.email.trim() : "";
115
116
  return email ? { email } : {};
116
117
  }
117
118
 
@@ -3,6 +3,7 @@
3
3
  "use strict";
4
4
 
5
5
  const { isHexColor, APPEARANCE_PREFERENCES } = require("./contract.cjs");
6
+ const { isPlainObject } = require("./plain-object.cjs");
6
7
 
7
8
  // APPEARANCE_PREFERENCES: what an app opens on and a user may choose.
8
9
 
@@ -21,10 +22,6 @@ const APPEARANCE_THEME_KEYS = ["dark", "appearance"];
21
22
  /** The two palettes an app can render. */
22
23
  const APPEARANCE_MODES = Object.freeze(["light", "dark"]);
23
24
 
24
- function isPlainObject(value) {
25
- return value !== null && typeof value === "object" && !Array.isArray(value);
26
- }
27
-
28
25
  // A gradient is one colour value: its angle and type travel with its stops.
29
26
  function isGradient(value) {
30
27
  return isPlainObject(value) && isHexColor(value.from) && isHexColor(value.to);
@@ -1,5 +1,6 @@
1
1
  // APPEARANCE_PREFERENCES: what an app opens on and a user may choose.
2
2
  import { isHexColor, APPEARANCE_PREFERENCES } from "./contract.js";
3
+ import { isPlainObject } from "./plain-object.js";
3
4
 
4
5
  export { APPEARANCE_PREFERENCES };
5
6
 
@@ -17,10 +18,6 @@ const APPEARANCE_THEME_KEYS = ["dark", "appearance"];
17
18
  /** The two palettes an app can render. */
18
19
  export const APPEARANCE_MODES = Object.freeze(["light", "dark"]);
19
20
 
20
- function isPlainObject(value) {
21
- return value !== null && typeof value === "object" && !Array.isArray(value);
22
- }
23
-
24
21
  // A gradient is one colour value: its angle and type travel with its stops.
25
22
  function isGradient(value) {
26
23
  return isPlainObject(value) && isHexColor(value.from) && isHexColor(value.to);
@@ -3,6 +3,7 @@
3
3
  "use strict";
4
4
 
5
5
  const { CONTRACT, normaliseComponentGradient } = require("./contract.cjs");
6
+ const { isPlainObject } = require("./plain-object.cjs");
6
7
 
7
8
  // REQ-THEME-15 (sc-1497) — the host side of per-component style tokens.
8
9
  //
@@ -26,10 +27,6 @@ const { CONTRACT, normaliseComponentGradient } = require("./contract.cjs");
26
27
  // must never drop a colour the runner already accepted.
27
28
  const HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
28
29
 
29
- function isPlainObject(value) {
30
- return value !== null && typeof value === "object" && !Array.isArray(value);
31
- }
32
-
33
30
  function coerceToken(def, value) {
34
31
  if (def.type === "color") {
35
32
  if (typeof value !== "string") return undefined;
@@ -16,15 +16,12 @@
16
16
  // literal, so what a planner may emit and what a host applies cannot diverge.
17
17
 
18
18
  import { CONTRACT, normaliseComponentGradient } from "./contract.js";
19
+ import { isPlainObject } from "./plain-object.js";
19
20
 
20
21
  // 3-, 6- and 8-digit hex, matching what the build runner persists — the host
21
22
  // must never drop a colour the runner already accepted.
22
23
  const HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
23
24
 
24
- function isPlainObject(value) {
25
- return value !== null && typeof value === "object" && !Array.isArray(value);
26
- }
27
-
28
25
  function coerceToken(def, value) {
29
26
  if (def.type === "color") {
30
27
  if (typeof value !== "string") return undefined;
@@ -23,6 +23,7 @@ import {
23
23
  withHexAlpha,
24
24
  THEME_TOKEN_REF_PREFIX,
25
25
  } from "./contract.js";
26
+ import { isPlainObject } from "./plain-object.js";
26
27
 
27
28
  // A value is a binding, not a colour, when it carries this prefix. No CSS
28
29
  // colour can begin with it, so a stored literal can never be misread as one.
@@ -67,10 +68,6 @@ export function resolveThemeTokenRef(value, theme) {
67
68
  return ref.alpha === null ? resolved : withHexAlpha(resolved, ref.alpha);
68
69
  }
69
70
 
70
- function isPlainObject(value) {
71
- return value !== null && typeof value === "object" && !Array.isArray(value);
72
- }
73
-
74
71
  // The same guard the neighbouring style validators apply: these props are
75
72
  // author-stored JSON, and assigning such a key while rebuilding an object would
76
73
  // silently reseat its prototype instead of carrying the value.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.284.0",
3
+ "version": "0.286.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "homepage": "https://github.com/Colix-AB/AppStudio",
6
6
  "type": "module",
package/src/contract.js CHANGED
@@ -3941,6 +3941,9 @@ const ACTION_SCRIPT_GLOBALS = [
3941
3941
  "directory",
3942
3942
  "console",
3943
3943
  "record",
3944
+ // sc-15609 — on record_updated, the row before the edit and what changed.
3945
+ "previous",
3946
+ "changedFields",
3944
3947
  // sc-5366 — `{ body }` on a run fired by the inbound `http_post` trigger,
3945
3948
  // null on every other trigger.
3946
3949
  "request",
@@ -6715,7 +6718,9 @@ const CONTRACT = deepFreeze({
6715
6718
  // "Rows per page" property.
6716
6719
  // 1.233.0: additive (sc-8115) — a `datastoreTemplate` table may declare
6717
6720
  // `deleteWhenLastMemberLeaves: true`.
6718
- version: "1.233.0",
6721
+ // 1.234.0: additive (sc-15609) — ACTION_SCRIPT_GLOBALS gains `previous` and
6722
+ // `changedFields`; no widget hook, primitive or manifest field changed.
6723
+ version: "1.234.0",
6719
6724
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6720
6725
  hooks: HOOKS,
6721
6726
  primitives: PRIMITIVES,