@colixsystems/widget-sdk 0.110.0 → 0.111.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 +16 -1
- package/dist/contract.cjs +13 -1
- package/dist/contract.js +13 -1
- package/dist/index.js +8 -0
- package/dist/index.native.js +8 -0
- package/dist/property-schema.js +38 -0
- package/dist/spacing.js +118 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,7 +69,22 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
69
69
|
|
|
70
70
|
## Status
|
|
71
71
|
|
|
72
|
-
`v0.
|
|
72
|
+
`v0.111.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**.
|
|
73
|
+
|
|
74
|
+
### What's new in 0.111.0 (contract 1.85.0)
|
|
75
|
+
|
|
76
|
+
**Each side can be spaced on its own — the `spacing` property type (sc-6447).** Padding and margin were single numbers, so every inset applied to all four sides at once: a hero with generous top padding and none at the bottom, or a card held off only its left neighbour, had no expression. `cornerRadius` already offered each corner (0.104.0); padding and margin were the last four-valued members of the box model that did not.
|
|
77
|
+
|
|
78
|
+
Declare `{ type: "spacing", label: "Padding", validation: { min: 0, max: 64 } }` in your `propertySchema` or `styleSchema`. The Studio renders a slider with a typeable number that sets all four sides, plus a disclosure for setting each one. The authored value is `number | { top, right, bottom, left }` — the scalar form is unchanged, so every value stored before is still valid.
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
import { normaliseSpacing, spacingStyle } from "@colixsystems/widget-sdk";
|
|
82
|
+
|
|
83
|
+
const padding = normaliseSpacing(props.style?.padding, 0, 64);
|
|
84
|
+
return <View style={[styles.card, spacingStyle(padding, "padding")]} />;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`normaliseSpacing(value, fallback, max)` returns all four sides resolved and clamped; `spacingStyle(spacing, property, format)` emits the `padding`/`margin` shorthand when the sides agree and the four long-hand props when they differ (pass `` n => `${n}px` `` for the DOM). `mapSpacing` pushes each side through your own scaling, `isUniformSpacing` and `isZeroSpacing` round out the set. `CONTRACT.version` → `1.85.0`. Additive: every value accepted before is accepted now.
|
|
73
88
|
|
|
74
89
|
### What's new in 0.110.0 (contract 1.84.0)
|
|
75
90
|
|
package/dist/contract.cjs
CHANGED
|
@@ -3374,7 +3374,19 @@ const CONTRACT = deepFreeze({
|
|
|
3374
3374
|
// one `headerTintColor` cannot express; an authored `topBar.textColor`
|
|
3375
3375
|
// drives both. `show` is deliberately absent -- it depends on the menu
|
|
3376
3376
|
// type, not the theme, and is a no-op on native.
|
|
3377
|
-
|
|
3377
|
+
//
|
|
3378
|
+
// 1.85.0: additive (sc-6447) — the `spacing` property type: a padding or
|
|
3379
|
+
// margin value is `number | { top, right, bottom, left }`, resolved by
|
|
3380
|
+
// `normaliseSpacing` and emitted by `spacingStyle(resolved, "padding" |
|
|
3381
|
+
// "margin")`. Padding and margin were the last four-valued members of the
|
|
3382
|
+
// box model an author could only set on all sides at once, while
|
|
3383
|
+
// `cornerRadius` (1.79.0) already offered each corner — so the same
|
|
3384
|
+
// master-plus-disclosure control now backs all three. The scalar form is
|
|
3385
|
+
// unchanged and still emits the shorthand, so an existing style renders
|
|
3386
|
+
// byte-identically. `mapSpacing` pushes each side through the responsive
|
|
3387
|
+
// and theme scaling the scalar already got; `isZeroSpacing` lets each box
|
|
3388
|
+
// property keep its own zero policy.
|
|
3389
|
+
version: "1.85.0",
|
|
3378
3390
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3379
3391
|
hooks: HOOKS,
|
|
3380
3392
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -3374,7 +3374,19 @@ const CONTRACT = deepFreeze({
|
|
|
3374
3374
|
// one `headerTintColor` cannot express; an authored `topBar.textColor`
|
|
3375
3375
|
// drives both. `show` is deliberately absent -- it depends on the menu
|
|
3376
3376
|
// type, not the theme, and is a no-op on native.
|
|
3377
|
-
|
|
3377
|
+
//
|
|
3378
|
+
// 1.85.0: additive (sc-6447) — the `spacing` property type: a padding or
|
|
3379
|
+
// margin value is `number | { top, right, bottom, left }`, resolved by
|
|
3380
|
+
// `normaliseSpacing` and emitted by `spacingStyle(resolved, "padding" |
|
|
3381
|
+
// "margin")`. Padding and margin were the last four-valued members of the
|
|
3382
|
+
// box model an author could only set on all sides at once, while
|
|
3383
|
+
// `cornerRadius` (1.79.0) already offered each corner — so the same
|
|
3384
|
+
// master-plus-disclosure control now backs all three. The scalar form is
|
|
3385
|
+
// unchanged and still emits the shorthand, so an existing style renders
|
|
3386
|
+
// byte-identically. `mapSpacing` pushes each side through the responsive
|
|
3387
|
+
// and theme scaling the scalar already got; `isZeroSpacing` lets each box
|
|
3388
|
+
// property keep its own zero policy.
|
|
3389
|
+
version: "1.85.0",
|
|
3378
3390
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3379
3391
|
hooks: HOOKS,
|
|
3380
3392
|
primitives: PRIMITIVES,
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,14 @@ export {
|
|
|
13
13
|
cornerRadiusStyle,
|
|
14
14
|
hasCornerRadius,
|
|
15
15
|
} from "./corner-radius.js";
|
|
16
|
+
export {
|
|
17
|
+
SPACING_KEYS,
|
|
18
|
+
normaliseSpacing,
|
|
19
|
+
isUniformSpacing,
|
|
20
|
+
isZeroSpacing,
|
|
21
|
+
mapSpacing,
|
|
22
|
+
spacingStyle,
|
|
23
|
+
} from "./spacing.js";
|
|
16
24
|
export {
|
|
17
25
|
WidgetContextProvider,
|
|
18
26
|
DatastoreError,
|
package/dist/index.native.js
CHANGED
|
@@ -13,6 +13,14 @@ export {
|
|
|
13
13
|
cornerRadiusStyle,
|
|
14
14
|
hasCornerRadius,
|
|
15
15
|
} from "./corner-radius.js";
|
|
16
|
+
export {
|
|
17
|
+
SPACING_KEYS,
|
|
18
|
+
normaliseSpacing,
|
|
19
|
+
isUniformSpacing,
|
|
20
|
+
isZeroSpacing,
|
|
21
|
+
mapSpacing,
|
|
22
|
+
spacingStyle,
|
|
23
|
+
} from "./spacing.js";
|
|
16
24
|
export {
|
|
17
25
|
WidgetContextProvider,
|
|
18
26
|
DatastoreError,
|
package/dist/property-schema.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Drives the schema-driven Properties Panel and validates persisted page JSON.
|
|
3
3
|
|
|
4
4
|
import { CORNER_RADIUS_KEYS } from "./corner-radius.js";
|
|
5
|
+
import { SPACING_KEYS } from "./spacing.js";
|
|
5
6
|
|
|
6
7
|
const VALID_TYPES = new Set([
|
|
7
8
|
"string", "number", "boolean",
|
|
@@ -57,6 +58,13 @@ const VALID_TYPES = new Set([
|
|
|
57
58
|
// which both hosts spell identically. Plain numbers, so tenant-copy needs
|
|
58
59
|
// no remap.
|
|
59
60
|
"cornerRadius",
|
|
61
|
+
// REQ-LAY-17 (sc-6447): `spacing` is a padding/margin picker. Its value is
|
|
62
|
+
// `number | { top, right, bottom, left }` — a scalar spaces all four sides,
|
|
63
|
+
// the object spaces each independently. The Studio renders a slider +
|
|
64
|
+
// typeable number with a per-side disclosure; a widget turns the value into
|
|
65
|
+
// style props with `spacingStyle(normaliseSpacing(v), "padding")`, which both
|
|
66
|
+
// hosts spell identically. Plain numbers, so tenant-copy needs no remap.
|
|
67
|
+
"spacing",
|
|
60
68
|
"expression", "eventBinding",
|
|
61
69
|
"object", "array",
|
|
62
70
|
]);
|
|
@@ -192,6 +200,36 @@ function coerceLeaf(def, value, path, errors) {
|
|
|
192
200
|
}
|
|
193
201
|
return value;
|
|
194
202
|
}
|
|
203
|
+
case "spacing": {
|
|
204
|
+
// REQ-LAY-17: a scalar spaces all four sides; an object spaces each.
|
|
205
|
+
// Every side is optional so a half-set object stays valid while the
|
|
206
|
+
// author is still adjusting — an unset side falls back to the scalar.
|
|
207
|
+
const { min = 0, max } = def.validation || {};
|
|
208
|
+
const checkSide = (n, at) => {
|
|
209
|
+
if (typeof n !== "number" || Number.isNaN(n)) {
|
|
210
|
+
errors.push(`${at}: expected number`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (n < min) errors.push(`${at}: must be >= ${min}`);
|
|
214
|
+
if (max !== undefined && n > max) errors.push(`${at}: must be <= ${max}`);
|
|
215
|
+
};
|
|
216
|
+
if (typeof value === "number") {
|
|
217
|
+
checkSide(value, path);
|
|
218
|
+
return value;
|
|
219
|
+
}
|
|
220
|
+
if (!isPlainObject(value)) {
|
|
221
|
+
errors.push(`${path}: expected number or per-side object`);
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
for (const [k, n] of Object.entries(value)) {
|
|
225
|
+
if (!SPACING_KEYS.includes(k)) {
|
|
226
|
+
errors.push(`${path}.${k}: unknown side`);
|
|
227
|
+
} else if (n !== undefined && n !== null) {
|
|
228
|
+
checkSide(n, `${path}.${k}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
195
233
|
case "select":
|
|
196
234
|
if (Array.isArray(def.enum) && !def.enum.some((e) => e.value === value)) {
|
|
197
235
|
errors.push(`${path}: value not in enum`);
|
package/dist/spacing.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// REQ-LAY-17 (sc-6447): the per-side spacing vocabulary. ONE normaliser and ONE
|
|
2
|
+
// style emitter for the web Player, the Builder canvas, the exported Expo app,
|
|
3
|
+
// and any custom widget declaring a `padding`/`margin` field — so the four can
|
|
4
|
+
// never disagree about what a spacing value means.
|
|
5
|
+
//
|
|
6
|
+
// The authored value is `number | { top, right, bottom, left }`. The scalar form
|
|
7
|
+
// is what every existing style holds, so it stays first-class rather than being
|
|
8
|
+
// migrated away: widening the shape beats adding a second key beside it
|
|
9
|
+
// (CLAUDE.md §3). This mirrors `corner-radius.js` deliberately — padding/margin
|
|
10
|
+
// and cornerRadius are the four-valued members of one box model, and an author
|
|
11
|
+
// meets the same affordance in both.
|
|
12
|
+
//
|
|
13
|
+
// Both hosts spell the long-hand props identically (`paddingTop` / `marginTop`
|
|
14
|
+
// &co in React inline style AND in React Native), which is why one emitter
|
|
15
|
+
// serves both.
|
|
16
|
+
|
|
17
|
+
export const SPACING_KEYS = Object.freeze(["top", "right", "bottom", "left"]);
|
|
18
|
+
|
|
19
|
+
// The CSS/RN suffix per side. `padding` + "Top" and `margin` + "Top" are both
|
|
20
|
+
// valid on web and native, so the property name is the caller's to choose.
|
|
21
|
+
const LONGHAND_SUFFIX = Object.freeze({
|
|
22
|
+
top: "Top",
|
|
23
|
+
right: "Right",
|
|
24
|
+
bottom: "Bottom",
|
|
25
|
+
left: "Left",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function clampSide(value, fallback, max) {
|
|
29
|
+
// Unset (undefined/null/"") falls back; an explicit 0 is honoured — that
|
|
30
|
+
// distinction is what makes "no padding on just this side" expressible.
|
|
31
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
32
|
+
const n = Number(value);
|
|
33
|
+
if (!Number.isFinite(n)) return fallback;
|
|
34
|
+
// Truncates, where `corner-radius.js` rounds: padding and margin have always
|
|
35
|
+
// truncated (`clampInt` / `clampMargin`), and changing that would shift every
|
|
36
|
+
// existing fractional value by a pixel.
|
|
37
|
+
return Math.min(Math.max(Math.trunc(n), 0), max);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve an authored spacing to its four sides.
|
|
42
|
+
*
|
|
43
|
+
* @param {number|object|null|undefined} value the authored `number | {sides}`
|
|
44
|
+
* @param {number} [fallback] the value each unset side takes
|
|
45
|
+
* @param {number} [max] the upper clamp, matching the field's declared max
|
|
46
|
+
* @returns {{top:number, right:number, bottom:number, left:number}}
|
|
47
|
+
*/
|
|
48
|
+
export function normaliseSpacing(value, fallback = 0, max = 64) {
|
|
49
|
+
const base = clampSide(
|
|
50
|
+
typeof value === "number" || typeof value === "string" ? value : undefined,
|
|
51
|
+
clampSide(fallback, 0, max),
|
|
52
|
+
max,
|
|
53
|
+
);
|
|
54
|
+
const sides = value && typeof value === "object" ? value : null;
|
|
55
|
+
const out = {};
|
|
56
|
+
for (const key of SPACING_KEYS) {
|
|
57
|
+
out[key] = clampSide(sides ? sides[key] : undefined, base, max);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** True when all four sides resolve to the same number. */
|
|
63
|
+
export function isUniformSpacing(spacing) {
|
|
64
|
+
if (!spacing) return true;
|
|
65
|
+
const { top } = spacing;
|
|
66
|
+
return SPACING_KEYS.every((key) => spacing[key] === top);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** True when every side resolves to 0 — i.e. the value asks for no spacing. */
|
|
70
|
+
export function isZeroSpacing(spacing) {
|
|
71
|
+
return !spacing || SPACING_KEYS.every((key) => !spacing[key]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Map every side through `fn`, keeping the resolved shape. Used to push each
|
|
76
|
+
* side through the responsive/theme scaling the scalar form already got, so a
|
|
77
|
+
* per-side value scales down on a phone exactly like a uniform one.
|
|
78
|
+
*
|
|
79
|
+
* @param {object|null} spacing output of {@link normaliseSpacing}
|
|
80
|
+
* @param {(n: number) => number} fn
|
|
81
|
+
* @returns {object|null}
|
|
82
|
+
*/
|
|
83
|
+
export function mapSpacing(spacing, fn) {
|
|
84
|
+
if (!spacing) return null;
|
|
85
|
+
const out = {};
|
|
86
|
+
for (const key of SPACING_KEYS) {
|
|
87
|
+
const next = Number(fn(spacing[key]));
|
|
88
|
+
out[key] = Number.isFinite(next) ? next : spacing[key];
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The style props for a resolved spacing, in the spelling BOTH hosts accept.
|
|
95
|
+
* A uniform value emits the shorthand so an untouched style is byte-identical
|
|
96
|
+
* to what the scalar form produced; only a genuinely mixed value pays for the
|
|
97
|
+
* four long-hand props.
|
|
98
|
+
*
|
|
99
|
+
* This emitter is MECHANICAL — it always emits, including an all-zero value.
|
|
100
|
+
* The zero policy differs per box property (a container's `padding: 0` has
|
|
101
|
+
* always been written, while a zero `margin` has always emitted nothing), so it
|
|
102
|
+
* belongs with each caller via {@link isZeroSpacing}, not baked in here.
|
|
103
|
+
*
|
|
104
|
+
* @param {object|null} spacing output of {@link normaliseSpacing}
|
|
105
|
+
* @param {"padding"|"margin"} [property] which box property to spell
|
|
106
|
+
* @param {(n: number) => any} [format] wraps each number — the DOM needs "12px"
|
|
107
|
+
* @returns {object|null}
|
|
108
|
+
*/
|
|
109
|
+
export function spacingStyle(spacing, property = "padding", format) {
|
|
110
|
+
if (!spacing) return null;
|
|
111
|
+
const wrap = format || ((n) => n);
|
|
112
|
+
if (isUniformSpacing(spacing)) return { [property]: wrap(spacing.top) };
|
|
113
|
+
const out = {};
|
|
114
|
+
for (const key of SPACING_KEYS) {
|
|
115
|
+
out[`${property}${LONGHAND_SUFFIX[key]}`] = wrap(spacing[key]);
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.111.0",
|
|
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",
|