@colixsystems/widget-sdk 0.284.0 → 0.285.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 +5 -1
- package/dist/container-pattern.cjs +196 -0
- package/dist/container-pattern.js +191 -0
- package/dist/host.d.ts +30 -0
- package/dist/host.js +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -89,7 +89,11 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
89
89
|
|
|
90
90
|
## Status
|
|
91
91
|
|
|
92
|
-
`v0.
|
|
92
|
+
`v0.285.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**.
|
|
93
|
+
|
|
94
|
+
### What's new in 0.285.0 (contract 1.233.0)
|
|
95
|
+
|
|
96
|
+
**Host only: container background patterns.** `@colixsystems/widget-sdk/host` exports `normaliseContainerPattern`, `containerPatternTile` and `containerPatternSvg`, the one definition of a layout container's `backgroundPattern` (dots, grid, lines, stripes, grain) that the web Player and the native export both tile. Nothing changes for widget authors.
|
|
93
97
|
|
|
94
98
|
### What's new in 0.284.0 (contract 1.233.0)
|
|
95
99
|
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// GENERATED from container-pattern.js by scripts/generate-cjs.js — DO NOT EDIT.
|
|
2
|
+
// Change container-pattern.js, then run `npm run generate:cjs` in packages/widget-sdk.
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const { isHexColor } = require("./contract.cjs");
|
|
6
|
+
|
|
7
|
+
// sc-14027 — a layout container's background pattern: dot paper, a grid, ruled
|
|
8
|
+
// lines, a diagonal hatch or film grain, tiled over the fill and under the
|
|
9
|
+
// scrim. The ONE definition of the vocabulary and of each tile's geometry, so
|
|
10
|
+
// the web Player (a CSS background tile), the compiled Expo app (a
|
|
11
|
+
// react-native-svg <Pattern>) and Mason's plan validator cannot disagree.
|
|
12
|
+
//
|
|
13
|
+
// Host-integration surface, re-exported from `host.js`.
|
|
14
|
+
|
|
15
|
+
const CONTAINER_PATTERN_KINDS = Object.freeze([
|
|
16
|
+
"dots",
|
|
17
|
+
"grid",
|
|
18
|
+
"lines",
|
|
19
|
+
"stripes",
|
|
20
|
+
"grain",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
// Tile edge in px (grain: the scale of its specks). Opacity is a 0-100
|
|
24
|
+
// percentage, like every other opacity a container style carries.
|
|
25
|
+
const CONTAINER_PATTERN_SIZE = Object.freeze({ min: 4, max: 64, default: 12 });
|
|
26
|
+
const CONTAINER_PATTERN_OPACITY = Object.freeze({ min: 0, max: 100, default: 15 });
|
|
27
|
+
|
|
28
|
+
const FALLBACK_COLOR = "#000000";
|
|
29
|
+
const PATTERN_KEYS = ["kind", "color", "size", "opacity"];
|
|
30
|
+
|
|
31
|
+
function clampNumber(value, { min, max, default: fallback }) {
|
|
32
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
33
|
+
const n = Number(value);
|
|
34
|
+
if (!Number.isFinite(n)) return fallback;
|
|
35
|
+
return Math.min(Math.max(n, min), max);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function inRange(value, { min, max }) {
|
|
39
|
+
return value === undefined || (typeof value === "number" && value >= min && value <= max);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Strict: what a writer (Mason) must send. The hosts clamp instead, so a
|
|
43
|
+
// value only strict mode refuses still paints.
|
|
44
|
+
function isStrictPattern(raw) {
|
|
45
|
+
return (
|
|
46
|
+
Object.keys(raw).every((key) => PATTERN_KEYS.includes(key)) &&
|
|
47
|
+
(raw.color === undefined || raw.color === "" || isHexColor(raw.color)) &&
|
|
48
|
+
inRange(raw.size, CONTAINER_PATTERN_SIZE) &&
|
|
49
|
+
inRange(raw.opacity, CONTAINER_PATTERN_OPACITY)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve an authored `style.backgroundPattern` into what a host paints.
|
|
55
|
+
*
|
|
56
|
+
* @param {unknown} raw `{ kind, color?, size?, opacity? }`
|
|
57
|
+
* @param {string} [fallbackColor] the ink an unset colour takes, usually the surface's `onSurface`
|
|
58
|
+
* @param {{strict?: boolean}} [options] strict: refuse unknown keys and out-of-range values instead of clamping
|
|
59
|
+
* @returns {{kind: string, color: string, size: number, opacity: number}|null}
|
|
60
|
+
* null when no recognised kind is set, so an unpatterned container paints nothing new.
|
|
61
|
+
* `opacity` is the 0-1 alpha.
|
|
62
|
+
*/
|
|
63
|
+
function normaliseContainerPattern(raw, fallbackColor, { strict = false } = {}) {
|
|
64
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
65
|
+
if (!CONTAINER_PATTERN_KINDS.includes(raw.kind)) return null;
|
|
66
|
+
if (strict && !isStrictPattern(raw)) return null;
|
|
67
|
+
// Hex only: the colour is written into SVG markup on the web.
|
|
68
|
+
const color = isHexColor(raw.color)
|
|
69
|
+
? raw.color
|
|
70
|
+
: isHexColor(fallbackColor)
|
|
71
|
+
? fallbackColor
|
|
72
|
+
: FALLBACK_COLOR;
|
|
73
|
+
const opacity = clampNumber(raw.opacity, CONTAINER_PATTERN_OPACITY) / 100;
|
|
74
|
+
if (opacity === 0) return null;
|
|
75
|
+
return {
|
|
76
|
+
kind: raw.kind,
|
|
77
|
+
color,
|
|
78
|
+
size: Math.round(clampNumber(raw.size, CONTAINER_PATTERN_SIZE)),
|
|
79
|
+
opacity,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
84
|
+
|
|
85
|
+
// The grain tile: a fixed speckle on a GRAIN_CELLS square grid, seeded so every
|
|
86
|
+
// host and every render draws the same noise. Not feTurbulence: native has no
|
|
87
|
+
// filter primitives, and a computed tile is identical on both hosts.
|
|
88
|
+
const GRAIN_CELLS = 48;
|
|
89
|
+
const GRAIN_LEVELS = [0.35, 0.6, 1];
|
|
90
|
+
const GRAIN_SPECKS = (() => {
|
|
91
|
+
let seed = 0x14027;
|
|
92
|
+
const next = () => {
|
|
93
|
+
seed = (seed + 0x6d2b79f5) | 0;
|
|
94
|
+
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
|
95
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
96
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
97
|
+
};
|
|
98
|
+
const byLevel = GRAIN_LEVELS.map(() => []);
|
|
99
|
+
for (let y = 0; y < GRAIN_CELLS; y += 1) {
|
|
100
|
+
for (let x = 0; x < GRAIN_CELLS; x += 1) {
|
|
101
|
+
if (next() < 0.1) byLevel[Math.floor(next() * GRAIN_LEVELS.length)].push([x, y]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return byLevel;
|
|
105
|
+
})();
|
|
106
|
+
|
|
107
|
+
function grainTile(size) {
|
|
108
|
+
const cell = size / CONTAINER_PATTERN_SIZE.default;
|
|
109
|
+
const c = round2(cell);
|
|
110
|
+
return {
|
|
111
|
+
size: round2(GRAIN_CELLS * cell),
|
|
112
|
+
paths: GRAIN_SPECKS.map((specks, i) => ({
|
|
113
|
+
d: specks.map(([x, y]) => `M${round2(x * cell)} ${round2(y * cell)}h${c}v${c}h-${c}z`).join(""),
|
|
114
|
+
fill: true,
|
|
115
|
+
alpha: GRAIN_LEVELS[i],
|
|
116
|
+
})),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Tiles depend only on kind and the clamped size, so the cache stays small.
|
|
121
|
+
const TILE_CACHE = new Map();
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The repeating tile for a resolved pattern, as plain SVG path data both hosts
|
|
125
|
+
* draw: the web as an SVG string, native as react-native-svg `<Path>`s.
|
|
126
|
+
*
|
|
127
|
+
* @param {{kind: string, size: number}} pattern output of {@link normaliseContainerPattern}
|
|
128
|
+
* @returns {{size: number, paths: Array<{d: string, fill?: boolean, strokeWidth?: number, alpha?: number}>}}
|
|
129
|
+
* shared and cached: do not mutate it.
|
|
130
|
+
*/
|
|
131
|
+
function containerPatternTile(pattern) {
|
|
132
|
+
const key = `${pattern.kind}:${pattern.size}`;
|
|
133
|
+
if (!TILE_CACHE.has(key)) TILE_CACHE.set(key, buildTile(pattern.kind, pattern.size));
|
|
134
|
+
return TILE_CACHE.get(key);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildTile(kind, s) {
|
|
138
|
+
if (kind === "grain") return grainTile(s);
|
|
139
|
+
if (kind === "dots") {
|
|
140
|
+
const r = round2(Math.max(1, s / 8));
|
|
141
|
+
const c = s / 2;
|
|
142
|
+
return {
|
|
143
|
+
size: s,
|
|
144
|
+
paths: [{ d: `M${round2(c - r)} ${c}a${r} ${r} 0 1 0 ${round2(r * 2)} 0a${r} ${r} 0 1 0 -${round2(r * 2)} 0z`, fill: true }],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (kind === "grid") {
|
|
148
|
+
return { size: s, paths: [{ d: `M0 0.5H${s}M0.5 0V${s}`, strokeWidth: 1 }] };
|
|
149
|
+
}
|
|
150
|
+
if (kind === "lines") {
|
|
151
|
+
return { size: s, paths: [{ d: `M0 0.5H${s}`, strokeWidth: 1 }] };
|
|
152
|
+
}
|
|
153
|
+
// stripes: a 45° hatch. The two corner segments complete the stripe the
|
|
154
|
+
// neighbouring tiles cut, so the hatch runs unbroken across tile edges.
|
|
155
|
+
const h = s / 2;
|
|
156
|
+
return {
|
|
157
|
+
size: s,
|
|
158
|
+
paths: [
|
|
159
|
+
{
|
|
160
|
+
d: `M0 ${s}L${s} 0M-${h} ${h}L${h} -${h}M${h} ${s + h}L${s + h} ${h}`,
|
|
161
|
+
strokeWidth: round2(Math.max(1, s / 4)),
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The tile as standalone SVG markup, for the web host's repeating CSS
|
|
169
|
+
* background. The colour is hex-validated by {@link normaliseContainerPattern}
|
|
170
|
+
* and the path data is numeric, so nothing authored reaches the markup raw.
|
|
171
|
+
*
|
|
172
|
+
* @param {{kind: string, color: string, size: number}} pattern
|
|
173
|
+
* @returns {string}
|
|
174
|
+
*/
|
|
175
|
+
function containerPatternSvg(pattern) {
|
|
176
|
+
const tile = containerPatternTile(pattern);
|
|
177
|
+
const paths = tile.paths
|
|
178
|
+
.map((p) => {
|
|
179
|
+
const paint = p.fill
|
|
180
|
+
? `fill="${pattern.color}"`
|
|
181
|
+
: `fill="none" stroke="${pattern.color}" stroke-width="${p.strokeWidth}"`;
|
|
182
|
+
const alpha = p.alpha === undefined ? "" : ` opacity="${p.alpha}"`;
|
|
183
|
+
return `<path d="${p.d}" ${paint}${alpha}/>`;
|
|
184
|
+
})
|
|
185
|
+
.join("");
|
|
186
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${tile.size}" height="${tile.size}">${paths}</svg>`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = {
|
|
190
|
+
CONTAINER_PATTERN_KINDS,
|
|
191
|
+
CONTAINER_PATTERN_SIZE,
|
|
192
|
+
CONTAINER_PATTERN_OPACITY,
|
|
193
|
+
normaliseContainerPattern,
|
|
194
|
+
containerPatternTile,
|
|
195
|
+
containerPatternSvg,
|
|
196
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// sc-14027 — a layout container's background pattern: dot paper, a grid, ruled
|
|
2
|
+
// lines, a diagonal hatch or film grain, tiled over the fill and under the
|
|
3
|
+
// scrim. The ONE definition of the vocabulary and of each tile's geometry, so
|
|
4
|
+
// the web Player (a CSS background tile), the compiled Expo app (a
|
|
5
|
+
// react-native-svg <Pattern>) and Mason's plan validator cannot disagree.
|
|
6
|
+
//
|
|
7
|
+
// Host-integration surface, re-exported from `host.js`.
|
|
8
|
+
import { isHexColor } from "./contract.js";
|
|
9
|
+
|
|
10
|
+
const CONTAINER_PATTERN_KINDS = Object.freeze([
|
|
11
|
+
"dots",
|
|
12
|
+
"grid",
|
|
13
|
+
"lines",
|
|
14
|
+
"stripes",
|
|
15
|
+
"grain",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
// Tile edge in px (grain: the scale of its specks). Opacity is a 0-100
|
|
19
|
+
// percentage, like every other opacity a container style carries.
|
|
20
|
+
const CONTAINER_PATTERN_SIZE = Object.freeze({ min: 4, max: 64, default: 12 });
|
|
21
|
+
const CONTAINER_PATTERN_OPACITY = Object.freeze({ min: 0, max: 100, default: 15 });
|
|
22
|
+
|
|
23
|
+
const FALLBACK_COLOR = "#000000";
|
|
24
|
+
const PATTERN_KEYS = ["kind", "color", "size", "opacity"];
|
|
25
|
+
|
|
26
|
+
function clampNumber(value, { min, max, default: fallback }) {
|
|
27
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
28
|
+
const n = Number(value);
|
|
29
|
+
if (!Number.isFinite(n)) return fallback;
|
|
30
|
+
return Math.min(Math.max(n, min), max);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function inRange(value, { min, max }) {
|
|
34
|
+
return value === undefined || (typeof value === "number" && value >= min && value <= max);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Strict: what a writer (Mason) must send. The hosts clamp instead, so a
|
|
38
|
+
// value only strict mode refuses still paints.
|
|
39
|
+
function isStrictPattern(raw) {
|
|
40
|
+
return (
|
|
41
|
+
Object.keys(raw).every((key) => PATTERN_KEYS.includes(key)) &&
|
|
42
|
+
(raw.color === undefined || raw.color === "" || isHexColor(raw.color)) &&
|
|
43
|
+
inRange(raw.size, CONTAINER_PATTERN_SIZE) &&
|
|
44
|
+
inRange(raw.opacity, CONTAINER_PATTERN_OPACITY)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve an authored `style.backgroundPattern` into what a host paints.
|
|
50
|
+
*
|
|
51
|
+
* @param {unknown} raw `{ kind, color?, size?, opacity? }`
|
|
52
|
+
* @param {string} [fallbackColor] the ink an unset colour takes, usually the surface's `onSurface`
|
|
53
|
+
* @param {{strict?: boolean}} [options] strict: refuse unknown keys and out-of-range values instead of clamping
|
|
54
|
+
* @returns {{kind: string, color: string, size: number, opacity: number}|null}
|
|
55
|
+
* null when no recognised kind is set, so an unpatterned container paints nothing new.
|
|
56
|
+
* `opacity` is the 0-1 alpha.
|
|
57
|
+
*/
|
|
58
|
+
function normaliseContainerPattern(raw, fallbackColor, { strict = false } = {}) {
|
|
59
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
60
|
+
if (!CONTAINER_PATTERN_KINDS.includes(raw.kind)) return null;
|
|
61
|
+
if (strict && !isStrictPattern(raw)) return null;
|
|
62
|
+
// Hex only: the colour is written into SVG markup on the web.
|
|
63
|
+
const color = isHexColor(raw.color)
|
|
64
|
+
? raw.color
|
|
65
|
+
: isHexColor(fallbackColor)
|
|
66
|
+
? fallbackColor
|
|
67
|
+
: FALLBACK_COLOR;
|
|
68
|
+
const opacity = clampNumber(raw.opacity, CONTAINER_PATTERN_OPACITY) / 100;
|
|
69
|
+
if (opacity === 0) return null;
|
|
70
|
+
return {
|
|
71
|
+
kind: raw.kind,
|
|
72
|
+
color,
|
|
73
|
+
size: Math.round(clampNumber(raw.size, CONTAINER_PATTERN_SIZE)),
|
|
74
|
+
opacity,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
79
|
+
|
|
80
|
+
// The grain tile: a fixed speckle on a GRAIN_CELLS square grid, seeded so every
|
|
81
|
+
// host and every render draws the same noise. Not feTurbulence: native has no
|
|
82
|
+
// filter primitives, and a computed tile is identical on both hosts.
|
|
83
|
+
const GRAIN_CELLS = 48;
|
|
84
|
+
const GRAIN_LEVELS = [0.35, 0.6, 1];
|
|
85
|
+
const GRAIN_SPECKS = (() => {
|
|
86
|
+
let seed = 0x14027;
|
|
87
|
+
const next = () => {
|
|
88
|
+
seed = (seed + 0x6d2b79f5) | 0;
|
|
89
|
+
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
|
90
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
91
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
92
|
+
};
|
|
93
|
+
const byLevel = GRAIN_LEVELS.map(() => []);
|
|
94
|
+
for (let y = 0; y < GRAIN_CELLS; y += 1) {
|
|
95
|
+
for (let x = 0; x < GRAIN_CELLS; x += 1) {
|
|
96
|
+
if (next() < 0.1) byLevel[Math.floor(next() * GRAIN_LEVELS.length)].push([x, y]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return byLevel;
|
|
100
|
+
})();
|
|
101
|
+
|
|
102
|
+
function grainTile(size) {
|
|
103
|
+
const cell = size / CONTAINER_PATTERN_SIZE.default;
|
|
104
|
+
const c = round2(cell);
|
|
105
|
+
return {
|
|
106
|
+
size: round2(GRAIN_CELLS * cell),
|
|
107
|
+
paths: GRAIN_SPECKS.map((specks, i) => ({
|
|
108
|
+
d: specks.map(([x, y]) => `M${round2(x * cell)} ${round2(y * cell)}h${c}v${c}h-${c}z`).join(""),
|
|
109
|
+
fill: true,
|
|
110
|
+
alpha: GRAIN_LEVELS[i],
|
|
111
|
+
})),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Tiles depend only on kind and the clamped size, so the cache stays small.
|
|
116
|
+
const TILE_CACHE = new Map();
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The repeating tile for a resolved pattern, as plain SVG path data both hosts
|
|
120
|
+
* draw: the web as an SVG string, native as react-native-svg `<Path>`s.
|
|
121
|
+
*
|
|
122
|
+
* @param {{kind: string, size: number}} pattern output of {@link normaliseContainerPattern}
|
|
123
|
+
* @returns {{size: number, paths: Array<{d: string, fill?: boolean, strokeWidth?: number, alpha?: number}>}}
|
|
124
|
+
* shared and cached: do not mutate it.
|
|
125
|
+
*/
|
|
126
|
+
function containerPatternTile(pattern) {
|
|
127
|
+
const key = `${pattern.kind}:${pattern.size}`;
|
|
128
|
+
if (!TILE_CACHE.has(key)) TILE_CACHE.set(key, buildTile(pattern.kind, pattern.size));
|
|
129
|
+
return TILE_CACHE.get(key);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildTile(kind, s) {
|
|
133
|
+
if (kind === "grain") return grainTile(s);
|
|
134
|
+
if (kind === "dots") {
|
|
135
|
+
const r = round2(Math.max(1, s / 8));
|
|
136
|
+
const c = s / 2;
|
|
137
|
+
return {
|
|
138
|
+
size: s,
|
|
139
|
+
paths: [{ d: `M${round2(c - r)} ${c}a${r} ${r} 0 1 0 ${round2(r * 2)} 0a${r} ${r} 0 1 0 -${round2(r * 2)} 0z`, fill: true }],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (kind === "grid") {
|
|
143
|
+
return { size: s, paths: [{ d: `M0 0.5H${s}M0.5 0V${s}`, strokeWidth: 1 }] };
|
|
144
|
+
}
|
|
145
|
+
if (kind === "lines") {
|
|
146
|
+
return { size: s, paths: [{ d: `M0 0.5H${s}`, strokeWidth: 1 }] };
|
|
147
|
+
}
|
|
148
|
+
// stripes: a 45° hatch. The two corner segments complete the stripe the
|
|
149
|
+
// neighbouring tiles cut, so the hatch runs unbroken across tile edges.
|
|
150
|
+
const h = s / 2;
|
|
151
|
+
return {
|
|
152
|
+
size: s,
|
|
153
|
+
paths: [
|
|
154
|
+
{
|
|
155
|
+
d: `M0 ${s}L${s} 0M-${h} ${h}L${h} -${h}M${h} ${s + h}L${s + h} ${h}`,
|
|
156
|
+
strokeWidth: round2(Math.max(1, s / 4)),
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The tile as standalone SVG markup, for the web host's repeating CSS
|
|
164
|
+
* background. The colour is hex-validated by {@link normaliseContainerPattern}
|
|
165
|
+
* and the path data is numeric, so nothing authored reaches the markup raw.
|
|
166
|
+
*
|
|
167
|
+
* @param {{kind: string, color: string, size: number}} pattern
|
|
168
|
+
* @returns {string}
|
|
169
|
+
*/
|
|
170
|
+
function containerPatternSvg(pattern) {
|
|
171
|
+
const tile = containerPatternTile(pattern);
|
|
172
|
+
const paths = tile.paths
|
|
173
|
+
.map((p) => {
|
|
174
|
+
const paint = p.fill
|
|
175
|
+
? `fill="${pattern.color}"`
|
|
176
|
+
: `fill="none" stroke="${pattern.color}" stroke-width="${p.strokeWidth}"`;
|
|
177
|
+
const alpha = p.alpha === undefined ? "" : ` opacity="${p.alpha}"`;
|
|
178
|
+
return `<path d="${p.d}" ${paint}${alpha}/>`;
|
|
179
|
+
})
|
|
180
|
+
.join("");
|
|
181
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${tile.size}" height="${tile.size}">${paths}</svg>`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export {
|
|
185
|
+
CONTAINER_PATTERN_KINDS,
|
|
186
|
+
CONTAINER_PATTERN_SIZE,
|
|
187
|
+
CONTAINER_PATTERN_OPACITY,
|
|
188
|
+
normaliseContainerPattern,
|
|
189
|
+
containerPatternTile,
|
|
190
|
+
containerPatternSvg,
|
|
191
|
+
};
|
package/dist/host.d.ts
CHANGED
|
@@ -712,3 +712,33 @@ export const DARK_PALETTE_KEYS: readonly string[];
|
|
|
712
712
|
* appearance setting stores it: the base light, `dark` the dark palette.
|
|
713
713
|
*/
|
|
714
714
|
export function themeInBothModes<T>(theme: T, mode: AppearanceMode, twin: unknown): T;
|
|
715
|
+
|
|
716
|
+
/** A container background pattern, resolved for painting. `opacity` is 0-1. */
|
|
717
|
+
export interface ContainerPattern {
|
|
718
|
+
kind: "dots" | "grid" | "lines" | "stripes" | "grain";
|
|
719
|
+
color: string;
|
|
720
|
+
size: number;
|
|
721
|
+
opacity: number;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** One path of a pattern tile: filled, or stroked at `strokeWidth`. */
|
|
725
|
+
export interface ContainerPatternPath {
|
|
726
|
+
d: string;
|
|
727
|
+
fill?: boolean;
|
|
728
|
+
strokeWidth?: number;
|
|
729
|
+
alpha?: number;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export const CONTAINER_PATTERN_KINDS: readonly ContainerPattern["kind"][];
|
|
733
|
+
export const CONTAINER_PATTERN_SIZE: { readonly min: number; readonly max: number; readonly default: number };
|
|
734
|
+
export const CONTAINER_PATTERN_OPACITY: { readonly min: number; readonly max: number; readonly default: number };
|
|
735
|
+
/** A container's `style.backgroundPattern`, or null when it names no known kind. */
|
|
736
|
+
export function normaliseContainerPattern(
|
|
737
|
+
raw: unknown,
|
|
738
|
+
fallbackColor?: string,
|
|
739
|
+
options?: { strict?: boolean },
|
|
740
|
+
): ContainerPattern | null;
|
|
741
|
+
/** The repeating tile both hosts draw. */
|
|
742
|
+
export function containerPatternTile(pattern: ContainerPattern): { size: number; paths: ContainerPatternPath[] };
|
|
743
|
+
/** The tile as standalone SVG markup, for a CSS background. */
|
|
744
|
+
export function containerPatternSvg(pattern: ContainerPattern): string;
|
package/dist/host.js
CHANGED
|
@@ -173,3 +173,14 @@ export {
|
|
|
173
173
|
dateTimeLocalInputToIso,
|
|
174
174
|
formatDisplayLabel as formatDateTimeLabel,
|
|
175
175
|
} from "./datetimepicker-format.js";
|
|
176
|
+
|
|
177
|
+
// sc-14027: a container's background pattern — vocabulary and tile geometry,
|
|
178
|
+
// so the Player's CSS tile and the export's <Pattern> draw the same thing.
|
|
179
|
+
export {
|
|
180
|
+
CONTAINER_PATTERN_KINDS,
|
|
181
|
+
CONTAINER_PATTERN_SIZE,
|
|
182
|
+
CONTAINER_PATTERN_OPACITY,
|
|
183
|
+
normaliseContainerPattern,
|
|
184
|
+
containerPatternTile,
|
|
185
|
+
containerPatternSvg,
|
|
186
|
+
} from "./container-pattern.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.285.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|