@colixsystems/widget-sdk 0.66.0 → 0.67.1
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 +12 -2
- package/dist/contract.cjs +130 -1
- package/dist/contract.js +130 -1
- package/dist/hooks.js +35 -0
- package/dist/host.d.ts +23 -0
- package/dist/host.js +10 -0
- package/dist/theme-components.cjs +134 -0
- package/dist/theme-components.js +124 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
17
17
|
|
|
18
18
|
| Group | Hook (signature) | Returns | Reads / scope |
|
|
19
19
|
| ----- | ---------------- | ------- | ------------- |
|
|
20
|
-
| **CORE** | `useTheme()` | `{ colors, spacing, radii, typography }` | `ctx.workspace.theme` — no scope |
|
|
20
|
+
| **CORE** | `useTheme()` | `{ colors, spacing, radii, typography, components }` | `ctx.workspace.theme` — no scope. `components` is HOST-OWNED (the theme's per-component style tokens); the host has already folded it into your `props.style`, so read `useWidgetStyle()` and ignore this slice. |
|
|
21
21
|
| **CORE** | `useWidgetStyle()` | `{ [styleField]: value }` | `ctx.props.style` — no scope. The author-set per-widget style values declared in `manifest.styleSchema`; apply each onto whatever element you choose. |
|
|
22
22
|
| **CORE** | `useUser()` | `{ id, email, displayName, roles, groupIds }` | `ctx.user` (host-built context, **camelCase** — not a wire payload; `id` null when anonymous) — no scope |
|
|
23
23
|
| **CORE** | `useNavigation()` | `{ goTo, goBack, push, replace, back, currentRoute }` | `ctx.navigation` — no scope (external URLs use the `Linking` primitive) |
|
|
@@ -53,7 +53,17 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
53
53
|
|
|
54
54
|
## Status
|
|
55
55
|
|
|
56
|
-
`v0.
|
|
56
|
+
`v0.67.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**.
|
|
57
|
+
|
|
58
|
+
### What's new in 0.67.0
|
|
59
|
+
|
|
60
|
+
**The theme can restyle ONE component type — buttons, cards or text — without moving the global palette (sc-1497).** A workspace theme may now carry `themeConfig.components` (`{ button, card, text }`), and the host resolves each scope onto the `styleSchema` fields the target widgets already read. **Nothing changes for a widget author:** you keep reading `props.style` / `useWidgetStyle()`, and an author's per-instance value still wins over a theme token — the theme is the app-wide default underneath it.
|
|
61
|
+
|
|
62
|
+
- **`useTheme()` gains a `components` slice.** It is HOST-OWNED plumbing, not an author API: by the time your component renders, the host has already folded the matching tokens into `props.style`. Do not read `theme.components` and do not re-apply it — you would double-apply the theme and defeat the author's own styling.
|
|
63
|
+
- **New host-only exports on `@colixsystems/widget-sdk/host`:** `normaliseThemeComponents(raw)` and `applyThemeComponentStyle(manifestId, theme, props)`. These are the platform-host surface (the web Player / Studio canvas and the exported Expo app), never the author API — one implementation, so the two hosts cannot diverge.
|
|
64
|
+
- **`CONTRACT.themeComponents` / `CONTRACT.themeComponentShadows`** publish the vocabulary: each scope's tokens, their value types and ranges, and the widget → style-field bindings. `themeTokens.components` defaults to `{}`.
|
|
65
|
+
|
|
66
|
+
`CONTRACT.version` → `1.45.0`. Additive; no existing export changed signature, and an unthemed app renders identically.
|
|
57
67
|
|
|
58
68
|
### What's new in 0.66.0
|
|
59
69
|
|
package/dist/contract.cjs
CHANGED
|
@@ -31,6 +31,111 @@ const DEFAULT_THEME_TOKENS = Object.freeze({
|
|
|
31
31
|
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
32
32
|
sizes: Object.freeze({ xs: 12, sm: 14, md: 16, lg: 20, xl: 24, xxl: 32 }),
|
|
33
33
|
}),
|
|
34
|
+
// REQ-THEME-15: the tenant's per-component style tokens, folded in by the
|
|
35
|
+
// host so ONE channel carries them to the Player and the export. Empty by
|
|
36
|
+
// default — an unconfigured theme resolves to no component overrides.
|
|
37
|
+
components: Object.freeze({}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// REQ-THEME-15 (sc-1497) — per-component style tokens. The global palette is a
|
|
41
|
+
// single lever: remapping `primaryColor` to recolour "the buttons" recolours
|
|
42
|
+
// every element that shares it. A theme may therefore also carry
|
|
43
|
+
// `themeConfig.components` — `{ <scope>: { <token>: <value> } }` — that restyles
|
|
44
|
+
// ONE component type app-wide.
|
|
45
|
+
//
|
|
46
|
+
// This is NOT a second styling vocabulary. Each scope's tokens bind to the
|
|
47
|
+
// REQ-THEME-13 per-instance `styleSchema` fields the target widgets ALREADY
|
|
48
|
+
// read, so a theme-level token needs no widget change: the host resolves the
|
|
49
|
+
// scope's tokens into the widget's `props.style` DEFAULTS and an author's
|
|
50
|
+
// per-instance value still wins.
|
|
51
|
+
//
|
|
52
|
+
// `tokens` declares each token's value type once per scope (what the Mason
|
|
53
|
+
// `set_theme` coercion and the host boundary validate against); `targets` binds
|
|
54
|
+
// token → the style field each widget reads, so one scope can drive differently
|
|
55
|
+
// named fields on different widgets (a Form's submit button is `submitBackground`,
|
|
56
|
+
// a Button's is `background`). The keys of `targets` ARE the widgets in scope.
|
|
57
|
+
//
|
|
58
|
+
// Single source for four consumers — the build runner's set_theme coercion, the
|
|
59
|
+
// shared host resolver, the planner prompt, and the SDK docs — so the vocabulary
|
|
60
|
+
// cannot drift between what Mason may emit and what a host actually applies.
|
|
61
|
+
const THEME_COMPONENT_SHADOWS = Object.freeze(["none", "sm", "md", "lg"]);
|
|
62
|
+
|
|
63
|
+
// The card-surface field names shared by every widget that paints its own card
|
|
64
|
+
// (frontend/src/components/widgets/_shared/cardStyle.js CARD_STYLE_SCHEMA).
|
|
65
|
+
const CARD_SURFACE_FIELDS = Object.freeze({
|
|
66
|
+
background: "cardBackground",
|
|
67
|
+
borderColor: "cardBorderColor",
|
|
68
|
+
radius: "cardRadius",
|
|
69
|
+
padding: "cardPadding",
|
|
70
|
+
shadow: "shadow",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// A form widget's submit button — the `button` scope reaches it through the
|
|
74
|
+
// form's own submit* fields, so "make the buttons coral" does not skip forms.
|
|
75
|
+
const FORM_SUBMIT_FIELDS = Object.freeze({
|
|
76
|
+
background: "submitBackground",
|
|
77
|
+
textColor: "submitTextColor",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const THEME_COMPONENTS = Object.freeze({
|
|
81
|
+
button: Object.freeze({
|
|
82
|
+
label: "Buttons",
|
|
83
|
+
tokens: Object.freeze({
|
|
84
|
+
background: Object.freeze({ type: "color", uiDefault: "colors.primary" }),
|
|
85
|
+
textColor: Object.freeze({ type: "color", uiDefault: "colors.onPrimary" }),
|
|
86
|
+
borderColor: Object.freeze({ type: "color", uiDefault: "colors.border" }),
|
|
87
|
+
radius: Object.freeze({ type: "size", min: 0, max: 48, uiDefault: "radii.sm" }),
|
|
88
|
+
fontSize: Object.freeze({ type: "size", min: 8, max: 96, uiDefault: "typography.sizes.sm" }),
|
|
89
|
+
shadow: Object.freeze({ type: "shadow" }),
|
|
90
|
+
}),
|
|
91
|
+
targets: Object.freeze({
|
|
92
|
+
"appstudio.button": Object.freeze({
|
|
93
|
+
background: "background",
|
|
94
|
+
textColor: "textColor",
|
|
95
|
+
borderColor: "borderColor",
|
|
96
|
+
radius: "radius",
|
|
97
|
+
fontSize: "fontSize",
|
|
98
|
+
shadow: "shadow",
|
|
99
|
+
}),
|
|
100
|
+
"appstudio.form-input": FORM_SUBMIT_FIELDS,
|
|
101
|
+
"appstudio.form-builder": FORM_SUBMIT_FIELDS,
|
|
102
|
+
}),
|
|
103
|
+
}),
|
|
104
|
+
card: Object.freeze({
|
|
105
|
+
label: "Cards",
|
|
106
|
+
tokens: Object.freeze({
|
|
107
|
+
background: Object.freeze({ type: "color", uiDefault: "colors.surface" }),
|
|
108
|
+
borderColor: Object.freeze({ type: "color", uiDefault: "colors.border" }),
|
|
109
|
+
radius: Object.freeze({ type: "size", min: 0, max: 48, uiDefault: "radii.md" }),
|
|
110
|
+
padding: Object.freeze({ type: "size", min: 0, max: 64, uiDefault: "spacing.md" }),
|
|
111
|
+
shadow: Object.freeze({ type: "shadow" }),
|
|
112
|
+
}),
|
|
113
|
+
targets: Object.freeze({
|
|
114
|
+
"appstudio.user": CARD_SURFACE_FIELDS,
|
|
115
|
+
"appstudio.data-list": CARD_SURFACE_FIELDS,
|
|
116
|
+
"appstudio.gallery": CARD_SURFACE_FIELDS,
|
|
117
|
+
"appstudio.files": CARD_SURFACE_FIELDS,
|
|
118
|
+
"appstudio.newsfeed": CARD_SURFACE_FIELDS,
|
|
119
|
+
"appstudio.notifications": CARD_SURFACE_FIELDS,
|
|
120
|
+
"appstudio.form-input": CARD_SURFACE_FIELDS,
|
|
121
|
+
"appstudio.form-builder": CARD_SURFACE_FIELDS,
|
|
122
|
+
}),
|
|
123
|
+
}),
|
|
124
|
+
text: Object.freeze({
|
|
125
|
+
label: "Text",
|
|
126
|
+
tokens: Object.freeze({
|
|
127
|
+
color: Object.freeze({ type: "color", uiDefault: "colors.onSurface" }),
|
|
128
|
+
fontSize: Object.freeze({ type: "size", min: 8, max: 96, uiDefault: "typography.sizes.md" }),
|
|
129
|
+
}),
|
|
130
|
+
targets: Object.freeze({
|
|
131
|
+
"appstudio.text": Object.freeze({ color: "color", fontSize: "fontSize" }),
|
|
132
|
+
"appstudio.label": Object.freeze({ color: "color", fontSize: "fontSize" }),
|
|
133
|
+
"appstudio.data-value": Object.freeze({
|
|
134
|
+
color: "color",
|
|
135
|
+
fontSize: "fontSize",
|
|
136
|
+
}),
|
|
137
|
+
}),
|
|
138
|
+
}),
|
|
34
139
|
});
|
|
35
140
|
|
|
36
141
|
const HOOKS = [
|
|
@@ -43,6 +148,10 @@ const HOOKS = [
|
|
|
43
148
|
spacing: "{ xs, sm, md, lg, xl }",
|
|
44
149
|
radii: "{ sm, md, lg, pill }",
|
|
45
150
|
typography: "{ fontFamily, sizes: { xs, sm, md, lg, xl, xxl } }",
|
|
151
|
+
components:
|
|
152
|
+
"{ [scope]: { [token]: value } } — REQ-THEME-15 per-component theme " +
|
|
153
|
+
"tokens. HOST-OWNED: the host already folds them into your " +
|
|
154
|
+
"props.style, so read props.style / useWidgetStyle() instead.",
|
|
46
155
|
},
|
|
47
156
|
requiredContextSlice: ["workspace.theme"],
|
|
48
157
|
scopes: null,
|
|
@@ -1960,7 +2069,25 @@ const CONTRACT = deepFreeze({
|
|
|
1960
2069
|
// matching, so a comment quoting the bad path is not a finding, and a
|
|
1961
2070
|
// legitimate third-party relative path can opt out with an
|
|
1962
2071
|
// `appstudio-lint-ignore no-host-api-url` comment. Additive.
|
|
1963
|
-
|
|
2072
|
+
//
|
|
2073
|
+
// 1.45.0: additive (REQ-THEME-15, sc-1497) — per-component style tokens. A
|
|
2074
|
+
// workspace theme may carry `themeConfig.components` (`{ button, card, text }`)
|
|
2075
|
+
// that restyles ONE component type app-wide instead of remapping the shared
|
|
2076
|
+
// global palette. New `themeComponents` (the scope -> token -> target-field
|
|
2077
|
+
// vocabulary, with each token's value type + range) and
|
|
2078
|
+
// `themeComponentShadows` (the closed shadow enum) publish that vocabulary as
|
|
2079
|
+
// the SINGLE source read by the Mason build runner's set_theme coercion, the
|
|
2080
|
+
// shared host resolver, and the planner prompt. `themeTokens` gains a
|
|
2081
|
+
// `components` namespace (default `{}`) because the resolved theme is the
|
|
2082
|
+
// channel that carries the tokens to both hosts, so `useTheme()` returnShape
|
|
2083
|
+
// gains `components` — HOST-OWNED: the host folds the matching tokens into a
|
|
2084
|
+
// widget's `props.style` before render, so an author still reads
|
|
2085
|
+
// `props.style` / `useWidgetStyle()` and must NOT re-apply this slice. The
|
|
2086
|
+
// resolvers themselves ship on the host-only entry
|
|
2087
|
+
// (`@colixsystems/widget-sdk/host`), never the author surface. Additive: no
|
|
2088
|
+
// export changed signature and a theme with no `components` key resolves to
|
|
2089
|
+
// an empty override, rendering identically to before.
|
|
2090
|
+
version: "1.45.0",
|
|
1964
2091
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
1965
2092
|
hooks: HOOKS,
|
|
1966
2093
|
primitives: PRIMITIVES,
|
|
@@ -1971,6 +2098,8 @@ const CONTRACT = deepFreeze({
|
|
|
1971
2098
|
actionScriptGlobals: ACTION_SCRIPT_GLOBALS,
|
|
1972
2099
|
actionScriptMaxBytes: ACTION_SCRIPT_MAX_BYTES,
|
|
1973
2100
|
themeTokens: DEFAULT_THEME_TOKENS,
|
|
2101
|
+
themeComponents: THEME_COMPONENTS,
|
|
2102
|
+
themeComponentShadows: THEME_COMPONENT_SHADOWS,
|
|
1974
2103
|
widgetContextShape: WIDGET_CONTEXT_SHAPE,
|
|
1975
2104
|
bundleExportContract: BUNDLE_EXPORT_CONTRACT,
|
|
1976
2105
|
bannedApis: BANNED_APIS,
|
package/dist/contract.js
CHANGED
|
@@ -31,6 +31,111 @@ const DEFAULT_THEME_TOKENS = Object.freeze({
|
|
|
31
31
|
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
32
32
|
sizes: Object.freeze({ xs: 12, sm: 14, md: 16, lg: 20, xl: 24, xxl: 32 }),
|
|
33
33
|
}),
|
|
34
|
+
// REQ-THEME-15: the tenant's per-component style tokens, folded in by the
|
|
35
|
+
// host so ONE channel carries them to the Player and the export. Empty by
|
|
36
|
+
// default — an unconfigured theme resolves to no component overrides.
|
|
37
|
+
components: Object.freeze({}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// REQ-THEME-15 (sc-1497) — per-component style tokens. The global palette is a
|
|
41
|
+
// single lever: remapping `primaryColor` to recolour "the buttons" recolours
|
|
42
|
+
// every element that shares it. A theme may therefore also carry
|
|
43
|
+
// `themeConfig.components` — `{ <scope>: { <token>: <value> } }` — that restyles
|
|
44
|
+
// ONE component type app-wide.
|
|
45
|
+
//
|
|
46
|
+
// This is NOT a second styling vocabulary. Each scope's tokens bind to the
|
|
47
|
+
// REQ-THEME-13 per-instance `styleSchema` fields the target widgets ALREADY
|
|
48
|
+
// read, so a theme-level token needs no widget change: the host resolves the
|
|
49
|
+
// scope's tokens into the widget's `props.style` DEFAULTS and an author's
|
|
50
|
+
// per-instance value still wins.
|
|
51
|
+
//
|
|
52
|
+
// `tokens` declares each token's value type once per scope (what the Mason
|
|
53
|
+
// `set_theme` coercion and the host boundary validate against); `targets` binds
|
|
54
|
+
// token → the style field each widget reads, so one scope can drive differently
|
|
55
|
+
// named fields on different widgets (a Form's submit button is `submitBackground`,
|
|
56
|
+
// a Button's is `background`). The keys of `targets` ARE the widgets in scope.
|
|
57
|
+
//
|
|
58
|
+
// Single source for four consumers — the build runner's set_theme coercion, the
|
|
59
|
+
// shared host resolver, the planner prompt, and the SDK docs — so the vocabulary
|
|
60
|
+
// cannot drift between what Mason may emit and what a host actually applies.
|
|
61
|
+
const THEME_COMPONENT_SHADOWS = Object.freeze(["none", "sm", "md", "lg"]);
|
|
62
|
+
|
|
63
|
+
// The card-surface field names shared by every widget that paints its own card
|
|
64
|
+
// (frontend/src/components/widgets/_shared/cardStyle.js CARD_STYLE_SCHEMA).
|
|
65
|
+
const CARD_SURFACE_FIELDS = Object.freeze({
|
|
66
|
+
background: "cardBackground",
|
|
67
|
+
borderColor: "cardBorderColor",
|
|
68
|
+
radius: "cardRadius",
|
|
69
|
+
padding: "cardPadding",
|
|
70
|
+
shadow: "shadow",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// A form widget's submit button — the `button` scope reaches it through the
|
|
74
|
+
// form's own submit* fields, so "make the buttons coral" does not skip forms.
|
|
75
|
+
const FORM_SUBMIT_FIELDS = Object.freeze({
|
|
76
|
+
background: "submitBackground",
|
|
77
|
+
textColor: "submitTextColor",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const THEME_COMPONENTS = Object.freeze({
|
|
81
|
+
button: Object.freeze({
|
|
82
|
+
label: "Buttons",
|
|
83
|
+
tokens: Object.freeze({
|
|
84
|
+
background: Object.freeze({ type: "color", uiDefault: "colors.primary" }),
|
|
85
|
+
textColor: Object.freeze({ type: "color", uiDefault: "colors.onPrimary" }),
|
|
86
|
+
borderColor: Object.freeze({ type: "color", uiDefault: "colors.border" }),
|
|
87
|
+
radius: Object.freeze({ type: "size", min: 0, max: 48, uiDefault: "radii.sm" }),
|
|
88
|
+
fontSize: Object.freeze({ type: "size", min: 8, max: 96, uiDefault: "typography.sizes.sm" }),
|
|
89
|
+
shadow: Object.freeze({ type: "shadow" }),
|
|
90
|
+
}),
|
|
91
|
+
targets: Object.freeze({
|
|
92
|
+
"appstudio.button": Object.freeze({
|
|
93
|
+
background: "background",
|
|
94
|
+
textColor: "textColor",
|
|
95
|
+
borderColor: "borderColor",
|
|
96
|
+
radius: "radius",
|
|
97
|
+
fontSize: "fontSize",
|
|
98
|
+
shadow: "shadow",
|
|
99
|
+
}),
|
|
100
|
+
"appstudio.form-input": FORM_SUBMIT_FIELDS,
|
|
101
|
+
"appstudio.form-builder": FORM_SUBMIT_FIELDS,
|
|
102
|
+
}),
|
|
103
|
+
}),
|
|
104
|
+
card: Object.freeze({
|
|
105
|
+
label: "Cards",
|
|
106
|
+
tokens: Object.freeze({
|
|
107
|
+
background: Object.freeze({ type: "color", uiDefault: "colors.surface" }),
|
|
108
|
+
borderColor: Object.freeze({ type: "color", uiDefault: "colors.border" }),
|
|
109
|
+
radius: Object.freeze({ type: "size", min: 0, max: 48, uiDefault: "radii.md" }),
|
|
110
|
+
padding: Object.freeze({ type: "size", min: 0, max: 64, uiDefault: "spacing.md" }),
|
|
111
|
+
shadow: Object.freeze({ type: "shadow" }),
|
|
112
|
+
}),
|
|
113
|
+
targets: Object.freeze({
|
|
114
|
+
"appstudio.user": CARD_SURFACE_FIELDS,
|
|
115
|
+
"appstudio.data-list": CARD_SURFACE_FIELDS,
|
|
116
|
+
"appstudio.gallery": CARD_SURFACE_FIELDS,
|
|
117
|
+
"appstudio.files": CARD_SURFACE_FIELDS,
|
|
118
|
+
"appstudio.newsfeed": CARD_SURFACE_FIELDS,
|
|
119
|
+
"appstudio.notifications": CARD_SURFACE_FIELDS,
|
|
120
|
+
"appstudio.form-input": CARD_SURFACE_FIELDS,
|
|
121
|
+
"appstudio.form-builder": CARD_SURFACE_FIELDS,
|
|
122
|
+
}),
|
|
123
|
+
}),
|
|
124
|
+
text: Object.freeze({
|
|
125
|
+
label: "Text",
|
|
126
|
+
tokens: Object.freeze({
|
|
127
|
+
color: Object.freeze({ type: "color", uiDefault: "colors.onSurface" }),
|
|
128
|
+
fontSize: Object.freeze({ type: "size", min: 8, max: 96, uiDefault: "typography.sizes.md" }),
|
|
129
|
+
}),
|
|
130
|
+
targets: Object.freeze({
|
|
131
|
+
"appstudio.text": Object.freeze({ color: "color", fontSize: "fontSize" }),
|
|
132
|
+
"appstudio.label": Object.freeze({ color: "color", fontSize: "fontSize" }),
|
|
133
|
+
"appstudio.data-value": Object.freeze({
|
|
134
|
+
color: "color",
|
|
135
|
+
fontSize: "fontSize",
|
|
136
|
+
}),
|
|
137
|
+
}),
|
|
138
|
+
}),
|
|
34
139
|
});
|
|
35
140
|
|
|
36
141
|
const HOOKS = [
|
|
@@ -43,6 +148,10 @@ const HOOKS = [
|
|
|
43
148
|
spacing: "{ xs, sm, md, lg, xl }",
|
|
44
149
|
radii: "{ sm, md, lg, pill }",
|
|
45
150
|
typography: "{ fontFamily, sizes: { xs, sm, md, lg, xl, xxl } }",
|
|
151
|
+
components:
|
|
152
|
+
"{ [scope]: { [token]: value } } — REQ-THEME-15 per-component theme " +
|
|
153
|
+
"tokens. HOST-OWNED: the host already folds them into your " +
|
|
154
|
+
"props.style, so read props.style / useWidgetStyle() instead.",
|
|
46
155
|
},
|
|
47
156
|
requiredContextSlice: ["workspace.theme"],
|
|
48
157
|
scopes: null,
|
|
@@ -1960,7 +2069,25 @@ const CONTRACT = deepFreeze({
|
|
|
1960
2069
|
// matching, so a comment quoting the bad path is not a finding, and a
|
|
1961
2070
|
// legitimate third-party relative path can opt out with an
|
|
1962
2071
|
// `appstudio-lint-ignore no-host-api-url` comment. Additive.
|
|
1963
|
-
|
|
2072
|
+
//
|
|
2073
|
+
// 1.45.0: additive (REQ-THEME-15, sc-1497) — per-component style tokens. A
|
|
2074
|
+
// workspace theme may carry `themeConfig.components` (`{ button, card, text }`)
|
|
2075
|
+
// that restyles ONE component type app-wide instead of remapping the shared
|
|
2076
|
+
// global palette. New `themeComponents` (the scope -> token -> target-field
|
|
2077
|
+
// vocabulary, with each token's value type + range) and
|
|
2078
|
+
// `themeComponentShadows` (the closed shadow enum) publish that vocabulary as
|
|
2079
|
+
// the SINGLE source read by the Mason build runner's set_theme coercion, the
|
|
2080
|
+
// shared host resolver, and the planner prompt. `themeTokens` gains a
|
|
2081
|
+
// `components` namespace (default `{}`) because the resolved theme is the
|
|
2082
|
+
// channel that carries the tokens to both hosts, so `useTheme()` returnShape
|
|
2083
|
+
// gains `components` — HOST-OWNED: the host folds the matching tokens into a
|
|
2084
|
+
// widget's `props.style` before render, so an author still reads
|
|
2085
|
+
// `props.style` / `useWidgetStyle()` and must NOT re-apply this slice. The
|
|
2086
|
+
// resolvers themselves ship on the host-only entry
|
|
2087
|
+
// (`@colixsystems/widget-sdk/host`), never the author surface. Additive: no
|
|
2088
|
+
// export changed signature and a theme with no `components` key resolves to
|
|
2089
|
+
// an empty override, rendering identically to before.
|
|
2090
|
+
version: "1.45.0",
|
|
1964
2091
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
1965
2092
|
hooks: HOOKS,
|
|
1966
2093
|
primitives: PRIMITIVES,
|
|
@@ -1971,6 +2098,8 @@ const CONTRACT = deepFreeze({
|
|
|
1971
2098
|
actionScriptGlobals: ACTION_SCRIPT_GLOBALS,
|
|
1972
2099
|
actionScriptMaxBytes: ACTION_SCRIPT_MAX_BYTES,
|
|
1973
2100
|
themeTokens: DEFAULT_THEME_TOKENS,
|
|
2101
|
+
themeComponents: THEME_COMPONENTS,
|
|
2102
|
+
themeComponentShadows: THEME_COMPONENT_SHADOWS,
|
|
1974
2103
|
widgetContextShape: WIDGET_CONTEXT_SHAPE,
|
|
1975
2104
|
bundleExportContract: BUNDLE_EXPORT_CONTRACT,
|
|
1976
2105
|
bannedApis: BANNED_APIS,
|
package/dist/hooks.js
CHANGED
|
@@ -593,6 +593,12 @@ function toDatastoreError(err) {
|
|
|
593
593
|
});
|
|
594
594
|
}
|
|
595
595
|
|
|
596
|
+
// sc-1579 — consecutive renders with a different serialized query before
|
|
597
|
+
// `useDatastoreQuery` calls the loop out. High enough that no real interaction
|
|
598
|
+
// reaches it (a settled fetch resets the count), low enough to fire instantly
|
|
599
|
+
// on a genuine loop.
|
|
600
|
+
const VOLATILE_QUERY_RENDER_LIMIT = 10;
|
|
601
|
+
|
|
596
602
|
/**
|
|
597
603
|
* Stateful datastore query hook. Returns { data, loading, error, refetch }.
|
|
598
604
|
*
|
|
@@ -682,6 +688,35 @@ export function useDatastoreQuery(table, query) {
|
|
|
682
688
|
return null;
|
|
683
689
|
}
|
|
684
690
|
})();
|
|
691
|
+
|
|
692
|
+
// sc-1579 — a query argument built from a volatile value (`Date.now()`,
|
|
693
|
+
// `new Date()`, `Math.random()`) serialises differently every render, so the
|
|
694
|
+
// effect below re-fetches forever and the widget never settles. A genuine
|
|
695
|
+
// edit (a typed filter) settles within one render because the re-render the
|
|
696
|
+
// fetch triggers reuses the same argument; a volatile one never does. That
|
|
697
|
+
// gap is the signal — no timers, no heuristics. Warned once per instance,
|
|
698
|
+
// and in production too: the Studio canvas and the widget agent's render
|
|
699
|
+
// smoke check both run production builds, which is where this must surface.
|
|
700
|
+
const volatileKeyRef = useRef({ key: queryKey, changes: 0, warned: false });
|
|
701
|
+
const keyWatch = volatileKeyRef.current;
|
|
702
|
+
if (keyWatch.key === queryKey) {
|
|
703
|
+
keyWatch.changes = 0;
|
|
704
|
+
} else {
|
|
705
|
+
keyWatch.key = queryKey;
|
|
706
|
+
keyWatch.changes += 1;
|
|
707
|
+
if (keyWatch.changes >= VOLATILE_QUERY_RENDER_LIMIT && !keyWatch.warned) {
|
|
708
|
+
keyWatch.warned = true;
|
|
709
|
+
// eslint-disable-next-line no-console
|
|
710
|
+
console.warn(
|
|
711
|
+
`useDatastoreQuery(${JSON.stringify(table)}): the query argument changed on ` +
|
|
712
|
+
`${keyWatch.changes} consecutive renders, so this widget is re-fetching in a ` +
|
|
713
|
+
"loop. Something in the query is regenerated every render — usually a date " +
|
|
714
|
+
"or random value. Compute it once outside render (or with useMemo) and pass " +
|
|
715
|
+
"the stable value in.",
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
685
720
|
useEffect(() => {
|
|
686
721
|
doFetch();
|
|
687
722
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
package/dist/host.d.ts
CHANGED
|
@@ -11,3 +11,26 @@ export function resolveProps<T = Record<string, unknown>>(
|
|
|
11
11
|
schema: WidgetPropertySchema,
|
|
12
12
|
props: unknown,
|
|
13
13
|
): T;
|
|
14
|
+
|
|
15
|
+
export type ThemeComponentStyle = Record<string, string | number>;
|
|
16
|
+
export type ThemeComponents = Record<string, ThemeComponentStyle>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* REQ-THEME-15 host helper: validates a raw `themeConfig.components` blob down
|
|
20
|
+
* to `CONTRACT.themeComponents` — unknown scopes/tokens and malformed values are
|
|
21
|
+
* dropped. Called when a host folds the tenant theme into the resolved widget
|
|
22
|
+
* theme, so `theme.components` is always clean.
|
|
23
|
+
*/
|
|
24
|
+
export function normaliseThemeComponents(raw: unknown): ThemeComponents;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* REQ-THEME-15 host render-boundary helper: folds the theme's per-component
|
|
28
|
+
* tokens into a widget's props as `style` DEFAULTS, with the author's
|
|
29
|
+
* per-instance values winning. Returns the same `props` reference when the theme
|
|
30
|
+
* sets nothing for this widget. Applied by the platform hosts, never by authors.
|
|
31
|
+
*/
|
|
32
|
+
export function applyThemeComponentStyle<T = Record<string, unknown>>(
|
|
33
|
+
manifestId: string,
|
|
34
|
+
theme: { components?: ThemeComponents } | null | undefined,
|
|
35
|
+
props: T,
|
|
36
|
+
): T;
|
package/dist/host.js
CHANGED
|
@@ -10,3 +10,13 @@
|
|
|
10
10
|
// not appear in the author import surface or the Developer guide.
|
|
11
11
|
|
|
12
12
|
export { resolveProps } from "./property-schema.js";
|
|
13
|
+
|
|
14
|
+
// REQ-THEME-15: the per-component theme-token resolvers. `normaliseThemeComponents`
|
|
15
|
+
// validates a raw `themeConfig.components` blob when the host folds it into the
|
|
16
|
+
// resolved widget theme; `applyThemeComponentStyle` turns that slice into a
|
|
17
|
+
// widget's `style` defaults at the render boundary. One implementation for both
|
|
18
|
+
// hosts, so the Player and the Expo export cannot diverge.
|
|
19
|
+
export {
|
|
20
|
+
normaliseThemeComponents,
|
|
21
|
+
applyThemeComponentStyle,
|
|
22
|
+
} from "./theme-components.js";
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// CommonJS mirror of theme-components.js — the Mason build runner (CJS)
|
|
2
|
+
// validates a `set_theme` components blob against the SAME vocabulary the hosts
|
|
3
|
+
// apply, so what a planner may persist and what a widget renders cannot diverge.
|
|
4
|
+
//
|
|
5
|
+
// The BODY below is copied VERBATIM from theme-components.js; only the module
|
|
6
|
+
// syntax differs. theme-components-parity.test.js pins both facts — identical
|
|
7
|
+
// bodies AND identical behaviour over a shared case table — so drift fails CI.
|
|
8
|
+
|
|
9
|
+
// REQ-THEME-15 (sc-1497) — the host side of per-component style tokens.
|
|
10
|
+
//
|
|
11
|
+
// Host-integration surface, re-exported from `host.js`: consumed ONLY by the
|
|
12
|
+
// platform hosts that render widgets (the web Player / Studio Canvas via
|
|
13
|
+
// buildHostWidgetContext.js, and the exported Expo app's generated WidgetHost),
|
|
14
|
+
// never by a widget author. A widget keeps reading `props.style` /
|
|
15
|
+
// `useWidgetStyle()` and never learns a theme token was involved.
|
|
16
|
+
//
|
|
17
|
+
// Living here — one implementation both hosts import — is what makes the tokens
|
|
18
|
+
// render identically in the Player and the export (widget-parity skill). The
|
|
19
|
+
// alternative, a copy per host, is the drift this file exists to prevent.
|
|
20
|
+
//
|
|
21
|
+
// `CONTRACT.themeComponents` is the single source of the vocabulary: which
|
|
22
|
+
// scopes exist, each token's value type, and the `styleSchema` field each target
|
|
23
|
+
// widget reads. The Mason build runner validates `set_theme` against the SAME
|
|
24
|
+
// literal, so what a planner may emit and what a host applies cannot diverge.
|
|
25
|
+
|
|
26
|
+
const { CONTRACT } = require("./contract.cjs");
|
|
27
|
+
|
|
28
|
+
// 3-, 6- and 8-digit hex, matching what the build runner persists — the host
|
|
29
|
+
// must never drop a colour the runner already accepted.
|
|
30
|
+
const HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
31
|
+
|
|
32
|
+
function isPlainObject(value) {
|
|
33
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function coerceToken(def, value) {
|
|
37
|
+
if (def.type === "color") {
|
|
38
|
+
if (typeof value !== "string") return undefined;
|
|
39
|
+
const trimmed = value.trim();
|
|
40
|
+
return HEX_COLOR.test(trimmed) ? trimmed : undefined;
|
|
41
|
+
}
|
|
42
|
+
if (def.type === "size") {
|
|
43
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
44
|
+
if (!Number.isFinite(n)) return undefined;
|
|
45
|
+
return Math.min(def.max, Math.max(def.min, Math.round(n)));
|
|
46
|
+
}
|
|
47
|
+
if (def.type === "shadow") {
|
|
48
|
+
return CONTRACT.themeComponentShadows.includes(value) ? value : undefined;
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validate a raw `themeConfig.components` blob down to the declared vocabulary:
|
|
55
|
+
* unknown scopes, unknown tokens and malformed values are dropped, and an
|
|
56
|
+
* emptied scope is omitted rather than kept as `{}`.
|
|
57
|
+
*
|
|
58
|
+
* `theme_config` is a JSON bag any workspace admin can PUT verbatim, so this is
|
|
59
|
+
* the guard that keeps a hand-edited theme from handing a widget a style value
|
|
60
|
+
* its renderer can't use. Returns a frozen-shaped plain object, never null, so
|
|
61
|
+
* callers can fold the result in unconditionally.
|
|
62
|
+
*
|
|
63
|
+
* @param {unknown} raw — `themeConfig.components`.
|
|
64
|
+
* @returns {Record<string, Record<string, string|number>>}
|
|
65
|
+
*/
|
|
66
|
+
function normaliseThemeComponents(raw) {
|
|
67
|
+
if (!isPlainObject(raw)) return {};
|
|
68
|
+
const out = {};
|
|
69
|
+
for (const [scope, definition] of Object.entries(CONTRACT.themeComponents)) {
|
|
70
|
+
const requested = raw[scope];
|
|
71
|
+
if (!isPlainObject(requested)) continue;
|
|
72
|
+
const tokens = {};
|
|
73
|
+
for (const [token, def] of Object.entries(definition.tokens)) {
|
|
74
|
+
if (requested[token] === undefined) continue;
|
|
75
|
+
const coerced = coerceToken(def, requested[token]);
|
|
76
|
+
if (coerced !== undefined) tokens[token] = coerced;
|
|
77
|
+
}
|
|
78
|
+
if (Object.keys(tokens).length > 0) out[scope] = tokens;
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The per-component style fields that apply to one widget, keyed by the
|
|
85
|
+
* `styleSchema` field name the widget actually reads. A widget may sit in more
|
|
86
|
+
* than one scope (a Form is a `card` whose submit button is a `button`), so
|
|
87
|
+
* every matching scope contributes.
|
|
88
|
+
*
|
|
89
|
+
* Re-validates the slice rather than trusting it: this is the last boundary
|
|
90
|
+
* before a value becomes a widget's style, and a host that folded the theme in
|
|
91
|
+
* without normalising must not be able to hand a widget malformed input.
|
|
92
|
+
*
|
|
93
|
+
* @returns {Record<string, string|number>|null} null when nothing applies.
|
|
94
|
+
*/
|
|
95
|
+
function componentStyleFor(manifestId, components) {
|
|
96
|
+
if (typeof manifestId !== "string") return null;
|
|
97
|
+
const validated = normaliseThemeComponents(components);
|
|
98
|
+
const out = {};
|
|
99
|
+
for (const [scope, definition] of Object.entries(CONTRACT.themeComponents)) {
|
|
100
|
+
const tokens = validated[scope];
|
|
101
|
+
const fields = definition.targets[manifestId];
|
|
102
|
+
if (!tokens || !fields) continue;
|
|
103
|
+
for (const [token, field] of Object.entries(fields)) {
|
|
104
|
+
if (tokens[token] !== undefined) out[field] = tokens[token];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Fold the theme's per-component tokens into a widget's props as `style`
|
|
112
|
+
* DEFAULTS. The author's per-instance REQ-THEME-13 values are spread last and
|
|
113
|
+
* therefore always win — the theme token is the app-wide baseline, the
|
|
114
|
+
* Properties Panel is the final word.
|
|
115
|
+
*
|
|
116
|
+
* Returns the SAME `props` reference when the theme sets nothing for this
|
|
117
|
+
* widget, so an unthemed app takes no extra render work and behaves exactly as
|
|
118
|
+
* it did before REQ-THEME-15.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} manifestId — the widget's canonical manifest id.
|
|
121
|
+
* @param {object} theme — the resolved widget theme (`workspace.theme`); its
|
|
122
|
+
* `components` slice is read.
|
|
123
|
+
* @param {object} props — the widget's resolved props (post-`resolveProps`).
|
|
124
|
+
* @returns {object} props, with `style` folded when the theme applies.
|
|
125
|
+
*/
|
|
126
|
+
function applyThemeComponentStyle(manifestId, theme, props) {
|
|
127
|
+
const themed = componentStyleFor(manifestId, theme && theme.components);
|
|
128
|
+
if (!themed) return props;
|
|
129
|
+
const base = isPlainObject(props) ? props : {};
|
|
130
|
+
const authored = isPlainObject(base.style) ? base.style : null;
|
|
131
|
+
return { ...base, style: { ...themed, ...authored } };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { normaliseThemeComponents, applyThemeComponentStyle };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// REQ-THEME-15 (sc-1497) — the host side of per-component style tokens.
|
|
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 `props.style` /
|
|
7
|
+
// `useWidgetStyle()` and never learns a theme token was involved.
|
|
8
|
+
//
|
|
9
|
+
// Living here — one implementation both hosts import — is what makes the tokens
|
|
10
|
+
// render identically in the Player and the export (widget-parity skill). The
|
|
11
|
+
// alternative, a copy per host, is the drift this file exists to prevent.
|
|
12
|
+
//
|
|
13
|
+
// `CONTRACT.themeComponents` is the single source of the vocabulary: which
|
|
14
|
+
// scopes exist, each token's value type, and the `styleSchema` field each target
|
|
15
|
+
// widget reads. The Mason build runner validates `set_theme` against the SAME
|
|
16
|
+
// literal, so what a planner may emit and what a host applies cannot diverge.
|
|
17
|
+
|
|
18
|
+
import { CONTRACT } from "./contract.js";
|
|
19
|
+
|
|
20
|
+
// 3-, 6- and 8-digit hex, matching what the build runner persists — the host
|
|
21
|
+
// must never drop a colour the runner already accepted.
|
|
22
|
+
const HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
23
|
+
|
|
24
|
+
function isPlainObject(value) {
|
|
25
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function coerceToken(def, value) {
|
|
29
|
+
if (def.type === "color") {
|
|
30
|
+
if (typeof value !== "string") return undefined;
|
|
31
|
+
const trimmed = value.trim();
|
|
32
|
+
return HEX_COLOR.test(trimmed) ? trimmed : undefined;
|
|
33
|
+
}
|
|
34
|
+
if (def.type === "size") {
|
|
35
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
36
|
+
if (!Number.isFinite(n)) return undefined;
|
|
37
|
+
return Math.min(def.max, Math.max(def.min, Math.round(n)));
|
|
38
|
+
}
|
|
39
|
+
if (def.type === "shadow") {
|
|
40
|
+
return CONTRACT.themeComponentShadows.includes(value) ? value : undefined;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validate a raw `themeConfig.components` blob down to the declared vocabulary:
|
|
47
|
+
* unknown scopes, unknown tokens and malformed values are dropped, and an
|
|
48
|
+
* emptied scope is omitted rather than kept as `{}`.
|
|
49
|
+
*
|
|
50
|
+
* `theme_config` is a JSON bag any workspace admin can PUT verbatim, so this is
|
|
51
|
+
* the guard that keeps a hand-edited theme from handing a widget a style value
|
|
52
|
+
* its renderer can't use. Returns a frozen-shaped plain object, never null, so
|
|
53
|
+
* callers can fold the result in unconditionally.
|
|
54
|
+
*
|
|
55
|
+
* @param {unknown} raw — `themeConfig.components`.
|
|
56
|
+
* @returns {Record<string, Record<string, string|number>>}
|
|
57
|
+
*/
|
|
58
|
+
export function normaliseThemeComponents(raw) {
|
|
59
|
+
if (!isPlainObject(raw)) return {};
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const [scope, definition] of Object.entries(CONTRACT.themeComponents)) {
|
|
62
|
+
const requested = raw[scope];
|
|
63
|
+
if (!isPlainObject(requested)) continue;
|
|
64
|
+
const tokens = {};
|
|
65
|
+
for (const [token, def] of Object.entries(definition.tokens)) {
|
|
66
|
+
if (requested[token] === undefined) continue;
|
|
67
|
+
const coerced = coerceToken(def, requested[token]);
|
|
68
|
+
if (coerced !== undefined) tokens[token] = coerced;
|
|
69
|
+
}
|
|
70
|
+
if (Object.keys(tokens).length > 0) out[scope] = tokens;
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The per-component style fields that apply to one widget, keyed by the
|
|
77
|
+
* `styleSchema` field name the widget actually reads. A widget may sit in more
|
|
78
|
+
* than one scope (a Form is a `card` whose submit button is a `button`), so
|
|
79
|
+
* every matching scope contributes.
|
|
80
|
+
*
|
|
81
|
+
* Re-validates the slice rather than trusting it: this is the last boundary
|
|
82
|
+
* before a value becomes a widget's style, and a host that folded the theme in
|
|
83
|
+
* without normalising must not be able to hand a widget malformed input.
|
|
84
|
+
*
|
|
85
|
+
* @returns {Record<string, string|number>|null} null when nothing applies.
|
|
86
|
+
*/
|
|
87
|
+
function componentStyleFor(manifestId, components) {
|
|
88
|
+
if (typeof manifestId !== "string") return null;
|
|
89
|
+
const validated = normaliseThemeComponents(components);
|
|
90
|
+
const out = {};
|
|
91
|
+
for (const [scope, definition] of Object.entries(CONTRACT.themeComponents)) {
|
|
92
|
+
const tokens = validated[scope];
|
|
93
|
+
const fields = definition.targets[manifestId];
|
|
94
|
+
if (!tokens || !fields) continue;
|
|
95
|
+
for (const [token, field] of Object.entries(fields)) {
|
|
96
|
+
if (tokens[token] !== undefined) out[field] = tokens[token];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Fold the theme's per-component tokens into a widget's props as `style`
|
|
104
|
+
* DEFAULTS. The author's per-instance REQ-THEME-13 values are spread last and
|
|
105
|
+
* therefore always win — the theme token is the app-wide baseline, the
|
|
106
|
+
* Properties Panel is the final word.
|
|
107
|
+
*
|
|
108
|
+
* Returns the SAME `props` reference when the theme sets nothing for this
|
|
109
|
+
* widget, so an unthemed app takes no extra render work and behaves exactly as
|
|
110
|
+
* it did before REQ-THEME-15.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} manifestId — the widget's canonical manifest id.
|
|
113
|
+
* @param {object} theme — the resolved widget theme (`workspace.theme`); its
|
|
114
|
+
* `components` slice is read.
|
|
115
|
+
* @param {object} props — the widget's resolved props (post-`resolveProps`).
|
|
116
|
+
* @returns {object} props, with `style` folded when the theme applies.
|
|
117
|
+
*/
|
|
118
|
+
export function applyThemeComponentStyle(manifestId, theme, props) {
|
|
119
|
+
const themed = componentStyleFor(manifestId, theme && theme.components);
|
|
120
|
+
if (!themed) return props;
|
|
121
|
+
const base = isPlainObject(props) ? props : {};
|
|
122
|
+
const authored = isPlainObject(base.style) ? base.style : null;
|
|
123
|
+
return { ...base, style: { ...themed, ...authored } };
|
|
124
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.1",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
],
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "node scripts/build.js",
|
|
51
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.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-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-image-height.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__/devserver.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js"
|
|
51
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.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-record-permissions.test.js src/__tests__/hooks-geolocation.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-image-height.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__/devserver.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/theme-components-parity.test.js"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=18"
|