@rogieking/figui3 6.20.2 → 6.21.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 +50 -1
- package/components.css +63 -2
- package/dist/components.css +1 -1
- package/dist/fig-editor.css +1 -1
- package/dist/fig-editor.js +115 -98
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +16 -16
- package/dist/fig.css +1 -1
- package/dist/fig.js +60 -43
- package/dist/propskit.css +1 -0
- package/dist/propskit.js +1 -0
- package/fig-editor.css +157 -17
- package/fig-editor.js +464 -188
- package/fig-lab.css +51 -42
- package/fig-lab.js +145 -23
- package/fig.js +577 -68
- package/package.json +33 -4
- package/propskit-core.js +556 -0
- package/propskit.d.ts +98 -0
- package/propskit.js +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rogieking/figui3",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.21.1",
|
|
4
4
|
"description": "A lightweight web components library for building Figma plugin and widget UIs with native look and feel",
|
|
5
5
|
"author": "Rogie King",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,6 +19,15 @@
|
|
|
19
19
|
"./fig-lab.css": "./dist/fig-lab.css",
|
|
20
20
|
"./base.css": "./dist/base.css",
|
|
21
21
|
"./components.css": "./dist/components.css",
|
|
22
|
+
"./propskit": {
|
|
23
|
+
"types": "./propskit.d.ts",
|
|
24
|
+
"default": "./dist/propskit.js"
|
|
25
|
+
},
|
|
26
|
+
"./propskit.js": {
|
|
27
|
+
"types": "./propskit.d.ts",
|
|
28
|
+
"default": "./dist/propskit.js"
|
|
29
|
+
},
|
|
30
|
+
"./propskit.css": "./dist/propskit.css",
|
|
22
31
|
"./src/fig.js": "./fig.js",
|
|
23
32
|
"./src/fig-layer.js": "./fig-layer.js",
|
|
24
33
|
"./src/fig-editor.js": "./fig-editor.js",
|
|
@@ -35,6 +44,9 @@
|
|
|
35
44
|
"fig-layer.js",
|
|
36
45
|
"fig-editor.js",
|
|
37
46
|
"fig-lab.js",
|
|
47
|
+
"propskit.js",
|
|
48
|
+
"propskit-core.js",
|
|
49
|
+
"propskit.d.ts",
|
|
38
50
|
"fig.css",
|
|
39
51
|
"fig-layer.css",
|
|
40
52
|
"fig-editor.css",
|
|
@@ -48,12 +60,18 @@
|
|
|
48
60
|
"LICENSE"
|
|
49
61
|
],
|
|
50
62
|
"sideEffects": [
|
|
51
|
-
"*.css"
|
|
63
|
+
"*.css",
|
|
64
|
+
"./propskit.js",
|
|
65
|
+
"./dist/propskit.js",
|
|
66
|
+
"./fig.js",
|
|
67
|
+
"./fig-lab.js",
|
|
68
|
+
"./fig-editor.js"
|
|
52
69
|
],
|
|
53
70
|
"scripts": {
|
|
54
71
|
"dev": "bun --hot server.ts",
|
|
55
|
-
"build": "bun build fig.js --minify --outdir dist && bun build fig-layer.js --minify --outdir dist && bun build fig-editor.js --minify --outdir dist --external ./fig.js && bun build fig-lab.js --minify --outdir dist --external ./fig.js --external ./fig-editor.js && npm run build:css",
|
|
72
|
+
"build": "bun build fig.js --minify --outdir dist && bun build fig-layer.js --minify --outdir dist && bun build fig-editor.js --minify --outdir dist --external ./fig.js --external ./fig-lab.js && bun build fig-lab.js --minify --outdir dist --external ./fig.js --external ./fig-editor.js && bun build propskit.js --minify --outdir dist --external ./fig.js --external ./fig-lab.js --external ./fig-editor.js && npm run build:css && npm run build:propskit-css",
|
|
56
73
|
"build:css": "node scripts/build-css.mjs",
|
|
74
|
+
"build:propskit-css": "node scripts/build-propskit-css.mjs",
|
|
57
75
|
"dev:playground": "node playground/dev.mjs",
|
|
58
76
|
"build:playground": "cd playground && npm run build",
|
|
59
77
|
"test": "npm run test:components",
|
|
@@ -107,6 +125,17 @@
|
|
|
107
125
|
"playwright": "^1.58.2"
|
|
108
126
|
},
|
|
109
127
|
"peerDependencies": {
|
|
110
|
-
"typescript": "^5.0.0"
|
|
128
|
+
"typescript": "^5.0.0",
|
|
129
|
+
"react": ">=18",
|
|
130
|
+
"vue": ">=3.3",
|
|
131
|
+
"svelte": ">=5",
|
|
132
|
+
"solid-js": ">=1.8"
|
|
133
|
+
},
|
|
134
|
+
"peerDependenciesMeta": {
|
|
135
|
+
"react": { "optional": true },
|
|
136
|
+
"vue": { "optional": true },
|
|
137
|
+
"svelte": { "optional": true },
|
|
138
|
+
"solid-js": { "optional": true },
|
|
139
|
+
"typescript": { "optional": true }
|
|
111
140
|
}
|
|
112
141
|
}
|
package/propskit-core.js
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PropsKit core — config store, theme, and DOM renderer.
|
|
3
|
+
* Framework-agnostic. Bundled into propskit.js.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const PROPSKIT_SCOPE_ROOT_CLASS = "figui-root";
|
|
7
|
+
export const PROPSKIT_OVERLAY_ROOT_ATTR = "data-figui-overlay-root";
|
|
8
|
+
|
|
9
|
+
/** @typedef {'system' | 'light' | 'dark'} PropsKitTheme */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {Element} el
|
|
13
|
+
* @param {PropsKitTheme} [theme]
|
|
14
|
+
*/
|
|
15
|
+
export function applyFiguiTheme(el, theme = "system") {
|
|
16
|
+
if (!el) return;
|
|
17
|
+
const normalized =
|
|
18
|
+
theme === "light" || theme === "dark" || theme === "system"
|
|
19
|
+
? theme
|
|
20
|
+
: "system";
|
|
21
|
+
el.setAttribute("theme", normalized);
|
|
22
|
+
el.classList.add(PROPSKIT_SCOPE_ROOT_CLASS);
|
|
23
|
+
el.classList.toggle("figma-light", normalized === "light");
|
|
24
|
+
el.classList.toggle("figma-dark", normalized === "dark");
|
|
25
|
+
if (normalized === "system") {
|
|
26
|
+
el.style.setProperty("color-scheme", "light dark");
|
|
27
|
+
} else {
|
|
28
|
+
el.style.setProperty("color-scheme", normalized);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Sync theme from a panel root onto the document overlay portal.
|
|
34
|
+
* @param {Element | null} [source]
|
|
35
|
+
*/
|
|
36
|
+
export function syncOverlayTheme(source) {
|
|
37
|
+
if (typeof document === "undefined" || !document.body) return null;
|
|
38
|
+
const attr = PROPSKIT_OVERLAY_ROOT_ATTR;
|
|
39
|
+
let root = document.body.querySelector(`:scope > [${attr}]`);
|
|
40
|
+
if (!root) {
|
|
41
|
+
root = document.createElement("div");
|
|
42
|
+
root.setAttribute(attr, "");
|
|
43
|
+
document.body.append(root);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const panel =
|
|
47
|
+
source?.closest?.(`.${PROPSKIT_SCOPE_ROOT_CLASS}`) ??
|
|
48
|
+
document.querySelector(`.${PROPSKIT_SCOPE_ROOT_CLASS}`);
|
|
49
|
+
|
|
50
|
+
if (!panel) return root;
|
|
51
|
+
|
|
52
|
+
const themeAttr = panel.getAttribute("theme");
|
|
53
|
+
const theme =
|
|
54
|
+
themeAttr === "light" || themeAttr === "dark" || themeAttr === "system"
|
|
55
|
+
? themeAttr
|
|
56
|
+
: panel.classList.contains("figma-dark")
|
|
57
|
+
? "dark"
|
|
58
|
+
: panel.classList.contains("figma-light")
|
|
59
|
+
? "light"
|
|
60
|
+
: "system";
|
|
61
|
+
|
|
62
|
+
applyFiguiTheme(root, theme);
|
|
63
|
+
// Overlay root is not the panel; keep portal class semantics without forcing layout.
|
|
64
|
+
root.classList.remove(PROPSKIT_SCOPE_ROOT_CLASS);
|
|
65
|
+
root.setAttribute(attr, "");
|
|
66
|
+
if (theme === "light") root.classList.add("figma-light");
|
|
67
|
+
if (theme === "dark") root.classList.add("figma-dark");
|
|
68
|
+
return root;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isPlainObject(value) {
|
|
72
|
+
return (
|
|
73
|
+
value !== null &&
|
|
74
|
+
typeof value === "object" &&
|
|
75
|
+
!Array.isArray(value) &&
|
|
76
|
+
Object.getPrototypeOf(value) === Object.prototype
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isHexColor(value) {
|
|
81
|
+
return typeof value === "string" && /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {string} key
|
|
86
|
+
* @param {unknown} value
|
|
87
|
+
* @returns {{ kind: string, key: string, label: string, [k: string]: unknown }}
|
|
88
|
+
*/
|
|
89
|
+
export function inferControl(key, value) {
|
|
90
|
+
const label = key
|
|
91
|
+
.replace(/([A-Z])/g, " $1")
|
|
92
|
+
.replace(/[_-]+/g, " ")
|
|
93
|
+
.replace(/^\w/, (c) => c.toUpperCase())
|
|
94
|
+
.trim();
|
|
95
|
+
|
|
96
|
+
if (Array.isArray(value) && value.length >= 3 && value.every((n) => typeof n === "number")) {
|
|
97
|
+
const [def, min, max, step] = value;
|
|
98
|
+
return {
|
|
99
|
+
kind: "slider",
|
|
100
|
+
key,
|
|
101
|
+
label,
|
|
102
|
+
default: def,
|
|
103
|
+
min,
|
|
104
|
+
max,
|
|
105
|
+
step: step ?? (max - min <= 1 ? 0.01 : 1),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (isPlainObject(value) && typeof value.type === "string") {
|
|
110
|
+
const type = value.type;
|
|
111
|
+
if (type === "select") {
|
|
112
|
+
return {
|
|
113
|
+
kind: "select",
|
|
114
|
+
key,
|
|
115
|
+
label,
|
|
116
|
+
options: value.options ?? [],
|
|
117
|
+
default: value.default ?? value.options?.[0]?.value ?? value.options?.[0] ?? "",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (type === "color") {
|
|
121
|
+
return { kind: "color", key, label, default: value.default ?? "#000000" };
|
|
122
|
+
}
|
|
123
|
+
if (type === "text") {
|
|
124
|
+
return {
|
|
125
|
+
kind: "text",
|
|
126
|
+
key,
|
|
127
|
+
label,
|
|
128
|
+
default: value.default ?? "",
|
|
129
|
+
placeholder: value.placeholder,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (type === "easing" || type === "spring") {
|
|
133
|
+
return {
|
|
134
|
+
kind: "easing",
|
|
135
|
+
key,
|
|
136
|
+
label,
|
|
137
|
+
default: value.value ?? value.ease ?? value.default ?? [0.4, 0, 0.2, 1],
|
|
138
|
+
spring: type === "spring" ? value : undefined,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (type === "action") {
|
|
142
|
+
return { kind: "action", key, label };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (isPlainObject(value)) {
|
|
147
|
+
const { _collapsed, ...rest } = value;
|
|
148
|
+
return {
|
|
149
|
+
kind: "folder",
|
|
150
|
+
key,
|
|
151
|
+
label,
|
|
152
|
+
collapsed: Boolean(_collapsed),
|
|
153
|
+
children: Object.entries(rest).map(([k, v]) => inferControl(k, v)),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (typeof value === "boolean") {
|
|
158
|
+
return { kind: "switch", key, label, default: value };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (typeof value === "number") {
|
|
162
|
+
const abs = Math.abs(value) || 1;
|
|
163
|
+
const max = abs <= 1 ? 1 : abs * 2;
|
|
164
|
+
const min = abs <= 1 ? 0 : 0;
|
|
165
|
+
return {
|
|
166
|
+
kind: "slider",
|
|
167
|
+
key,
|
|
168
|
+
label,
|
|
169
|
+
default: value,
|
|
170
|
+
min,
|
|
171
|
+
max,
|
|
172
|
+
step: abs <= 1 ? 0.01 : 1,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (isHexColor(value)) {
|
|
177
|
+
return { kind: "color", key, label, default: value };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (typeof value === "string") {
|
|
181
|
+
return { kind: "text", key, label, default: value };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { kind: "text", key, label, default: String(value ?? "") };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @param {Record<string, unknown>} config
|
|
189
|
+
*/
|
|
190
|
+
export function parseConfig(config) {
|
|
191
|
+
return Object.entries(config ?? {}).map(([key, value]) => inferControl(key, value));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function optionValue(option) {
|
|
195
|
+
if (option && typeof option === "object") return String(option.value ?? "");
|
|
196
|
+
return String(option ?? "");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function optionLabel(option) {
|
|
200
|
+
if (option && typeof option === "object") {
|
|
201
|
+
return String(option.label ?? option.value ?? "");
|
|
202
|
+
}
|
|
203
|
+
return String(option ?? "");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function getPath(obj, path) {
|
|
207
|
+
return path.split(".").reduce((acc, key) => (acc == null ? undefined : acc[key]), obj);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function setPath(obj, path, value) {
|
|
211
|
+
const parts = path.split(".");
|
|
212
|
+
let cursor = obj;
|
|
213
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
214
|
+
const key = parts[i];
|
|
215
|
+
if (!isPlainObject(cursor[key])) cursor[key] = {};
|
|
216
|
+
cursor = cursor[key];
|
|
217
|
+
}
|
|
218
|
+
cursor[parts[parts.length - 1]] = value;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function defaultsFromDescriptors(descriptors, target = {}) {
|
|
222
|
+
for (const d of descriptors) {
|
|
223
|
+
if (d.kind === "folder") {
|
|
224
|
+
target[d.key] = {};
|
|
225
|
+
defaultsFromDescriptors(d.children, target[d.key]);
|
|
226
|
+
} else if (d.kind !== "action") {
|
|
227
|
+
target[d.key] = d.default;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return target;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function cloneValues(values) {
|
|
234
|
+
return structuredClone(values);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @param {ReturnType<typeof inferControl>} descriptor
|
|
239
|
+
* @param {string} path
|
|
240
|
+
* @param {Record<string, unknown>} values
|
|
241
|
+
* @param {(type: string, path: string, value?: unknown) => void} emit
|
|
242
|
+
*/
|
|
243
|
+
function renderControl(descriptor, path, values, emit) {
|
|
244
|
+
if (descriptor.kind === "folder") {
|
|
245
|
+
const group = document.createElement("propskit-group");
|
|
246
|
+
group.setAttribute("name", descriptor.label);
|
|
247
|
+
if (!descriptor.collapsed) group.setAttribute("open", "true");
|
|
248
|
+
else group.setAttribute("open", "false");
|
|
249
|
+
for (const child of descriptor.children) {
|
|
250
|
+
group.append(
|
|
251
|
+
renderControl(child, path ? `${path}.${child.key}` : child.key, values, emit),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return group;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (descriptor.kind === "action") {
|
|
258
|
+
const button = document.createElement("fig-button");
|
|
259
|
+
button.textContent = descriptor.label;
|
|
260
|
+
button.addEventListener("click", () => emit("action", path || descriptor.key));
|
|
261
|
+
return button;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (descriptor.kind === "slider") {
|
|
265
|
+
const el = document.createElement("propskit-slider");
|
|
266
|
+
el.setAttribute("label", descriptor.label);
|
|
267
|
+
el.setAttribute("min", String(descriptor.min));
|
|
268
|
+
el.setAttribute("max", String(descriptor.max));
|
|
269
|
+
el.setAttribute("step", String(descriptor.step));
|
|
270
|
+
el.setAttribute("value", String(getPath(values, path) ?? descriptor.default));
|
|
271
|
+
el.setAttribute("full", "");
|
|
272
|
+
const forward = (event) => {
|
|
273
|
+
const detail =
|
|
274
|
+
event instanceof CustomEvent && event.detail !== undefined
|
|
275
|
+
? event.detail
|
|
276
|
+
: el.value;
|
|
277
|
+
const next = typeof detail === "number" ? detail : Number(detail);
|
|
278
|
+
emit("change", path, Number.isFinite(next) ? next : detail);
|
|
279
|
+
};
|
|
280
|
+
el.addEventListener("input", forward);
|
|
281
|
+
el.addEventListener("change", forward);
|
|
282
|
+
return el;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (descriptor.kind === "switch") {
|
|
286
|
+
const el = document.createElement("propskit-switch");
|
|
287
|
+
el.setAttribute("label", descriptor.label);
|
|
288
|
+
if (getPath(values, path) ?? descriptor.default) el.setAttribute("checked", "");
|
|
289
|
+
const forward = (event) => {
|
|
290
|
+
const detail =
|
|
291
|
+
event instanceof CustomEvent && event.detail !== undefined
|
|
292
|
+
? event.detail
|
|
293
|
+
: null;
|
|
294
|
+
const checked =
|
|
295
|
+
detail && typeof detail === "object" && "checked" in detail
|
|
296
|
+
? Boolean(detail.checked)
|
|
297
|
+
: el.hasAttribute("checked");
|
|
298
|
+
emit("change", path, checked);
|
|
299
|
+
};
|
|
300
|
+
el.addEventListener("input", forward);
|
|
301
|
+
el.addEventListener("change", forward);
|
|
302
|
+
return el;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (descriptor.kind === "color") {
|
|
306
|
+
const el = document.createElement("propskit-color");
|
|
307
|
+
el.setAttribute("label", descriptor.label);
|
|
308
|
+
el.setAttribute("value", String(getPath(values, path) ?? descriptor.default));
|
|
309
|
+
const forward = (event) => {
|
|
310
|
+
const detail =
|
|
311
|
+
event instanceof CustomEvent && event.detail !== undefined
|
|
312
|
+
? event.detail
|
|
313
|
+
: el.value;
|
|
314
|
+
emit("change", path, detail);
|
|
315
|
+
};
|
|
316
|
+
el.addEventListener("input", forward);
|
|
317
|
+
el.addEventListener("change", forward);
|
|
318
|
+
return el;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (descriptor.kind === "text") {
|
|
322
|
+
const el = document.createElement("propskit-text");
|
|
323
|
+
el.setAttribute("label", descriptor.label);
|
|
324
|
+
el.setAttribute("value", String(getPath(values, path) ?? descriptor.default ?? ""));
|
|
325
|
+
if (descriptor.placeholder) el.setAttribute("placeholder", descriptor.placeholder);
|
|
326
|
+
const forward = (event) => {
|
|
327
|
+
const detail =
|
|
328
|
+
event instanceof CustomEvent && event.detail !== undefined
|
|
329
|
+
? event.detail
|
|
330
|
+
: el.value;
|
|
331
|
+
emit("change", path, detail);
|
|
332
|
+
};
|
|
333
|
+
el.addEventListener("input", forward);
|
|
334
|
+
el.addEventListener("change", forward);
|
|
335
|
+
return el;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (descriptor.kind === "select") {
|
|
339
|
+
const el = document.createElement("propskit-select");
|
|
340
|
+
el.setAttribute("label", descriptor.label);
|
|
341
|
+
const options = (descriptor.options ?? [])
|
|
342
|
+
.map((opt) => optionLabel(opt))
|
|
343
|
+
.join(",");
|
|
344
|
+
el.setAttribute("options", options);
|
|
345
|
+
const current = getPath(values, path) ?? descriptor.default;
|
|
346
|
+
el.setAttribute("value", String(current ?? ""));
|
|
347
|
+
const forward = (event) => {
|
|
348
|
+
const detail =
|
|
349
|
+
event instanceof CustomEvent && event.detail !== undefined
|
|
350
|
+
? event.detail
|
|
351
|
+
: el.value;
|
|
352
|
+
const raw = typeof detail === "object" && detail && "value" in detail
|
|
353
|
+
? detail.value
|
|
354
|
+
: detail;
|
|
355
|
+
const match = (descriptor.options ?? []).find(
|
|
356
|
+
(opt) => optionLabel(opt) === String(raw) || optionValue(opt) === String(raw),
|
|
357
|
+
);
|
|
358
|
+
emit("change", path, match ? optionValue(match) || optionLabel(match) : raw);
|
|
359
|
+
};
|
|
360
|
+
el.addEventListener("input", forward);
|
|
361
|
+
el.addEventListener("change", forward);
|
|
362
|
+
return el;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (descriptor.kind === "easing") {
|
|
366
|
+
const field = document.createElement("fig-field");
|
|
367
|
+
field.setAttribute("direction", "horizontal");
|
|
368
|
+
const label = document.createElement("label");
|
|
369
|
+
label.textContent = descriptor.label;
|
|
370
|
+
const curve = document.createElement("fig-easing-curve");
|
|
371
|
+
const current = getPath(values, path) ?? descriptor.default;
|
|
372
|
+
if (Array.isArray(current)) {
|
|
373
|
+
curve.setAttribute("value", current.join(","));
|
|
374
|
+
}
|
|
375
|
+
field.append(label, curve);
|
|
376
|
+
const forward = (event) => {
|
|
377
|
+
const detail =
|
|
378
|
+
event instanceof CustomEvent && event.detail !== undefined
|
|
379
|
+
? event.detail
|
|
380
|
+
: null;
|
|
381
|
+
emit(
|
|
382
|
+
"change",
|
|
383
|
+
path,
|
|
384
|
+
detail && typeof detail === "object" && "value" in detail
|
|
385
|
+
? detail.value
|
|
386
|
+
: detail,
|
|
387
|
+
);
|
|
388
|
+
};
|
|
389
|
+
curve.addEventListener("input", forward);
|
|
390
|
+
curve.addEventListener("change", forward);
|
|
391
|
+
return field;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const fallback = document.createElement("propskit-text");
|
|
395
|
+
fallback.setAttribute("label", descriptor.label);
|
|
396
|
+
fallback.setAttribute("value", String(getPath(values, path) ?? ""));
|
|
397
|
+
return fallback;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* @param {ParentNode} target
|
|
402
|
+
* @param {string} name
|
|
403
|
+
* @param {Record<string, unknown>} config
|
|
404
|
+
* @param {{
|
|
405
|
+
* theme?: PropsKitTheme,
|
|
406
|
+
* onChange?: (path: string, value: unknown, values: Record<string, unknown>) => void,
|
|
407
|
+
* onAction?: (name: string) => void,
|
|
408
|
+
* scoped?: boolean,
|
|
409
|
+
* }} [options]
|
|
410
|
+
*/
|
|
411
|
+
export function createPropsKit(target, name, config, options = {}) {
|
|
412
|
+
if (
|
|
413
|
+
!target ||
|
|
414
|
+
(!(target instanceof Element) && !(target instanceof DocumentFragment))
|
|
415
|
+
) {
|
|
416
|
+
throw new Error("createPropsKit requires a mount element");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const mount =
|
|
420
|
+
target instanceof DocumentFragment
|
|
421
|
+
? (() => {
|
|
422
|
+
const el = document.createElement("div");
|
|
423
|
+
target.append(el);
|
|
424
|
+
return el;
|
|
425
|
+
})()
|
|
426
|
+
: target;
|
|
427
|
+
|
|
428
|
+
const theme = options.theme ?? "system";
|
|
429
|
+
const scoped = options.scoped !== false;
|
|
430
|
+
if (scoped) applyFiguiTheme(mount, theme);
|
|
431
|
+
else applyFiguiTheme(mount, theme);
|
|
432
|
+
|
|
433
|
+
const descriptors = parseConfig(config);
|
|
434
|
+
/** @type {Record<string, unknown>} */
|
|
435
|
+
let values = defaultsFromDescriptors(descriptors);
|
|
436
|
+
const listeners = new Set();
|
|
437
|
+
|
|
438
|
+
const panel = document.createElement("propskit-group");
|
|
439
|
+
if (name) {
|
|
440
|
+
panel.setAttribute("name", name);
|
|
441
|
+
panel.setAttribute("open", "true");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const emit = (type, path, value) => {
|
|
445
|
+
if (type === "action") {
|
|
446
|
+
options.onAction?.(path);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
setPath(values, path, value);
|
|
450
|
+
const snapshot = cloneValues(values);
|
|
451
|
+
options.onChange?.(path, value, snapshot);
|
|
452
|
+
for (const fn of listeners) fn(snapshot);
|
|
453
|
+
mount.dispatchEvent(
|
|
454
|
+
new CustomEvent("change", {
|
|
455
|
+
detail: { path, value, values: snapshot },
|
|
456
|
+
bubbles: true,
|
|
457
|
+
composed: true,
|
|
458
|
+
}),
|
|
459
|
+
);
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
for (const descriptor of descriptors) {
|
|
463
|
+
panel.append(renderControl(descriptor, descriptor.key, values, emit));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
mount.replaceChildren(panel);
|
|
467
|
+
syncOverlayTheme(mount);
|
|
468
|
+
|
|
469
|
+
const themeObserver = new MutationObserver(() => syncOverlayTheme(mount));
|
|
470
|
+
themeObserver.observe(mount, {
|
|
471
|
+
attributes: true,
|
|
472
|
+
attributeFilter: ["theme", "class"],
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
get values() {
|
|
477
|
+
return cloneValues(values);
|
|
478
|
+
},
|
|
479
|
+
get(path) {
|
|
480
|
+
return getPath(values, path);
|
|
481
|
+
},
|
|
482
|
+
set(path, value) {
|
|
483
|
+
setPath(values, path, value);
|
|
484
|
+
const snapshot = cloneValues(values);
|
|
485
|
+
options.onChange?.(path, value, snapshot);
|
|
486
|
+
for (const fn of listeners) fn(snapshot);
|
|
487
|
+
// Re-render for simplicity
|
|
488
|
+
mount.replaceChildren();
|
|
489
|
+
const nextPanel = document.createElement("propskit-group");
|
|
490
|
+
if (name) {
|
|
491
|
+
nextPanel.setAttribute("name", name);
|
|
492
|
+
nextPanel.setAttribute("open", "true");
|
|
493
|
+
}
|
|
494
|
+
for (const descriptor of descriptors) {
|
|
495
|
+
nextPanel.append(renderControl(descriptor, descriptor.key, values, emit));
|
|
496
|
+
}
|
|
497
|
+
mount.append(nextPanel);
|
|
498
|
+
},
|
|
499
|
+
subscribe(fn) {
|
|
500
|
+
listeners.add(fn);
|
|
501
|
+
return () => listeners.delete(fn);
|
|
502
|
+
},
|
|
503
|
+
destroy() {
|
|
504
|
+
themeObserver.disconnect();
|
|
505
|
+
listeners.clear();
|
|
506
|
+
mount.replaceChildren();
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Panel shell: marks `.figui-root` and honors `theme`. */
|
|
512
|
+
class FigPanel extends HTMLElement {
|
|
513
|
+
static get observedAttributes() {
|
|
514
|
+
return ["theme"];
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
connectedCallback() {
|
|
518
|
+
this.#apply();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
attributeChangedCallback() {
|
|
522
|
+
this.#apply();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
#apply() {
|
|
526
|
+
const theme = this.getAttribute("theme") || "system";
|
|
527
|
+
applyFiguiTheme(this, /** @type {PropsKitTheme} */ (theme));
|
|
528
|
+
syncOverlayTheme(this);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (typeof customElements !== "undefined" && !customElements.get("fig-panel")) {
|
|
533
|
+
customElements.define("fig-panel", FigPanel);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Auto-sync overlay theme when interacting inside a scoped root.
|
|
537
|
+
if (typeof document !== "undefined") {
|
|
538
|
+
document.addEventListener(
|
|
539
|
+
"pointerdown",
|
|
540
|
+
(event) => {
|
|
541
|
+
const t = event.target;
|
|
542
|
+
if (!(t instanceof Element)) return;
|
|
543
|
+
if (t.closest(`.${PROPSKIT_SCOPE_ROOT_CLASS}`)) syncOverlayTheme(t);
|
|
544
|
+
},
|
|
545
|
+
true,
|
|
546
|
+
);
|
|
547
|
+
document.addEventListener(
|
|
548
|
+
"focusin",
|
|
549
|
+
(event) => {
|
|
550
|
+
const t = event.target;
|
|
551
|
+
if (!(t instanceof Element)) return;
|
|
552
|
+
if (t.closest(`.${PROPSKIT_SCOPE_ROOT_CLASS}`)) syncOverlayTheme(t);
|
|
553
|
+
},
|
|
554
|
+
true,
|
|
555
|
+
);
|
|
556
|
+
}
|