@rogieking/figui3 11.5.0 → 11.6.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/.cursor/skills/fig-editor/SKILL.md +2 -1
- package/.cursor/skills/fig-editor/components.md +87 -0
- package/.cursor/skills/fig-editor/reference.md +37 -0
- package/.cursor/skills/figui3/components.md +6 -1
- package/.cursor/skills/figui3/react.md +1 -0
- package/CHANGELOG.md +28 -0
- package/README.md +84 -4
- package/components.css +30 -1
- package/dist/components.css +1 -1
- package/dist/fig-editor.css +1 -1
- package/dist/fig-editor.js +3 -3
- package/dist/fig.css +1 -1
- package/dist/fig.js +6 -6
- package/fig-editor.css +4 -168
- package/fig-editor.js +46 -7
- package/fig-gradient-editor.css +76 -0
- package/fig-gradient-editor.js +598 -0
- package/fig-popup-easing.css +8 -0
- package/fig-popup-easing.js +141 -0
- package/fig-popup-gradient.css +11 -0
- package/fig-popup-gradient.js +138 -0
- package/fig-popup-palette.css +7 -0
- package/fig-popup-palette.js +173 -0
- package/fig.js +404 -109
- package/package.json +9 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/* Copyright (c) 2026 Rogie King. PolyForm Shield 1.0.0. See LICENSE. */
|
|
2
|
+
import "./fig.js";
|
|
3
|
+
|
|
4
|
+
const EASING_ATTRIBUTES = [
|
|
5
|
+
"value",
|
|
6
|
+
"mode",
|
|
7
|
+
"precision",
|
|
8
|
+
"edit",
|
|
9
|
+
"presets",
|
|
10
|
+
"text",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
customElements.whenDefined("fig-popup").then(() => {
|
|
14
|
+
if (customElements.get("fig-popup-easing")) return;
|
|
15
|
+
|
|
16
|
+
const FigPopup = customElements.get("fig-popup");
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A reusable popup editor for easing curves.
|
|
20
|
+
*
|
|
21
|
+
* @element fig-popup-easing
|
|
22
|
+
* @attr {string} value - Cubic bezier or spring easing value
|
|
23
|
+
* @attr {string} mode - Optional "bezier" or "spring" constraint
|
|
24
|
+
* @fires input - During easing edits
|
|
25
|
+
* @fires change - On committed easing edits
|
|
26
|
+
*/
|
|
27
|
+
class FigPopupEasing extends FigPopup {
|
|
28
|
+
#curve = null;
|
|
29
|
+
#syncingValueAttribute = false;
|
|
30
|
+
|
|
31
|
+
static get observedAttributes() {
|
|
32
|
+
return [
|
|
33
|
+
...new Set([
|
|
34
|
+
...(super.observedAttributes || []),
|
|
35
|
+
...EASING_ATTRIBUTES,
|
|
36
|
+
]),
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get value() {
|
|
41
|
+
return this.#curve?.value ?? this.getAttribute("value") ?? "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
set value(value) {
|
|
45
|
+
this.setAttribute(
|
|
46
|
+
"value",
|
|
47
|
+
value && typeof value === "object"
|
|
48
|
+
? JSON.stringify(value)
|
|
49
|
+
: String(value ?? ""),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
connectedCallback() {
|
|
54
|
+
this.#ensureStructure();
|
|
55
|
+
this.#applyDefaults();
|
|
56
|
+
super.connectedCallback();
|
|
57
|
+
this.#syncCurve();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
61
|
+
super.attributeChangedCallback(name, oldValue, newValue);
|
|
62
|
+
if (
|
|
63
|
+
oldValue !== newValue &&
|
|
64
|
+
EASING_ATTRIBUTES.includes(name) &&
|
|
65
|
+
!this.#syncingValueAttribute
|
|
66
|
+
) {
|
|
67
|
+
this.#syncCurveAttribute(name);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#applyDefaults() {
|
|
72
|
+
if (!this.hasAttribute("aria-label")) {
|
|
73
|
+
this.setAttribute("aria-label", "Edit easing");
|
|
74
|
+
}
|
|
75
|
+
if (!this.hasAttribute("position")) {
|
|
76
|
+
this.setAttribute("position", "bottom left");
|
|
77
|
+
}
|
|
78
|
+
if (!this.hasAttribute("offset")) this.setAttribute("offset", "0 8");
|
|
79
|
+
if (!this.hasAttribute("viewport-margin")) {
|
|
80
|
+
this.setAttribute("viewport-margin", "8");
|
|
81
|
+
}
|
|
82
|
+
if (!this.hasAttribute("popover")) {
|
|
83
|
+
this.setAttribute("popover", "manual");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#ensureStructure() {
|
|
88
|
+
if (this.#curve?.isConnected) return;
|
|
89
|
+
let content = this.querySelector(
|
|
90
|
+
":scope > fig-content.fig-easing-curve-content",
|
|
91
|
+
);
|
|
92
|
+
let curve = content?.querySelector(":scope > fig-easing-curve");
|
|
93
|
+
if (!content) {
|
|
94
|
+
content = document.createElement("fig-content");
|
|
95
|
+
content.className = "fig-easing-curve-content";
|
|
96
|
+
this.append(content);
|
|
97
|
+
}
|
|
98
|
+
if (!curve) {
|
|
99
|
+
curve = document.createElement("fig-easing-curve");
|
|
100
|
+
curve.setAttribute("aspect-ratio", "4/3");
|
|
101
|
+
content.append(curve);
|
|
102
|
+
}
|
|
103
|
+
curve.setAttribute("aspect-ratio", "4/3");
|
|
104
|
+
curve.addEventListener("input", this.#handleCurveEvent);
|
|
105
|
+
curve.addEventListener("change", this.#handleCurveEvent);
|
|
106
|
+
this.#curve = curve;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#syncCurve() {
|
|
110
|
+
for (const name of EASING_ATTRIBUTES) {
|
|
111
|
+
this.#syncCurveAttribute(name);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#syncCurveAttribute(name) {
|
|
116
|
+
if (!this.#curve) return;
|
|
117
|
+
const value = this.getAttribute(name);
|
|
118
|
+
if (value === null) this.#curve.removeAttribute(name);
|
|
119
|
+
else this.#curve.setAttribute(name, value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#handleCurveEvent = (event) => {
|
|
123
|
+
event.stopPropagation();
|
|
124
|
+
const value = this.#curve?.value ?? event.detail;
|
|
125
|
+
this.#syncingValueAttribute = true;
|
|
126
|
+
this.setAttribute("value", String(value ?? ""));
|
|
127
|
+
this.#syncingValueAttribute = false;
|
|
128
|
+
this.dispatchEvent(
|
|
129
|
+
new CustomEvent(event.type, {
|
|
130
|
+
bubbles: true,
|
|
131
|
+
composed: true,
|
|
132
|
+
detail: event.detail,
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
customElements.define("fig-popup-easing", FigPopupEasing, {
|
|
139
|
+
extends: "dialog",
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/* Copyright (c) 2026 Rogie King. PolyForm Shield 1.0.0. See LICENSE. */
|
|
2
|
+
import "./fig.js";
|
|
3
|
+
import "./fig-gradient-editor.js";
|
|
4
|
+
|
|
5
|
+
const GRADIENT_ATTRIBUTES = ["value", "color-space", "disabled"];
|
|
6
|
+
|
|
7
|
+
customElements.whenDefined("fig-popup").then(() => {
|
|
8
|
+
if (customElements.get("fig-popup-gradient")) return;
|
|
9
|
+
|
|
10
|
+
const FigPopup = customElements.get("fig-popup");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A reusable popup containing a full gradient editor.
|
|
14
|
+
*
|
|
15
|
+
* @element fig-popup-gradient
|
|
16
|
+
* @attr {string} value - JSON gradient config or full gradient fill value
|
|
17
|
+
* @attr {string} color-space - `srgb` or `display-p3`
|
|
18
|
+
* @attr {boolean} disabled - Disables gradient editing
|
|
19
|
+
* @fires input - During gradient edits
|
|
20
|
+
* @fires change - On committed gradient edits
|
|
21
|
+
*/
|
|
22
|
+
class FigPopupGradient extends FigPopup {
|
|
23
|
+
#editor = null;
|
|
24
|
+
#syncingValueAttribute = false;
|
|
25
|
+
|
|
26
|
+
static get observedAttributes() {
|
|
27
|
+
return [
|
|
28
|
+
...new Set([
|
|
29
|
+
...(super.observedAttributes || []),
|
|
30
|
+
...GRADIENT_ATTRIBUTES,
|
|
31
|
+
]),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get value() {
|
|
36
|
+
return this.#editor?.value ?? {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
set value(value) {
|
|
40
|
+
this.setAttribute(
|
|
41
|
+
"value",
|
|
42
|
+
typeof value === "string" ? value : JSON.stringify(value ?? {}),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
connectedCallback() {
|
|
47
|
+
this.#ensureStructure();
|
|
48
|
+
this.#applyDefaults();
|
|
49
|
+
super.connectedCallback();
|
|
50
|
+
this.#syncEditor();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
54
|
+
super.attributeChangedCallback(name, oldValue, newValue);
|
|
55
|
+
if (
|
|
56
|
+
oldValue !== newValue &&
|
|
57
|
+
GRADIENT_ATTRIBUTES.includes(name) &&
|
|
58
|
+
!this.#syncingValueAttribute
|
|
59
|
+
) {
|
|
60
|
+
this.#syncEditorAttribute(name);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#applyDefaults() {
|
|
65
|
+
if (!this.hasAttribute("aria-label")) {
|
|
66
|
+
this.setAttribute("aria-label", "Edit gradient");
|
|
67
|
+
}
|
|
68
|
+
if (!this.hasAttribute("position")) {
|
|
69
|
+
this.setAttribute("position", "bottom left");
|
|
70
|
+
}
|
|
71
|
+
if (!this.hasAttribute("offset")) this.setAttribute("offset", "0 8");
|
|
72
|
+
if (!this.hasAttribute("viewport-margin")) {
|
|
73
|
+
this.setAttribute("viewport-margin", "8");
|
|
74
|
+
}
|
|
75
|
+
if (!this.hasAttribute("popover")) {
|
|
76
|
+
this.setAttribute("popover", "manual");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#ensureStructure() {
|
|
81
|
+
if (this.#editor?.isConnected) return;
|
|
82
|
+
let content = this.querySelector(
|
|
83
|
+
":scope > fig-content.fig-gradient-editor-content",
|
|
84
|
+
);
|
|
85
|
+
let editor = content?.querySelector(":scope > fig-gradient-editor");
|
|
86
|
+
if (!content) {
|
|
87
|
+
content = document.createElement("fig-content");
|
|
88
|
+
content.className = "fig-gradient-editor-content";
|
|
89
|
+
this.append(content);
|
|
90
|
+
}
|
|
91
|
+
if (!editor) {
|
|
92
|
+
editor = document.createElement("fig-gradient-editor");
|
|
93
|
+
content.append(editor);
|
|
94
|
+
}
|
|
95
|
+
editor.pickerAnchor = this;
|
|
96
|
+
editor.addEventListener("input", this.#handleEditorEvent);
|
|
97
|
+
editor.addEventListener("change", this.#handleEditorEvent);
|
|
98
|
+
this.#editor = editor;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#syncEditor() {
|
|
102
|
+
for (const name of GRADIENT_ATTRIBUTES) {
|
|
103
|
+
this.#syncEditorAttribute(name);
|
|
104
|
+
}
|
|
105
|
+
if (this.#editor) this.#editor.pickerAnchor = this;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#syncEditorAttribute(name) {
|
|
109
|
+
if (!this.#editor) return;
|
|
110
|
+
const value = this.getAttribute(name);
|
|
111
|
+
if (value === null) this.#editor.removeAttribute(name);
|
|
112
|
+
else this.#editor.setAttribute(name, value);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#handleEditorEvent = (event) => {
|
|
116
|
+
event.stopPropagation();
|
|
117
|
+
const detail = event.detail ?? {
|
|
118
|
+
type: "gradient",
|
|
119
|
+
gradient: this.#editor?.value,
|
|
120
|
+
css: this.#editor?.cssValue,
|
|
121
|
+
};
|
|
122
|
+
this.#syncingValueAttribute = true;
|
|
123
|
+
this.setAttribute("value", JSON.stringify(detail.gradient ?? {}));
|
|
124
|
+
this.#syncingValueAttribute = false;
|
|
125
|
+
this.dispatchEvent(
|
|
126
|
+
new CustomEvent(event.type, {
|
|
127
|
+
bubbles: true,
|
|
128
|
+
composed: true,
|
|
129
|
+
detail,
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
customElements.define("fig-popup-gradient", FigPopupGradient, {
|
|
136
|
+
extends: "dialog",
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/* Copyright (c) 2026 Rogie King. PolyForm Shield 1.0.0. See LICENSE. */
|
|
2
|
+
import "./fig.js";
|
|
3
|
+
|
|
4
|
+
const PALETTE_ATTRIBUTES = ["value", "min", "max", "fixed", "disabled"];
|
|
5
|
+
|
|
6
|
+
function normalizePaletteValue(value) {
|
|
7
|
+
let entries = value;
|
|
8
|
+
if (typeof entries === "string") {
|
|
9
|
+
const raw = entries.trim();
|
|
10
|
+
if (!raw) return [];
|
|
11
|
+
try {
|
|
12
|
+
entries = JSON.parse(raw);
|
|
13
|
+
} catch {
|
|
14
|
+
entries = raw.includes(",") ? raw.split(",") : [raw];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (!Array.isArray(entries)) return [];
|
|
18
|
+
return entries.map((entry) => {
|
|
19
|
+
if (typeof entry === "string") {
|
|
20
|
+
const color = entry.trim();
|
|
21
|
+
return {
|
|
22
|
+
color: color.slice(0, 7) || "#D9D9D9",
|
|
23
|
+
alpha:
|
|
24
|
+
color.length > 7
|
|
25
|
+
? Math.max(0, Math.min(1, parseInt(color.slice(7, 9), 16) / 255))
|
|
26
|
+
: 1,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (entry && typeof entry === "object") {
|
|
30
|
+
const alpha =
|
|
31
|
+
entry.alpha !== undefined
|
|
32
|
+
? Number(entry.alpha)
|
|
33
|
+
: entry.opacity !== undefined
|
|
34
|
+
? Number(entry.opacity) / 100
|
|
35
|
+
: 1;
|
|
36
|
+
return {
|
|
37
|
+
color: String(entry.color || "#D9D9D9").slice(0, 7),
|
|
38
|
+
alpha: Number.isFinite(alpha) ? Math.max(0, Math.min(1, alpha)) : 1,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { color: "#D9D9D9", alpha: 1 };
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
customElements.whenDefined("fig-popup").then(() => {
|
|
46
|
+
if (customElements.get("fig-popup-palette")) return;
|
|
47
|
+
|
|
48
|
+
const FigPopup = customElements.get("fig-popup");
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A reusable popup editor for an ordered palette of solid colors.
|
|
52
|
+
*
|
|
53
|
+
* @element fig-popup-palette
|
|
54
|
+
* @attr {string} value - JSON color array or comma-separated hex colors
|
|
55
|
+
* @attr {number} min - Minimum number of colors
|
|
56
|
+
* @attr {number} max - Maximum number of colors
|
|
57
|
+
* @attr {boolean} fixed - Locks the palette length
|
|
58
|
+
* @attr {boolean} disabled - Disables palette editing
|
|
59
|
+
* @fires input - During editing with the normalized color array in detail
|
|
60
|
+
* @fires change - On committed edits with the normalized color array in detail
|
|
61
|
+
*/
|
|
62
|
+
class FigPopupPalette extends FigPopup {
|
|
63
|
+
#editor = null;
|
|
64
|
+
#syncingValueAttribute = false;
|
|
65
|
+
|
|
66
|
+
static get observedAttributes() {
|
|
67
|
+
return [
|
|
68
|
+
...new Set([
|
|
69
|
+
...(super.observedAttributes || []),
|
|
70
|
+
...PALETTE_ATTRIBUTES,
|
|
71
|
+
]),
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get value() {
|
|
76
|
+
return normalizePaletteValue(this.getAttribute("value"));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
set value(value) {
|
|
80
|
+
this.setAttribute(
|
|
81
|
+
"value",
|
|
82
|
+
typeof value === "string" ? value : JSON.stringify(value ?? []),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
connectedCallback() {
|
|
87
|
+
this.#ensureStructure();
|
|
88
|
+
this.#applyDefaults();
|
|
89
|
+
super.connectedCallback();
|
|
90
|
+
this.#syncEditor();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
94
|
+
super.attributeChangedCallback(name, oldValue, newValue);
|
|
95
|
+
if (
|
|
96
|
+
oldValue !== newValue &&
|
|
97
|
+
PALETTE_ATTRIBUTES.includes(name) &&
|
|
98
|
+
!this.#syncingValueAttribute
|
|
99
|
+
) {
|
|
100
|
+
this.#syncEditorAttribute(name);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#applyDefaults() {
|
|
105
|
+
if (!this.hasAttribute("aria-label")) {
|
|
106
|
+
this.setAttribute("aria-label", "Edit palette");
|
|
107
|
+
}
|
|
108
|
+
if (!this.hasAttribute("position")) {
|
|
109
|
+
this.setAttribute("position", "bottom left");
|
|
110
|
+
}
|
|
111
|
+
if (!this.hasAttribute("offset")) this.setAttribute("offset", "0 8");
|
|
112
|
+
if (!this.hasAttribute("viewport-margin")) {
|
|
113
|
+
this.setAttribute("viewport-margin", "8");
|
|
114
|
+
}
|
|
115
|
+
if (!this.hasAttribute("popover")) {
|
|
116
|
+
this.setAttribute("popover", "manual");
|
|
117
|
+
}
|
|
118
|
+
this.classList.add("fig-palette-editor-popup");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#ensureStructure() {
|
|
122
|
+
if (this.#editor?.isConnected) return;
|
|
123
|
+
let content = this.querySelector(
|
|
124
|
+
":scope > fig-content.fig-palette-editor-content",
|
|
125
|
+
);
|
|
126
|
+
let editor = content?.querySelector(":scope > fig-palette-editor");
|
|
127
|
+
if (!content) {
|
|
128
|
+
content = document.createElement("fig-content");
|
|
129
|
+
content.className = "fig-palette-editor-content";
|
|
130
|
+
this.append(content);
|
|
131
|
+
}
|
|
132
|
+
if (!editor) {
|
|
133
|
+
editor = document.createElement("fig-palette-editor");
|
|
134
|
+
content.append(editor);
|
|
135
|
+
}
|
|
136
|
+
editor.addEventListener("input", this.#handleEditorEvent);
|
|
137
|
+
editor.addEventListener("change", this.#handleEditorEvent);
|
|
138
|
+
this.#editor = editor;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#syncEditor() {
|
|
142
|
+
for (const name of PALETTE_ATTRIBUTES) {
|
|
143
|
+
this.#syncEditorAttribute(name);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#syncEditorAttribute(name) {
|
|
148
|
+
if (!this.#editor) return;
|
|
149
|
+
const value = this.getAttribute(name);
|
|
150
|
+
if (value === null) this.#editor.removeAttribute(name);
|
|
151
|
+
else this.#editor.setAttribute(name, value);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#handleEditorEvent = (event) => {
|
|
155
|
+
event.stopPropagation();
|
|
156
|
+
const value = normalizePaletteValue(event.detail);
|
|
157
|
+
this.#syncingValueAttribute = true;
|
|
158
|
+
this.setAttribute("value", JSON.stringify(value));
|
|
159
|
+
this.#syncingValueAttribute = false;
|
|
160
|
+
this.dispatchEvent(
|
|
161
|
+
new CustomEvent(event.type, {
|
|
162
|
+
bubbles: true,
|
|
163
|
+
composed: true,
|
|
164
|
+
detail: value,
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
customElements.define("fig-popup-palette", FigPopupPalette, {
|
|
171
|
+
extends: "dialog",
|
|
172
|
+
});
|
|
173
|
+
});
|