@rogieking/figui3 6.22.0 → 6.23.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/dist/propskit.js DELETED
@@ -1 +0,0 @@
1
- import"./fig-editor.js";var P="figui-root",w="data-figui-overlay-root";function L(R,S="system"){if(!R)return;let K=S==="light"||S==="dark"||S==="system"?S:"system";if(R.setAttribute("theme",K),R.classList.add("figui-root"),R.classList.toggle("figma-light",K==="light"),R.classList.toggle("figma-dark",K==="dark"),K==="system")R.style.setProperty("color-scheme","light dark");else R.style.setProperty("color-scheme",K)}function H(R){if(typeof document>"u"||!document.body)return null;let S="data-figui-overlay-root",K=document.body.querySelector(`:scope > [${S}]`);if(!K)K=document.createElement("div"),K.setAttribute(S,""),document.body.append(K);let E=R?.closest?.(".figui-root")??document.querySelector(".figui-root");if(!E)return K;let I=E.getAttribute("theme"),_=I==="light"||I==="dark"||I==="system"?I:E.classList.contains("figma-dark")?"dark":E.classList.contains("figma-light")?"light":"system";if(L(K,_),K.classList.remove("figui-root"),K.setAttribute(S,""),_==="light")K.classList.add("figma-light");if(_==="dark")K.classList.add("figma-dark");return K}function A(R){return R!==null&&typeof R==="object"&&!Array.isArray(R)&&Object.getPrototypeOf(R)===Object.prototype}function f(R){return typeof R==="string"&&/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(R)}function C(R,S){let K=R.replace(/([A-Z])/g," $1").replace(/[_-]+/g," ").replace(/^\w/,(E)=>E.toUpperCase()).trim();if(Array.isArray(S)&&S.length>=3&&S.every((E)=>typeof E==="number")){let[E,I,_,B]=S;return{kind:"slider",key:R,label:K,default:E,min:I,max:_,step:B??(_-I<=1?0.01:1)}}if(A(S)&&typeof S.type==="string"){let E=S.type;if(E==="select")return{kind:"select",key:R,label:K,options:S.options??[],default:S.default??S.options?.[0]?.value??S.options?.[0]??""};if(E==="color")return{kind:"color",key:R,label:K,default:S.default??"#000000"};if(E==="text")return{kind:"text",key:R,label:K,default:S.default??"",placeholder:S.placeholder};if(E==="easing"||E==="spring")return{kind:"easing",key:R,label:K,default:S.value??S.ease??S.default??[0.4,0,0.2,1],spring:E==="spring"?S:void 0};if(E==="action")return{kind:"action",key:R,label:K}}if(A(S)){let{_collapsed:E,...I}=S;return{kind:"folder",key:R,label:K,collapsed:Boolean(E),children:Object.entries(I).map(([_,B])=>C(_,B))}}if(typeof S==="boolean")return{kind:"switch",key:R,label:K,default:S};if(typeof S==="number"){let E=Math.abs(S)||1,I=E<=1?1:E*2,_=E<=1?0:0;return{kind:"slider",key:R,label:K,default:S,min:_,max:I,step:E<=1?0.01:1}}if(f(S))return{kind:"color",key:R,label:K,default:S};if(typeof S==="string")return{kind:"text",key:R,label:K,default:S};return{kind:"text",key:R,label:K,default:String(S??"")}}function j(R){return Object.entries(R??{}).map(([S,K])=>C(S,K))}function V(R){if(R&&typeof R==="object")return String(R.value??"");return String(R??"")}function O(R){if(R&&typeof R==="object")return String(R.label??R.value??"");return String(R??"")}function U(R,S){return S.split(".").reduce((K,E)=>K==null?void 0:K[E],R)}function z(R,S,K){let E=S.split("."),I=R;for(let _=0;_<E.length-1;_++){let B=E[_];if(!A(I[B]))I[B]={};I=I[B]}I[E[E.length-1]]=K}function F(R,S={}){for(let K of R)if(K.kind==="folder")S[K.key]={},F(K.children,S[K.key]);else if(K.kind!=="action")S[K.key]=K.default;return S}function T(R){return structuredClone(R)}function D(R,S,K,E){if(R.kind==="folder"){let _=document.createElement("propskit-group");if(_.setAttribute("name",R.label),!R.collapsed)_.setAttribute("open","true");else _.setAttribute("open","false");for(let B of R.children)_.append(D(B,S?`${S}.${B.key}`:B.key,K,E));return _}if(R.kind==="action"){let _=document.createElement("fig-button");return _.textContent=R.label,_.addEventListener("click",()=>E("action",S||R.key)),_}if(R.kind==="slider"){let _=document.createElement("propskit-slider");_.setAttribute("label",R.label),_.setAttribute("min",String(R.min)),_.setAttribute("max",String(R.max)),_.setAttribute("step",String(R.step)),_.setAttribute("value",String(U(K,S)??R.default)),_.setAttribute("full","");let B=(Y)=>{let J=Y instanceof CustomEvent&&Y.detail!==void 0?Y.detail:_.value,q=typeof J==="number"?J:Number(J);E("change",S,Number.isFinite(q)?q:J)};return _.addEventListener("input",B),_.addEventListener("change",B),_}if(R.kind==="switch"){let _=document.createElement("propskit-switch");if(_.setAttribute("label",R.label),U(K,S)??R.default)_.setAttribute("checked","");let B=(Y)=>{let J=Y instanceof CustomEvent&&Y.detail!==void 0?Y.detail:null,q=J&&typeof J==="object"&&"checked"in J?Boolean(J.checked):_.hasAttribute("checked");E("change",S,q)};return _.addEventListener("input",B),_.addEventListener("change",B),_}if(R.kind==="color"){let _=document.createElement("propskit-color");_.setAttribute("label",R.label),_.setAttribute("value",String(U(K,S)??R.default));let B=(Y)=>{let J=Y instanceof CustomEvent&&Y.detail!==void 0?Y.detail:_.value;E("change",S,J)};return _.addEventListener("input",B),_.addEventListener("change",B),_}if(R.kind==="text"){let _=document.createElement("propskit-text");if(_.setAttribute("label",R.label),_.setAttribute("value",String(U(K,S)??R.default??"")),R.placeholder)_.setAttribute("placeholder",R.placeholder);let B=(Y)=>{let J=Y instanceof CustomEvent&&Y.detail!==void 0?Y.detail:_.value;E("change",S,J)};return _.addEventListener("input",B),_.addEventListener("change",B),_}if(R.kind==="select"){let _=document.createElement("propskit-select");_.setAttribute("label",R.label);let B=(R.options??[]).map((q)=>O(q)).join(",");_.setAttribute("options",B);let Y=U(K,S)??R.default;_.setAttribute("value",String(Y??""));let J=(q)=>{let M=q instanceof CustomEvent&&q.detail!==void 0?q.detail:_.value,N=typeof M==="object"&&M&&"value"in M?M.value:M,X=(R.options??[]).find((G)=>O(G)===String(N)||V(G)===String(N));E("change",S,X?V(X)||O(X):N)};return _.addEventListener("input",J),_.addEventListener("change",J),_}if(R.kind==="easing"){let _=document.createElement("fig-field");_.setAttribute("direction","horizontal");let B=document.createElement("label");B.textContent=R.label;let Y=document.createElement("fig-easing-curve"),J=U(K,S)??R.default;if(Array.isArray(J))Y.setAttribute("value",J.join(","));_.append(B,Y);let q=(M)=>{let N=M instanceof CustomEvent&&M.detail!==void 0?M.detail:null;E("change",S,N&&typeof N==="object"&&"value"in N?N.value:N)};return Y.addEventListener("input",q),Y.addEventListener("change",q),_}let I=document.createElement("propskit-text");return I.setAttribute("label",R.label),I.setAttribute("value",String(U(K,S)??"")),I}function b(R,S,K,E={}){if(!R||!(R instanceof Element)&&!(R instanceof DocumentFragment))throw Error("createPropsKit requires a mount element");let I=R instanceof DocumentFragment?(()=>{let G=document.createElement("div");return R.append(G),G})():R,_=E.theme??"system";if(E.scoped!==!1)L(I,_);else L(I,_);let Y=j(K),J=F(Y),q=new Set,M=document.createElement("propskit-group");if(S)M.setAttribute("name",S),M.setAttribute("open","true");let N=(G,W,Z)=>{if(G==="action"){E.onAction?.(W);return}z(J,W,Z);let Q=T(J);E.onChange?.(W,Z,Q);for(let $ of q)$(Q);I.dispatchEvent(new CustomEvent("change",{detail:{path:W,value:Z,values:Q},bubbles:!0,composed:!0}))};for(let G of Y)M.append(D(G,G.key,J,N));I.replaceChildren(M),H(I);let X=new MutationObserver(()=>H(I));return X.observe(I,{attributes:!0,attributeFilter:["theme","class"]}),{get values(){return T(J)},get(G){return U(J,G)},set(G,W){z(J,G,W);let Z=T(J);E.onChange?.(G,W,Z);for(let $ of q)$(Z);I.replaceChildren();let Q=document.createElement("propskit-group");if(S)Q.setAttribute("name",S),Q.setAttribute("open","true");for(let $ of Y)Q.append(D($,$.key,J,N));I.append(Q)},subscribe(G){return q.add(G),()=>q.delete(G)},destroy(){X.disconnect(),q.clear(),I.replaceChildren()}}}class x extends HTMLElement{static get observedAttributes(){return["theme"]}connectedCallback(){this.#R()}attributeChangedCallback(){this.#R()}#R(){let R=this.getAttribute("theme")||"system";L(this,R),H(this)}}if(typeof customElements<"u"&&!customElements.get("fig-panel"))customElements.define("fig-panel",x);if(typeof document<"u")document.addEventListener("pointerdown",(R)=>{let S=R.target;if(!(S instanceof Element))return;if(S.closest(".figui-root"))H(S)},!0),document.addEventListener("focusin",(R)=>{let S=R.target;if(!(S instanceof Element))return;if(S.closest(".figui-root"))H(S)},!0);export{H as syncOverlayTheme,j as parseConfig,C as inferControl,b as createPropsKit,L as applyFiguiTheme,P as PROPSKIT_SCOPE_ROOT_CLASS,w as PROPSKIT_OVERLAY_ROOT_ATTR};
package/propskit-core.js DELETED
@@ -1,556 +0,0 @@
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
- }
package/propskit.d.ts DELETED
@@ -1,98 +0,0 @@
1
- export type PropsKitTheme = "system" | "light" | "dark";
2
-
3
- export type PropsKitSliderTuple = [number, number, number, number?];
4
-
5
- export type PropsKitSelectConfig = {
6
- type: "select";
7
- options: Array<string | { value: string; label?: string }>;
8
- default?: string;
9
- };
10
-
11
- export type PropsKitColorConfig = {
12
- type: "color";
13
- default: string;
14
- };
15
-
16
- export type PropsKitTextConfig = {
17
- type: "text";
18
- default?: string;
19
- placeholder?: string;
20
- };
21
-
22
- export type PropsKitEasingConfig = {
23
- type: "easing" | "spring";
24
- value?: number[];
25
- ease?: number[];
26
- default?: number[];
27
- [key: string]: unknown;
28
- };
29
-
30
- export type PropsKitActionConfig = {
31
- type: "action";
32
- };
33
-
34
- export type PropsKitFolderConfig = {
35
- _collapsed?: boolean;
36
- [key: string]: PropsKitConfigValue;
37
- };
38
-
39
- export type PropsKitConfigValue =
40
- | number
41
- | boolean
42
- | string
43
- | PropsKitSliderTuple
44
- | PropsKitSelectConfig
45
- | PropsKitColorConfig
46
- | PropsKitTextConfig
47
- | PropsKitEasingConfig
48
- | PropsKitActionConfig
49
- | PropsKitFolderConfig;
50
-
51
- export type PropsKitConfig = Record<string, PropsKitConfigValue>;
52
-
53
- export type PropsKitOptions = {
54
- theme?: PropsKitTheme;
55
- onChange?: (
56
- path: string,
57
- value: unknown,
58
- values: Record<string, unknown>,
59
- ) => void;
60
- onAction?: (name: string) => void;
61
- scoped?: boolean;
62
- };
63
-
64
- export type PropsKitInstance = {
65
- readonly values: Record<string, unknown>;
66
- get(path: string): unknown;
67
- set(path: string, value: unknown): void;
68
- subscribe(fn: (values: Record<string, unknown>) => void): () => void;
69
- destroy(): void;
70
- };
71
-
72
- export declare const PROPSKIT_SCOPE_ROOT_CLASS: "figui-root";
73
- export declare const PROPSKIT_OVERLAY_ROOT_ATTR: "data-figui-overlay-root";
74
-
75
- export declare function applyFiguiTheme(
76
- el: Element,
77
- theme?: PropsKitTheme,
78
- ): void;
79
-
80
- export declare function syncOverlayTheme(source?: Element | null): Element | null;
81
-
82
- export declare function inferControl(
83
- key: string,
84
- value: unknown,
85
- ): Record<string, unknown>;
86
-
87
- export declare function parseConfig(
88
- config: PropsKitConfig,
89
- ): Array<Record<string, unknown>>;
90
-
91
- export declare function createPropsKit(
92
- target: Element | DocumentFragment,
93
- name: string,
94
- config: PropsKitConfig,
95
- options?: PropsKitOptions,
96
- ): PropsKitInstance;
97
-
98
- export as namespace PropsKit;
package/propskit.js DELETED
@@ -1,15 +0,0 @@
1
- /**
2
- * PropsKit entry — one JS file for consumers.
3
- * Bundles fig + fig-lab + fig-editor + propskit-core.
4
- */
5
- import "./fig-editor.js";
6
-
7
- export {
8
- PROPSKIT_SCOPE_ROOT_CLASS,
9
- PROPSKIT_OVERLAY_ROOT_ATTR,
10
- applyFiguiTheme,
11
- syncOverlayTheme,
12
- inferControl,
13
- parseConfig,
14
- createPropsKit,
15
- } from "./propskit-core.js";