@colixsystems/widget-sdk 0.117.0 → 0.119.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.
@@ -245,6 +245,51 @@ function normaliseWidgetStyles(raw) {
245
245
  return out;
246
246
  }
247
247
 
248
+ // Defaults are static manifest config; clone the structured ones so a widget
249
+ // mutating props.style can never corrupt the shared manifest default. Mirrors
250
+ // cloneDefault in property-schema.js.
251
+ function cloneStyleDefault(value) {
252
+ if (value === null || typeof value !== "object") return value;
253
+ return JSON.parse(JSON.stringify(value));
254
+ }
255
+
256
+ // sc-6750: one styleSchema leaf's declared default, with the SAME nesting
257
+ // semantics resolveLeaf (property-schema.js) gives the property side — an
258
+ // `object` def whose own default is absent still contributes its children's.
259
+ function styleLeafDefault(def) {
260
+ if (!isPlainObject(def)) return undefined;
261
+ if (def.default !== undefined) return cloneStyleDefault(def.default);
262
+ if (def.type !== "object" || !isPlainObject(def.properties)) return undefined;
263
+ const nested = {};
264
+ for (const [child, childDef] of Object.entries(def.properties)) {
265
+ if (!isUsableStyleKey(child)) continue;
266
+ const value = styleLeafDefault(childDef);
267
+ if (value !== undefined) nested[child] = value;
268
+ }
269
+ return Object.keys(nested).length > 0 ? nested : undefined;
270
+ }
271
+
272
+ /**
273
+ * sc-6750 — the widget author's OWN declared style baseline: each
274
+ * `styleSchema` leaf's `default`.
275
+ *
276
+ * The weakest layer by design, seeded beneath every theme layer: a widget's
277
+ * declared default must never out-rank the workspace theme, or every already-
278
+ * published widget would stop following the theme it follows today.
279
+ *
280
+ * @returns {Record<string, unknown>} field → declared default; `{}` when none.
281
+ */
282
+ function styleSchemaDefaults(styleSchema) {
283
+ if (!isPlainObject(styleSchema)) return {};
284
+ const out = {};
285
+ for (const [field, def] of Object.entries(styleSchema)) {
286
+ if (!isUsableStyleKey(field)) continue;
287
+ const value = styleLeafDefault(def);
288
+ if (value !== undefined) out[field] = value;
289
+ }
290
+ return out;
291
+ }
292
+
248
293
  /**
249
294
  * The per-component style fields that apply to one widget, keyed by the
250
295
  * `styleSchema` field name the widget actually reads. A widget may sit in more
@@ -255,12 +300,19 @@ function normaliseWidgetStyles(raw) {
255
300
  * before a value becomes a widget's style, and a host that folded the theme in
256
301
  * without normalising must not be able to hand a widget malformed input.
257
302
  *
258
- * @returns {Record<string, string|number>|null} null when nothing applies.
303
+ * Layered weakest-first: the widget's own `styleSchema` defaults, then the
304
+ * component scopes, then `widgetStyles[manifestId]`. The caller spreads the
305
+ * per-instance `props.style` last, so the full order is
306
+ * styleSchema default -> palette/scopes -> widgetStyles -> props.style.
307
+ *
308
+ * @returns {Record<string, unknown>|null} null when nothing applies.
259
309
  */
260
310
  function componentStyleFor(manifestId, components, styleSchema, widgetStyles) {
261
311
  if (typeof manifestId !== "string") return null;
262
312
  const validated = normaliseThemeComponents(components);
263
- const out = {};
313
+ // sc-6750: the author's declared defaults are the WEAKEST layer, so they go
314
+ // in first and every theme layer below overwrites them.
315
+ const out = styleSchemaDefaults(styleSchema);
264
316
  for (const [scope, definition] of Object.entries(CONTRACT.themeComponents)) {
265
317
  const tokens = validated[scope];
266
318
  if (!tokens) continue;
@@ -305,20 +357,28 @@ function componentStyleFor(manifestId, components, styleSchema, widgetStyles) {
305
357
  }
306
358
 
307
359
  /**
308
- * Fold the theme's per-component tokens into a widget's props as `style`
309
- * DEFAULTS. The author's per-instance REQ-THEME-13 values are spread last and
310
- * therefore always win the theme token is the app-wide baseline, the
311
- * Properties Panel is the final word.
360
+ * Fold the widget's declared `styleSchema` defaults and the theme's
361
+ * per-component tokens into a widget's props as `style` DEFAULTS. The author's
362
+ * per-instance REQ-THEME-13 values are spread last and therefore always win
363
+ * the theme token is the app-wide baseline, the Properties Panel the final word.
364
+ *
365
+ * sc-6750: a `styleSchema` leaf's own `default` is resolved here too, as the
366
+ * BOTTOM layer. It is the widget author's baseline, so a workspace theme still
367
+ * out-ranks it and a field the author never defaulted follows the theme exactly
368
+ * as before — precedence: styleSchema default -> palette/`components.<scope>`
369
+ * -> `widgetStyles[manifestId]` -> per-instance `props.style`.
312
370
  *
313
- * Returns the SAME `props` reference when the theme sets nothing for this
314
- * widget, so an unthemed app takes no extra render work and behaves exactly as
315
- * it did before REQ-THEME-15.
371
+ * Returns the SAME `props` reference when neither the theme nor the schema sets
372
+ * anything for this widget, so an unthemed app with no declared style defaults
373
+ * takes no extra render work and behaves exactly as it did before REQ-THEME-15.
316
374
  *
317
375
  * @param {string} manifestId — the widget's canonical manifest id.
318
376
  * @param {object} theme — the resolved widget theme (`workspace.theme`); its
319
377
  * `components` slice is read.
320
378
  * @param {object} props — the widget's resolved props (post-`resolveProps`).
321
- * @returns {object} props, with `style` folded when the theme applies.
379
+ * @param {object} [styleSchema] the widget's declared style fields; their
380
+ * `default`s form the bottom style layer.
381
+ * @returns {object} props, with `style` folded when anything applies.
322
382
  */
323
383
  function applyThemeComponentStyle(manifestId, theme, props, styleSchema) {
324
384
  const themed = componentStyleFor(
@@ -237,6 +237,51 @@ export function normaliseWidgetStyles(raw) {
237
237
  return out;
238
238
  }
239
239
 
240
+ // Defaults are static manifest config; clone the structured ones so a widget
241
+ // mutating props.style can never corrupt the shared manifest default. Mirrors
242
+ // cloneDefault in property-schema.js.
243
+ function cloneStyleDefault(value) {
244
+ if (value === null || typeof value !== "object") return value;
245
+ return JSON.parse(JSON.stringify(value));
246
+ }
247
+
248
+ // sc-6750: one styleSchema leaf's declared default, with the SAME nesting
249
+ // semantics resolveLeaf (property-schema.js) gives the property side — an
250
+ // `object` def whose own default is absent still contributes its children's.
251
+ function styleLeafDefault(def) {
252
+ if (!isPlainObject(def)) return undefined;
253
+ if (def.default !== undefined) return cloneStyleDefault(def.default);
254
+ if (def.type !== "object" || !isPlainObject(def.properties)) return undefined;
255
+ const nested = {};
256
+ for (const [child, childDef] of Object.entries(def.properties)) {
257
+ if (!isUsableStyleKey(child)) continue;
258
+ const value = styleLeafDefault(childDef);
259
+ if (value !== undefined) nested[child] = value;
260
+ }
261
+ return Object.keys(nested).length > 0 ? nested : undefined;
262
+ }
263
+
264
+ /**
265
+ * sc-6750 — the widget author's OWN declared style baseline: each
266
+ * `styleSchema` leaf's `default`.
267
+ *
268
+ * The weakest layer by design, seeded beneath every theme layer: a widget's
269
+ * declared default must never out-rank the workspace theme, or every already-
270
+ * published widget would stop following the theme it follows today.
271
+ *
272
+ * @returns {Record<string, unknown>} field → declared default; `{}` when none.
273
+ */
274
+ function styleSchemaDefaults(styleSchema) {
275
+ if (!isPlainObject(styleSchema)) return {};
276
+ const out = {};
277
+ for (const [field, def] of Object.entries(styleSchema)) {
278
+ if (!isUsableStyleKey(field)) continue;
279
+ const value = styleLeafDefault(def);
280
+ if (value !== undefined) out[field] = value;
281
+ }
282
+ return out;
283
+ }
284
+
240
285
  /**
241
286
  * The per-component style fields that apply to one widget, keyed by the
242
287
  * `styleSchema` field name the widget actually reads. A widget may sit in more
@@ -247,12 +292,19 @@ export function normaliseWidgetStyles(raw) {
247
292
  * before a value becomes a widget's style, and a host that folded the theme in
248
293
  * without normalising must not be able to hand a widget malformed input.
249
294
  *
250
- * @returns {Record<string, string|number>|null} null when nothing applies.
295
+ * Layered weakest-first: the widget's own `styleSchema` defaults, then the
296
+ * component scopes, then `widgetStyles[manifestId]`. The caller spreads the
297
+ * per-instance `props.style` last, so the full order is
298
+ * styleSchema default -> palette/scopes -> widgetStyles -> props.style.
299
+ *
300
+ * @returns {Record<string, unknown>|null} null when nothing applies.
251
301
  */
252
302
  function componentStyleFor(manifestId, components, styleSchema, widgetStyles) {
253
303
  if (typeof manifestId !== "string") return null;
254
304
  const validated = normaliseThemeComponents(components);
255
- const out = {};
305
+ // sc-6750: the author's declared defaults are the WEAKEST layer, so they go
306
+ // in first and every theme layer below overwrites them.
307
+ const out = styleSchemaDefaults(styleSchema);
256
308
  for (const [scope, definition] of Object.entries(CONTRACT.themeComponents)) {
257
309
  const tokens = validated[scope];
258
310
  if (!tokens) continue;
@@ -297,20 +349,28 @@ function componentStyleFor(manifestId, components, styleSchema, widgetStyles) {
297
349
  }
298
350
 
299
351
  /**
300
- * Fold the theme's per-component tokens into a widget's props as `style`
301
- * DEFAULTS. The author's per-instance REQ-THEME-13 values are spread last and
302
- * therefore always win the theme token is the app-wide baseline, the
303
- * Properties Panel is the final word.
352
+ * Fold the widget's declared `styleSchema` defaults and the theme's
353
+ * per-component tokens into a widget's props as `style` DEFAULTS. The author's
354
+ * per-instance REQ-THEME-13 values are spread last and therefore always win
355
+ * the theme token is the app-wide baseline, the Properties Panel the final word.
356
+ *
357
+ * sc-6750: a `styleSchema` leaf's own `default` is resolved here too, as the
358
+ * BOTTOM layer. It is the widget author's baseline, so a workspace theme still
359
+ * out-ranks it and a field the author never defaulted follows the theme exactly
360
+ * as before — precedence: styleSchema default -> palette/`components.<scope>`
361
+ * -> `widgetStyles[manifestId]` -> per-instance `props.style`.
304
362
  *
305
- * Returns the SAME `props` reference when the theme sets nothing for this
306
- * widget, so an unthemed app takes no extra render work and behaves exactly as
307
- * it did before REQ-THEME-15.
363
+ * Returns the SAME `props` reference when neither the theme nor the schema sets
364
+ * anything for this widget, so an unthemed app with no declared style defaults
365
+ * takes no extra render work and behaves exactly as it did before REQ-THEME-15.
308
366
  *
309
367
  * @param {string} manifestId — the widget's canonical manifest id.
310
368
  * @param {object} theme — the resolved widget theme (`workspace.theme`); its
311
369
  * `components` slice is read.
312
370
  * @param {object} props — the widget's resolved props (post-`resolveProps`).
313
- * @returns {object} props, with `style` folded when the theme applies.
371
+ * @param {object} [styleSchema] the widget's declared style fields; their
372
+ * `default`s form the bottom style layer.
373
+ * @returns {object} props, with `style` folded when anything applies.
314
374
  */
315
375
  export function applyThemeComponentStyle(manifestId, theme, props, styleSchema) {
316
376
  const themed = componentStyleFor(
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.117.0",
3
+ "version": "0.119.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
+ "homepage": "https://github.com/Colix-AB/AppStudio",
5
6
  "type": "module",
6
7
  "main": "./dist/index.js",
7
8
  "module": "./dist/index.js",
@@ -48,7 +49,7 @@
48
49
  ],
49
50
  "scripts": {
50
51
  "build": "node scripts/build.js",
51
- "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-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__/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-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.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-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-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js"
52
53
  },
53
54
  "engines": {
54
55
  "node": ">=18"
@@ -83,5 +84,8 @@
83
84
  "sdk",
84
85
  "low-code"
85
86
  ],
86
- "license": "MIT"
87
+ "license": "MIT",
88
+ "devDependencies": {
89
+ "@babel/parser": "^7.29.7"
90
+ }
87
91
  }