@colixsystems/widget-sdk 0.117.0 → 0.118.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 +22 -1
- package/dist/contract.cjs +15 -3
- package/dist/contract.js +15 -3
- package/dist/devserver.js +92 -40
- package/dist/flatten-entry.js +817 -0
- package/dist/host.d.ts +8 -4
- package/dist/linter.js +3 -146
- package/dist/source-mask.js +162 -0
- package/dist/theme-components.cjs +70 -10
- package/dist/theme-components.js +70 -10
- package/package.json +7 -3
package/dist/host.d.ts
CHANGED
|
@@ -59,10 +59,14 @@ export function normaliseWidgetStyleFields(
|
|
|
59
59
|
export function normaliseWidgetStyles(raw: unknown): ThemeWidgetStyles;
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* REQ-THEME-15 host render-boundary helper: folds the
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
62
|
+
* REQ-THEME-15 host render-boundary helper: folds the widget's declared
|
|
63
|
+
* `styleSchema` defaults and the theme's per-component tokens into a widget's
|
|
64
|
+
* props as `style` DEFAULTS, with the author's per-instance values winning.
|
|
65
|
+
*
|
|
66
|
+
* sc-6750 — precedence, weakest first: `styleSchema` default ->
|
|
67
|
+
* palette/`components.<scope>` -> `widgetStyles[manifestId]` -> per-instance
|
|
68
|
+
* `props.style`. Returns the same `props` reference when neither the theme nor
|
|
69
|
+
* the schema sets anything. Applied by the platform hosts, never by authors.
|
|
66
70
|
*/
|
|
67
71
|
export function applyThemeComponentStyle<T = Record<string, unknown>>(
|
|
68
72
|
manifestId: string,
|
package/dist/linter.js
CHANGED
|
@@ -56,152 +56,9 @@ const CONTRACT_RULES = CONTRACT.bannedApis.map((b) =>
|
|
|
56
56
|
_ruleForIdentifier(b.identifier, b.reason),
|
|
57
57
|
);
|
|
58
58
|
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
// is only dangerous as a real identifier reference — never as prose in a
|
|
63
|
-
// `//` comment or as character data inside a string — so matching the bare
|
|
64
|
-
// word there is a false positive that blocks an otherwise-clean widget (a
|
|
65
|
-
// comment that reads "the hour window the grid renders" must not trip
|
|
66
|
-
// `no-window`).
|
|
67
|
-
//
|
|
68
|
-
// Newlines are preserved verbatim so reported line numbers still line up
|
|
69
|
-
// with the original source. Template-literal `${ … }` expression holes are
|
|
70
|
-
// left intact: real code lives there and must still be scanned (`${window}`
|
|
71
|
-
// is a genuine escape). Backslash escapes inside strings/templates are
|
|
72
|
-
// consumed so an escaped quote (`"\""`) doesn't end the literal early.
|
|
73
|
-
function _stripNonCode(source, { keepStrings = false } = {}) {
|
|
74
|
-
let out = "";
|
|
75
|
-
const n = source.length;
|
|
76
|
-
let mode = "code"; // code | line | block | sq | dq | tmpl
|
|
77
|
-
// Brace depth, plus a stack of the depths at which an enclosing template
|
|
78
|
-
// literal resumes — lets a `${ … }` hole (which may itself contain `{}`,
|
|
79
|
-
// strings, or nested templates) be told apart from the literal text.
|
|
80
|
-
let braceDepth = 0;
|
|
81
|
-
const tmplStack = [];
|
|
82
|
-
const keep = (ch) => {
|
|
83
|
-
out += ch;
|
|
84
|
-
};
|
|
85
|
-
const blank = (ch) => {
|
|
86
|
-
out += ch === "\n" || ch === "\r" ? ch : " ";
|
|
87
|
-
};
|
|
88
|
-
// String / template CONTENT: blanked for the banned-identifier scan
|
|
89
|
-
// (prose must not trip `no-window`), kept for the host-API-URL scan,
|
|
90
|
-
// whose whole job is to find a URL literal.
|
|
91
|
-
const str = keepStrings ? keep : blank;
|
|
92
|
-
let i = 0;
|
|
93
|
-
while (i < n) {
|
|
94
|
-
const ch = source[i];
|
|
95
|
-
const nx = source[i + 1];
|
|
96
|
-
if (mode === "code") {
|
|
97
|
-
if (ch === "/" && nx === "/") {
|
|
98
|
-
mode = "line";
|
|
99
|
-
blank(ch);
|
|
100
|
-
blank(nx);
|
|
101
|
-
i += 2;
|
|
102
|
-
} else if (ch === "/" && nx === "*") {
|
|
103
|
-
mode = "block";
|
|
104
|
-
blank(ch);
|
|
105
|
-
blank(nx);
|
|
106
|
-
i += 2;
|
|
107
|
-
} else if (ch === "'") {
|
|
108
|
-
mode = "sq";
|
|
109
|
-
str(ch);
|
|
110
|
-
i += 1;
|
|
111
|
-
} else if (ch === '"') {
|
|
112
|
-
mode = "dq";
|
|
113
|
-
str(ch);
|
|
114
|
-
i += 1;
|
|
115
|
-
} else if (ch === "`") {
|
|
116
|
-
mode = "tmpl";
|
|
117
|
-
str(ch);
|
|
118
|
-
i += 1;
|
|
119
|
-
} else if (ch === "{") {
|
|
120
|
-
braceDepth += 1;
|
|
121
|
-
keep(ch);
|
|
122
|
-
i += 1;
|
|
123
|
-
} else if (ch === "}") {
|
|
124
|
-
braceDepth -= 1;
|
|
125
|
-
if (
|
|
126
|
-
tmplStack.length > 0 &&
|
|
127
|
-
tmplStack[tmplStack.length - 1] === braceDepth
|
|
128
|
-
) {
|
|
129
|
-
tmplStack.pop();
|
|
130
|
-
mode = "tmpl";
|
|
131
|
-
str(ch);
|
|
132
|
-
} else {
|
|
133
|
-
keep(ch);
|
|
134
|
-
}
|
|
135
|
-
i += 1;
|
|
136
|
-
} else {
|
|
137
|
-
keep(ch);
|
|
138
|
-
i += 1;
|
|
139
|
-
}
|
|
140
|
-
} else if (mode === "line") {
|
|
141
|
-
if (ch === "\n") {
|
|
142
|
-
mode = "code";
|
|
143
|
-
keep(ch);
|
|
144
|
-
} else {
|
|
145
|
-
blank(ch);
|
|
146
|
-
}
|
|
147
|
-
i += 1;
|
|
148
|
-
} else if (mode === "block") {
|
|
149
|
-
if (ch === "*" && nx === "/") {
|
|
150
|
-
mode = "code";
|
|
151
|
-
blank(ch);
|
|
152
|
-
blank(nx);
|
|
153
|
-
i += 2;
|
|
154
|
-
} else {
|
|
155
|
-
blank(ch);
|
|
156
|
-
i += 1;
|
|
157
|
-
}
|
|
158
|
-
} else if (mode === "sq" || mode === "dq") {
|
|
159
|
-
const quote = mode === "sq" ? "'" : '"';
|
|
160
|
-
if (ch === "\\") {
|
|
161
|
-
str(ch);
|
|
162
|
-
if (i + 1 < n) str(nx);
|
|
163
|
-
i += 2;
|
|
164
|
-
} else if (ch === quote) {
|
|
165
|
-
mode = "code";
|
|
166
|
-
str(ch);
|
|
167
|
-
i += 1;
|
|
168
|
-
} else if (ch === "\n") {
|
|
169
|
-
// A bare newline terminates an unterminated string in JS; bail back
|
|
170
|
-
// to code so malformed input can't blank the rest of the file.
|
|
171
|
-
mode = "code";
|
|
172
|
-
keep(ch);
|
|
173
|
-
i += 1;
|
|
174
|
-
} else {
|
|
175
|
-
str(ch);
|
|
176
|
-
i += 1;
|
|
177
|
-
}
|
|
178
|
-
} else {
|
|
179
|
-
// mode === "tmpl"
|
|
180
|
-
if (ch === "\\") {
|
|
181
|
-
str(ch);
|
|
182
|
-
if (i + 1 < n) str(nx);
|
|
183
|
-
i += 2;
|
|
184
|
-
} else if (ch === "`") {
|
|
185
|
-
mode = "code";
|
|
186
|
-
str(ch);
|
|
187
|
-
i += 1;
|
|
188
|
-
} else if (ch === "$" && nx === "{") {
|
|
189
|
-
// Enter an expression hole. Remember the brace depth the template
|
|
190
|
-
// resumes at, then count the `{` so its matching `}` is recognised.
|
|
191
|
-
tmplStack.push(braceDepth);
|
|
192
|
-
braceDepth += 1;
|
|
193
|
-
mode = "code";
|
|
194
|
-
keep(ch);
|
|
195
|
-
keep(nx);
|
|
196
|
-
i += 2;
|
|
197
|
-
} else {
|
|
198
|
-
str(ch);
|
|
199
|
-
i += 1;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return out;
|
|
204
|
-
}
|
|
59
|
+
// sc-6086: moved to source-mask.js so the packer's scanner masks source the
|
|
60
|
+
// same way this linter does. Alias kept — the rules below read _stripNonCode.
|
|
61
|
+
import { stripNonCode as _stripNonCode } from "./source-mask.js";
|
|
205
62
|
|
|
206
63
|
// Extra rules that don't map 1:1 to a banned identifier in the contract:
|
|
207
64
|
// host-internal imports that widgets must never touch.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// sc-6086: the shared source masker, extracted verbatim from linter.js so the
|
|
2
|
+
// linter, the packer's import scanner, and the dev-server guard all agree on
|
|
3
|
+
// what is code and what is a comment or string literal (CLAUDE.md §3).
|
|
4
|
+
//
|
|
5
|
+
// Length-preserving: every blanked character becomes a space and newlines are
|
|
6
|
+
// kept, so an index into the mask is the SAME index in the original source.
|
|
7
|
+
// That is what lets a caller match against the mask and splice the original.
|
|
8
|
+
|
|
9
|
+
// Replace the *content* of comments and string / template literals with
|
|
10
|
+
// spaces so the banned-identifier scan only ever sees executable code. A
|
|
11
|
+
// banned host-escape identifier (`window`, `document`, `eval`, `process`, …)
|
|
12
|
+
// is only dangerous as a real identifier reference — never as prose in a
|
|
13
|
+
// `//` comment or as character data inside a string — so matching the bare
|
|
14
|
+
// word there is a false positive that blocks an otherwise-clean widget (a
|
|
15
|
+
// comment that reads "the hour window the grid renders" must not trip
|
|
16
|
+
// `no-window`).
|
|
17
|
+
//
|
|
18
|
+
// Newlines are preserved verbatim so reported line numbers still line up
|
|
19
|
+
// with the original source. Template-literal `${ … }` expression holes are
|
|
20
|
+
// left intact: real code lives there and must still be scanned (`${window}`
|
|
21
|
+
// is a genuine escape). Backslash escapes inside strings/templates are
|
|
22
|
+
// consumed so an escaped quote (`"\""`) doesn't end the literal early.
|
|
23
|
+
export function stripNonCode(
|
|
24
|
+
source,
|
|
25
|
+
{ keepStrings = false, keepTemplates = keepStrings } = {},
|
|
26
|
+
) {
|
|
27
|
+
let out = "";
|
|
28
|
+
const n = source.length;
|
|
29
|
+
let mode = "code"; // code | line | block | sq | dq | tmpl
|
|
30
|
+
// Brace depth, plus a stack of the depths at which an enclosing template
|
|
31
|
+
// literal resumes — lets a `${ … }` hole (which may itself contain `{}`,
|
|
32
|
+
// strings, or nested templates) be told apart from the literal text.
|
|
33
|
+
let braceDepth = 0;
|
|
34
|
+
const tmplStack = [];
|
|
35
|
+
const keep = (ch) => {
|
|
36
|
+
out += ch;
|
|
37
|
+
};
|
|
38
|
+
const blank = (ch) => {
|
|
39
|
+
out += ch === "\n" || ch === "\r" ? ch : " ";
|
|
40
|
+
};
|
|
41
|
+
// String / template CONTENT: blanked for the banned-identifier scan
|
|
42
|
+
// (prose must not trip `no-window`), kept for the host-API-URL scan,
|
|
43
|
+
// whose whole job is to find a URL literal.
|
|
44
|
+
const str = keepStrings ? keep : blank;
|
|
45
|
+
// An import specifier is always a quoted string, never a template literal —
|
|
46
|
+
// so the import scanner keeps quoted content and blanks templates, which is
|
|
47
|
+
// what stops an `import … from "./x"` line inside a backtick block from
|
|
48
|
+
// reading as a real statement.
|
|
49
|
+
const tmplStr = keepTemplates ? keep : blank;
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < n) {
|
|
52
|
+
const ch = source[i];
|
|
53
|
+
const nx = source[i + 1];
|
|
54
|
+
if (mode === "code") {
|
|
55
|
+
if (ch === "/" && nx === "/") {
|
|
56
|
+
mode = "line";
|
|
57
|
+
blank(ch);
|
|
58
|
+
blank(nx);
|
|
59
|
+
i += 2;
|
|
60
|
+
} else if (ch === "/" && nx === "*") {
|
|
61
|
+
mode = "block";
|
|
62
|
+
blank(ch);
|
|
63
|
+
blank(nx);
|
|
64
|
+
i += 2;
|
|
65
|
+
} else if (ch === "'") {
|
|
66
|
+
mode = "sq";
|
|
67
|
+
str(ch);
|
|
68
|
+
i += 1;
|
|
69
|
+
} else if (ch === '"') {
|
|
70
|
+
mode = "dq";
|
|
71
|
+
str(ch);
|
|
72
|
+
i += 1;
|
|
73
|
+
} else if (ch === "`") {
|
|
74
|
+
mode = "tmpl";
|
|
75
|
+
tmplStr(ch);
|
|
76
|
+
i += 1;
|
|
77
|
+
} else if (ch === "{") {
|
|
78
|
+
braceDepth += 1;
|
|
79
|
+
keep(ch);
|
|
80
|
+
i += 1;
|
|
81
|
+
} else if (ch === "}") {
|
|
82
|
+
braceDepth -= 1;
|
|
83
|
+
if (
|
|
84
|
+
tmplStack.length > 0 &&
|
|
85
|
+
tmplStack[tmplStack.length - 1] === braceDepth
|
|
86
|
+
) {
|
|
87
|
+
tmplStack.pop();
|
|
88
|
+
mode = "tmpl";
|
|
89
|
+
str(ch);
|
|
90
|
+
} else {
|
|
91
|
+
keep(ch);
|
|
92
|
+
}
|
|
93
|
+
i += 1;
|
|
94
|
+
} else {
|
|
95
|
+
keep(ch);
|
|
96
|
+
i += 1;
|
|
97
|
+
}
|
|
98
|
+
} else if (mode === "line") {
|
|
99
|
+
if (ch === "\n") {
|
|
100
|
+
mode = "code";
|
|
101
|
+
keep(ch);
|
|
102
|
+
} else {
|
|
103
|
+
blank(ch);
|
|
104
|
+
}
|
|
105
|
+
i += 1;
|
|
106
|
+
} else if (mode === "block") {
|
|
107
|
+
if (ch === "*" && nx === "/") {
|
|
108
|
+
mode = "code";
|
|
109
|
+
blank(ch);
|
|
110
|
+
blank(nx);
|
|
111
|
+
i += 2;
|
|
112
|
+
} else {
|
|
113
|
+
blank(ch);
|
|
114
|
+
i += 1;
|
|
115
|
+
}
|
|
116
|
+
} else if (mode === "sq" || mode === "dq") {
|
|
117
|
+
const quote = mode === "sq" ? "'" : '"';
|
|
118
|
+
if (ch === "\\") {
|
|
119
|
+
str(ch);
|
|
120
|
+
if (i + 1 < n) str(nx);
|
|
121
|
+
i += 2;
|
|
122
|
+
} else if (ch === quote) {
|
|
123
|
+
mode = "code";
|
|
124
|
+
str(ch);
|
|
125
|
+
i += 1;
|
|
126
|
+
} else if (ch === "\n") {
|
|
127
|
+
// A bare newline terminates an unterminated string in JS; bail back
|
|
128
|
+
// to code so malformed input can't blank the rest of the file.
|
|
129
|
+
mode = "code";
|
|
130
|
+
keep(ch);
|
|
131
|
+
i += 1;
|
|
132
|
+
} else {
|
|
133
|
+
str(ch);
|
|
134
|
+
i += 1;
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
// mode === "tmpl"
|
|
138
|
+
if (ch === "\\") {
|
|
139
|
+
tmplStr(ch);
|
|
140
|
+
if (i + 1 < n) tmplStr(nx);
|
|
141
|
+
i += 2;
|
|
142
|
+
} else if (ch === "`") {
|
|
143
|
+
mode = "code";
|
|
144
|
+
tmplStr(ch);
|
|
145
|
+
i += 1;
|
|
146
|
+
} else if (ch === "$" && nx === "{") {
|
|
147
|
+
// Enter an expression hole. Remember the brace depth the template
|
|
148
|
+
// resumes at, then count the `{` so its matching `}` is recognised.
|
|
149
|
+
tmplStack.push(braceDepth);
|
|
150
|
+
braceDepth += 1;
|
|
151
|
+
mode = "code";
|
|
152
|
+
keep(ch);
|
|
153
|
+
keep(nx);
|
|
154
|
+
i += 2;
|
|
155
|
+
} else {
|
|
156
|
+
tmplStr(ch);
|
|
157
|
+
i += 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
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
|
|
314
|
-
* widget, so an unthemed app
|
|
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
|
-
* @
|
|
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(
|
package/dist/theme-components.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
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
|
|
306
|
-
* widget, so an unthemed app
|
|
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
|
-
* @
|
|
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.
|
|
3
|
+
"version": "0.118.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
|
}
|