@colixsystems/widget-sdk 0.138.0 → 0.139.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
@@ -72,6 +72,17 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
72
72
 
73
73
  `v0.137.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**.
74
74
 
75
+ ### What's new in 0.139.0 (contract 1.108.0)
76
+
77
+ **An author can BIND a colour field to the workspace brand instead of freezing a hex (sc-7800).** A `type: "color"` field tracked the theme only while it was *unset*, through its `themeDefault` placeholder. The moment the author picked a colour it became a literal that outranked the theme forever, so "make this our brand colour" was inexpressible — the author had to type today's hex, and it went stale silently at the next rebrand.
78
+
79
+ A colour field's value may now be a **binding** — `"theme:colors.primary"` or `"theme:colors.secondary"` — which both hosts resolve to the workspace's current colour at render.
80
+
81
+ - **Nothing changes for a widget author.** You keep declaring `{ type: "color", … }` and reading the value off `props.style` / `props`. The host hands your code an ordinary colour string either way, so never branch on a binding, never parse the `theme:` prefix, and never add a "use theme colour" toggle beside a colour field — the Studio's picker already offers the choice, and a second control for it is a dead knob.
82
+ - **Only the two brand tokens are bindable**, listed on `CONTRACT.themeColorTokens`. The derived and contrast-paired tokens (`onPrimary`, `primarySoft`, `primaryStrong`, `loader`) are deliberately withheld: they are the host's to compute, and binding one half of a contrast pair produces unreadable text.
83
+ - **An unresolvable binding falls back to the field's `themeDefault`**, exactly as an unset field does — it never paints blank.
84
+ - **Fully additive.** Every colour already stored is a literal, is untouched, and renders byte-identically.
85
+
75
86
  ### What's new in 0.138.0 (contract 1.107.0)
76
87
 
77
88
  **Every file upload from the Expo export works again.** Expo's `fetch` implements the WEB FormData spec, so a part must be a string or a Blob — but `useCamera`, `useImageEditor` and `<FilePicker>` all handed back React Native's legacy `{ uri, name, type }` triple, which it rejects with `Unsupported FormDataPart implementation` before a byte leaves the device. Web was unaffected because a browser `File` **is** a Blob, so it only ever reproduced on device. All three now hand back expo-file-system's `File`, carrying the same `uri` / `name` / `type` / `size` — so no widget needs a change.
package/dist/contract.cjs CHANGED
@@ -121,6 +121,26 @@ const DEFAULT_THEME_TOKENS = Object.freeze({
121
121
  widgetStyles: Object.freeze({}),
122
122
  });
123
123
 
124
+ // sc-7800 -- the colour tokens an AUTHOR may BIND a `type: "color"` field to.
125
+ //
126
+ // A colour field already tracked the theme while unset (its `themeDefault`
127
+ // placeholder); the moment it was set it froze to a hex and stopped following a
128
+ // rebrand. A bound field stores `theme:<path>` instead, and the hosts resolve it
129
+ // per render, so the author's INTENT ("our brand colour") survives the rebrand.
130
+ //
131
+ // Deliberately only the two BRAND tokens. The derived and contrast-paired
132
+ // tokens -- `onPrimary`, `primarySoft`, `primaryStrong`, `loader` -- are the
133
+ // host's to compute: offering them would let an author pick one half of a
134
+ // contrast pair and produce unreadable text with no way to see it was their
135
+ // choice. `label` is the author-facing name; the Studio picker, the host
136
+ // resolver and the AI widget agent all read this ONE list, so what may be
137
+ // written and what will resolve cannot drift.
138
+ const THEME_COLOR_TOKENS = Object.freeze([
139
+ Object.freeze({ path: "colors.primary", label: "Primary" }),
140
+ Object.freeze({ path: "colors.secondary", label: "Secondary" }),
141
+ ]);
142
+
143
+
124
144
  // REQ-THEME-15 (sc-1497) — per-component style tokens. The global palette is a
125
145
  // single lever: remapping `primaryColor` to recolour "the buttons" recolours
126
146
  // every element that shares it. A theme may therefore also carry
@@ -3947,7 +3967,16 @@ const CONTRACT = deepFreeze({
3947
3967
  // ON the part otherwise, since expo-file-system's File is NOT
3948
3968
  // `instanceof Blob` at runtime and Expo reads the name off the part.
3949
3969
  // One upload path on both hosts, as before.
3950
- version: "1.107.0",
3970
+ // 1.108.0: additive (sc-7800) -- `themeColorTokens`: the brand colour tokens
3971
+ // an author may BIND a `type: "color"` field to, plus the `theme:<path>`
3972
+ // value grammar and the `resolveThemeTokenRefs` host resolver behind it.
3973
+ // A set colour field used to freeze to a hex literal and stop following
3974
+ // the workspace theme, so "make this our brand colour" was inexpressible
3975
+ // and every picked colour went stale on the next rebrand. Purely additive:
3976
+ // a stored hex is untouched and still renders byte-identically, and an
3977
+ // unresolvable binding falls back to the field's `themeDefault` rather
3978
+ // than painting blank.
3979
+ version: "1.108.0",
3951
3980
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
3952
3981
  hooks: HOOKS,
3953
3982
  primitives: PRIMITIVES,
@@ -3958,6 +3987,7 @@ const CONTRACT = deepFreeze({
3958
3987
  actionScriptGlobals: ACTION_SCRIPT_GLOBALS,
3959
3988
  actionScriptMaxBytes: ACTION_SCRIPT_MAX_BYTES,
3960
3989
  themeTokens: DEFAULT_THEME_TOKENS,
3990
+ themeColorTokens: THEME_COLOR_TOKENS,
3961
3991
  themeComponents: THEME_COMPONENTS,
3962
3992
  themeComponentShadows: THEME_COMPONENT_SHADOWS,
3963
3993
  themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
package/dist/contract.js CHANGED
@@ -121,6 +121,26 @@ const DEFAULT_THEME_TOKENS = Object.freeze({
121
121
  widgetStyles: Object.freeze({}),
122
122
  });
123
123
 
124
+ // sc-7800 -- the colour tokens an AUTHOR may BIND a `type: "color"` field to.
125
+ //
126
+ // A colour field already tracked the theme while unset (its `themeDefault`
127
+ // placeholder); the moment it was set it froze to a hex and stopped following a
128
+ // rebrand. A bound field stores `theme:<path>` instead, and the hosts resolve it
129
+ // per render, so the author's INTENT ("our brand colour") survives the rebrand.
130
+ //
131
+ // Deliberately only the two BRAND tokens. The derived and contrast-paired
132
+ // tokens -- `onPrimary`, `primarySoft`, `primaryStrong`, `loader` -- are the
133
+ // host's to compute: offering them would let an author pick one half of a
134
+ // contrast pair and produce unreadable text with no way to see it was their
135
+ // choice. `label` is the author-facing name; the Studio picker, the host
136
+ // resolver and the AI widget agent all read this ONE list, so what may be
137
+ // written and what will resolve cannot drift.
138
+ const THEME_COLOR_TOKENS = Object.freeze([
139
+ Object.freeze({ path: "colors.primary", label: "Primary" }),
140
+ Object.freeze({ path: "colors.secondary", label: "Secondary" }),
141
+ ]);
142
+
143
+
124
144
  // REQ-THEME-15 (sc-1497) — per-component style tokens. The global palette is a
125
145
  // single lever: remapping `primaryColor` to recolour "the buttons" recolours
126
146
  // every element that shares it. A theme may therefore also carry
@@ -3947,7 +3967,16 @@ const CONTRACT = deepFreeze({
3947
3967
  // ON the part otherwise, since expo-file-system's File is NOT
3948
3968
  // `instanceof Blob` at runtime and Expo reads the name off the part.
3949
3969
  // One upload path on both hosts, as before.
3950
- version: "1.107.0",
3970
+ // 1.108.0: additive (sc-7800) -- `themeColorTokens`: the brand colour tokens
3971
+ // an author may BIND a `type: "color"` field to, plus the `theme:<path>`
3972
+ // value grammar and the `resolveThemeTokenRefs` host resolver behind it.
3973
+ // A set colour field used to freeze to a hex literal and stop following
3974
+ // the workspace theme, so "make this our brand colour" was inexpressible
3975
+ // and every picked colour went stale on the next rebrand. Purely additive:
3976
+ // a stored hex is untouched and still renders byte-identically, and an
3977
+ // unresolvable binding falls back to the field's `themeDefault` rather
3978
+ // than painting blank.
3979
+ version: "1.108.0",
3951
3980
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
3952
3981
  hooks: HOOKS,
3953
3982
  primitives: PRIMITIVES,
@@ -3958,6 +3987,7 @@ const CONTRACT = deepFreeze({
3958
3987
  actionScriptGlobals: ACTION_SCRIPT_GLOBALS,
3959
3988
  actionScriptMaxBytes: ACTION_SCRIPT_MAX_BYTES,
3960
3989
  themeTokens: DEFAULT_THEME_TOKENS,
3990
+ themeColorTokens: THEME_COLOR_TOKENS,
3961
3991
  themeComponents: THEME_COMPONENTS,
3962
3992
  themeComponentShadows: THEME_COMPONENT_SHADOWS,
3963
3993
  themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
package/dist/host.d.ts CHANGED
@@ -334,3 +334,28 @@ export interface LoginDesign {
334
334
 
335
335
  /** The sign-in screen's authored design, or null when nothing is configured. */
336
336
  export function normaliseLoginDesign(raw: unknown): LoginDesign | null;
337
+
338
+ /** sc-7800 — true when a stored value is a `theme:<path>` colour binding. */
339
+ export function isThemeTokenRef(value: unknown): boolean;
340
+
341
+ /**
342
+ * Resolve one `theme:<path>` binding against a resolved widget theme. Returns
343
+ * `undefined` for a non-binding, a token outside `CONTRACT.themeColorTokens`,
344
+ * or a path that does not resolve to a colour.
345
+ */
346
+ export function resolveThemeTokenRef(
347
+ value: unknown,
348
+ theme: unknown,
349
+ ): string | undefined;
350
+
351
+ /**
352
+ * Replace every bound `type: "color"` field on a widget's props — both the
353
+ * top-level `propertySchema` fields and the `styleSchema` fields under
354
+ * `props.style` — with the colour its token currently resolves to. Returns the
355
+ * same reference when nothing is bound.
356
+ */
357
+ export function resolveThemeTokenRefs<T>(
358
+ props: T,
359
+ theme: unknown,
360
+ schemas?: { propertySchema?: unknown; styleSchema?: unknown },
361
+ ): T;
package/dist/host.js CHANGED
@@ -23,6 +23,17 @@ export {
23
23
  applyThemeComponentStyle,
24
24
  } from "./theme-components.js";
25
25
 
26
+ // sc-7800: the theme-token colour bindings. `resolveThemeTokenRefs` replaces a
27
+ // bound `type: "color"` field with the colour its token currently resolves to,
28
+ // at the SAME render boundary on both hosts — so a rebrand repaints the Player,
29
+ // the Canvas and the exported app alike, and the widget still receives a plain
30
+ // colour string.
31
+ export {
32
+ isThemeTokenRef,
33
+ resolveThemeTokenRef,
34
+ resolveThemeTokenRefs,
35
+ } from "./theme-token-refs.js";
36
+
26
37
  // REQ-THEME-SURFACE: the surface-token derivation both hosts apply per painted
27
38
  // surface — the page background and every container that carries its own fill.
28
39
  // Host plumbing, deliberately off the author entry point: a widget reads the
package/dist/index.d.ts CHANGED
@@ -2700,6 +2700,8 @@ export interface AiWidgetContract {
2700
2700
  readonly manifestCategories: ReadonlyArray<string>;
2701
2701
  readonly manifestPlatforms: ReadonlyArray<string>;
2702
2702
  readonly themeTokens: ThemeTokens;
2703
+ /** sc-7800 — the brand colour tokens a `type: "color"` field may bind to. */
2704
+ readonly themeColorTokens: ReadonlyArray<{ path: string; label: string }>;
2703
2705
  readonly widgetContextShape: Readonly<
2704
2706
  Record<
2705
2707
  string,
@@ -0,0 +1,198 @@
1
+ // sc-7800 — the host side of THEME-TOKEN COLOUR BINDINGS.
2
+ //
3
+ // Host-integration surface, re-exported from `host.js`: consumed ONLY by the
4
+ // platform hosts that render widgets (the web Player / Studio Canvas via
5
+ // buildHostWidgetContext.js, and the exported Expo app's generated WidgetHost),
6
+ // never by a widget author. A widget keeps reading a plain colour string off
7
+ // `props` / `props.style` and never learns a token was involved.
8
+ //
9
+ // Living here — one implementation both hosts import — is what makes a bound
10
+ // colour resolve identically in the Player and the export (widget-parity
11
+ // skill). The alternative, a copy per host, is the drift this file exists to
12
+ // prevent.
13
+ //
14
+ // WHY THIS EXISTS: a `type: "color"` field already tracks the workspace theme
15
+ // while it is UNSET, through its `themeDefault` placeholder. The moment an
16
+ // author picks a colour it became a frozen hex that never followed a rebrand
17
+ // again, and "make this our brand colour" was inexpressible — the author had to
18
+ // type today's hex, which went stale silently. A bound field stores the TOKEN,
19
+ // so the intent survives the rebrand.
20
+
21
+ import { CONTRACT } from "./contract.js";
22
+
23
+ // A value is a binding, not a colour, when it carries this prefix. No CSS
24
+ // colour can begin with it, so a stored literal can never be misread as one.
25
+ const PREFIX = "theme:";
26
+
27
+ /** True when `value` is a theme-token binding rather than a literal colour. */
28
+ export function isThemeTokenRef(value) {
29
+ return typeof value === "string" && value.startsWith(PREFIX);
30
+ }
31
+
32
+ /**
33
+ * Resolve one binding against a resolved widget theme.
34
+ *
35
+ * Returns `undefined` for anything that is not a binding, names a token outside
36
+ * the bindable allowlist, or resolves to a non-string — the caller then leaves
37
+ * the field unset so it falls back to its `themeDefault` exactly as an
38
+ * un-bound field does. A malformed binding must never paint blank.
39
+ */
40
+ export function resolveThemeTokenRef(value, theme) {
41
+ if (!isThemeTokenRef(value)) return undefined;
42
+ const path = value.slice(PREFIX.length);
43
+ // The allowlist is the contract's, so what the Studio may WRITE and what a
44
+ // host will RESOLVE cannot drift apart.
45
+ if (!CONTRACT.themeColorTokens.some((token) => token.path === path)) {
46
+ return undefined;
47
+ }
48
+ const resolved = path
49
+ .split(".")
50
+ .reduce((o, k) => (o == null ? undefined : o[k]), theme);
51
+ return typeof resolved === "string" && resolved.trim() !== ""
52
+ ? resolved
53
+ : undefined;
54
+ }
55
+
56
+ function isPlainObject(value) {
57
+ return value !== null && typeof value === "object" && !Array.isArray(value);
58
+ }
59
+
60
+ // The same guard the neighbouring style validators apply: these props are
61
+ // author-stored JSON, and assigning such a key while rebuilding an object would
62
+ // silently reseat its prototype instead of carrying the value.
63
+ const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
64
+
65
+ /**
66
+ * Walk a declared schema alongside a value and replace every bound `color`
67
+ * leaf with the colour its token currently resolves to.
68
+ *
69
+ * Schema-GUIDED rather than a blind string sweep, because top-level props hold
70
+ * arbitrary author content — body copy, rich text, a stored link — where a
71
+ * `theme:`-prefixed string is data and must be left verbatim. Only a field the
72
+ * widget declared as a colour is eligible here. (`props.style` is different and
73
+ * is handled by `resolveStyleBag` below.)
74
+ *
75
+ * Returns the SAME reference when nothing is bound, so an app that uses no
76
+ * bindings takes no extra render work and behaves exactly as it did before.
77
+ */
78
+ function resolveAgainstSchema(schema, value, theme) {
79
+ if (!isPlainObject(schema) || !isPlainObject(value)) return value;
80
+ let changed = false;
81
+ const out = {};
82
+ for (const [key, raw] of Object.entries(value)) {
83
+ if (UNSAFE_KEYS.has(key)) continue;
84
+ const def = schema[key];
85
+ out[key] = raw;
86
+ if (!isPlainObject(def)) continue;
87
+
88
+ if (def.type === "color") {
89
+ if (isThemeTokenRef(raw)) {
90
+ // An unresolvable binding is dropped rather than painted: the field goes
91
+ // back to unset, which is what already renders its `themeDefault`.
92
+ out[key] = resolveThemeTokenRef(raw, theme);
93
+ changed = true;
94
+ }
95
+ continue;
96
+ }
97
+ if (def.type === "object" && isPlainObject(def.properties)) {
98
+ const next = resolveAgainstSchema(def.properties, raw, theme);
99
+ if (next !== raw) {
100
+ out[key] = next;
101
+ changed = true;
102
+ }
103
+ continue;
104
+ }
105
+ if (def.type === "array" && isPlainObject(def.items) && Array.isArray(raw)) {
106
+ let itemChanged = false;
107
+ const items = raw.map((item) => {
108
+ const next =
109
+ def.items.type === "object" && isPlainObject(def.items.properties)
110
+ ? resolveAgainstSchema(def.items.properties, item, theme)
111
+ : item;
112
+ if (next !== item) itemChanged = true;
113
+ return next;
114
+ });
115
+ if (itemChanged) {
116
+ out[key] = items;
117
+ changed = true;
118
+ }
119
+ }
120
+ }
121
+ return changed ? out : value;
122
+ }
123
+
124
+ // A style bag is resolved WITHOUT a schema, and deliberately so. Both hosts
125
+ // thread `styleSchema` only for marketplace / AI-agent widgets; a BUILT-IN
126
+ // passes `undefined` on web and `null` on native (it keeps its defaults in
127
+ // code). Requiring the schema would therefore have left every built-in's colour
128
+ // field bindable in the Studio but unresolved at render — the author picks
129
+ // "Primary" and the widget is handed the literal string. Resolving the bag
130
+ // directly is what makes a built-in and a generated widget behave the same, on
131
+ // both hosts, with nothing new baked into the export.
132
+ //
133
+ // Safe without a schema because `props.style` is by construction a bag of style
134
+ // VALUES — colours, sizes, enums — so an allowlisted `theme:` string in it can
135
+ // only ever have been a binding. The depth cap keeps the walk bounded for the
136
+ // structured style values (a gradient's stops, per-corner radii).
137
+ const MAX_STYLE_DEPTH = 3;
138
+
139
+ function resolveStyleBag(value, theme, depth) {
140
+ if (typeof value === "string") {
141
+ if (!isThemeTokenRef(value)) return value;
142
+ // Unresolvable drops to unset, which is what renders `themeDefault`.
143
+ return resolveThemeTokenRef(value, theme);
144
+ }
145
+ if (depth >= MAX_STYLE_DEPTH) return value;
146
+ if (Array.isArray(value)) {
147
+ let changed = false;
148
+ const items = value.map((item) => {
149
+ const next = resolveStyleBag(item, theme, depth + 1);
150
+ if (next !== item) changed = true;
151
+ return next;
152
+ });
153
+ return changed ? items : value;
154
+ }
155
+ if (isPlainObject(value)) {
156
+ let changed = false;
157
+ const out = {};
158
+ for (const [key, item] of Object.entries(value)) {
159
+ if (UNSAFE_KEYS.has(key)) continue;
160
+ const next = resolveStyleBag(item, theme, depth + 1);
161
+ out[key] = next;
162
+ if (next !== item) changed = true;
163
+ }
164
+ return changed ? out : value;
165
+ }
166
+ return value;
167
+ }
168
+
169
+ /**
170
+ * The host render-boundary call: resolve every theme-token colour binding on a
171
+ * widget's props, both the top-level `propertySchema` fields and the style
172
+ * values under `props.style`.
173
+ *
174
+ * Called by BOTH hosts in the same place, immediately after
175
+ * `applyThemeComponentStyle` — a binding is the author's own per-instance
176
+ * value, so it sits at the top of the style precedence and is resolved once the
177
+ * lower layers have been folded in.
178
+ *
179
+ * @param {object} props — the widget's props (post-`applyThemeComponentStyle`).
180
+ * @param {object} theme — the resolved widget theme (`workspace.theme`).
181
+ * @param {object} [schemas] — `{ propertySchema }` as declared by the widget's
182
+ * manifest, used for the top-level props only. `props.style` needs no schema
183
+ * (see above), so a built-in resolves exactly like a generated widget.
184
+ * @returns {object} props, with every bound colour resolved.
185
+ */
186
+ export function resolveThemeTokenRefs(props, theme, schemas) {
187
+ if (!isPlainObject(props)) return props;
188
+ const { propertySchema } = schemas || {};
189
+ let out = resolveAgainstSchema(propertySchema, props, theme);
190
+ const style = out.style;
191
+ if (isPlainObject(style)) {
192
+ const nextStyle = resolveStyleBag(style, theme, 1);
193
+ if (nextStyle !== style) {
194
+ out = { ...out, style: nextStyle };
195
+ }
196
+ }
197
+ return out;
198
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.138.0",
3
+ "version": "0.139.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",
@@ -49,7 +49,7 @@
49
49
  ],
50
50
  "scripts": {
51
51
  "build": "node scripts/build.js",
52
- "test": "node --test src/__tests__/contract.test.js src/__tests__/vetted-imports-audit.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-invites.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-page-url.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-flex-basis-percent.test.js src/__tests__/linter-hardcoded-design.test.js src/__tests__/linter-measured-padding.test.js src/__tests__/linter-payment-error.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/flatten-entry.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/corner-radius.test.js src/__tests__/theme-components-parity.test.js src/__tests__/navigation-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/interaction-lift.test.js src/__tests__/toast-host.test.js src/__tests__/overlay-tokens.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js src/__tests__/hooks-speech-to-text.test.js src/__tests__/hooks-camera.test.js src/__tests__/hooks-barcode.test.js src/__tests__/hooks-image-editor.test.js src/__tests__/hooks-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js src/__tests__/linter-html-in-content.test.js src/__tests__/markdown.test.js src/__tests__/markdown-edit.test.js src/__tests__/richtext-tokens.test.js src/__tests__/style-group.test.js"
52
+ "test": "node --test src/__tests__/contract.test.js src/__tests__/theme-token-refs.test.js src/__tests__/vetted-imports-audit.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-invites.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-page-url.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-flex-basis-percent.test.js src/__tests__/linter-hardcoded-design.test.js src/__tests__/linter-measured-padding.test.js src/__tests__/linter-payment-error.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/flatten-entry.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/corner-radius.test.js src/__tests__/theme-components-parity.test.js src/__tests__/navigation-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/interaction-lift.test.js src/__tests__/toast-host.test.js src/__tests__/overlay-tokens.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js src/__tests__/hooks-speech-to-text.test.js src/__tests__/hooks-camera.test.js src/__tests__/hooks-barcode.test.js src/__tests__/hooks-image-editor.test.js src/__tests__/hooks-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js src/__tests__/linter-html-in-content.test.js src/__tests__/markdown.test.js src/__tests__/markdown-edit.test.js src/__tests__/richtext-tokens.test.js src/__tests__/style-group.test.js"
53
53
  },
54
54
  "engines": {
55
55
  "node": ">=18"