use-chisel 0.1.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 +42 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +1701 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1701 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
const CSS_NUMERIC_UNITS = ["px", "rem", "em"];
|
|
5
|
+
const FONT_FAMILY_OPTIONS = [
|
|
6
|
+
{ label: "Inter", value: "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif" },
|
|
7
|
+
{ label: "System", value: "system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif" },
|
|
8
|
+
{ label: "Arial", value: "Arial, Helvetica, sans-serif" },
|
|
9
|
+
{ label: "Georgia", value: "Georgia, \"Times New Roman\", serif" },
|
|
10
|
+
{ label: "Mono", value: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace" }
|
|
11
|
+
];
|
|
12
|
+
const COLOR_FORMATS = ["Hex", "RGB", "RGBA", "HSL"];
|
|
13
|
+
const COLOR_MODES = ["Solid", "Gradient", "Image"];
|
|
14
|
+
const NUMERIC_CONTROLS = [
|
|
15
|
+
{ label: "Font Size", property: "fontSize", cssProperty: "font-size", min: 8, max: 96, step: 1, unit: "px" },
|
|
16
|
+
{ label: "Line", property: "lineHeight", cssProperty: "line-height", min: 8, max: 120, step: 1, unit: "px" },
|
|
17
|
+
{ label: "Tracking", property: "letterSpacing", cssProperty: "letter-spacing", min: -2, max: 12, step: 0.25, unit: "px" },
|
|
18
|
+
{ label: "Padding X", property: "paddingInline", cssProperty: "padding-inline", min: 0, max: 120, step: 1, unit: "px" },
|
|
19
|
+
{ label: "Padding Y", property: "paddingBlock", cssProperty: "padding-block", min: 0, max: 120, step: 1, unit: "px" },
|
|
20
|
+
{ label: "Margin X", property: "marginInline", cssProperty: "margin-inline", min: -80, max: 160, step: 1, unit: "px" },
|
|
21
|
+
{ label: "Margin Y", property: "marginBlock", cssProperty: "margin-block", min: -80, max: 160, step: 1, unit: "px" },
|
|
22
|
+
{ label: "Radius", property: "borderRadius", cssProperty: "border-radius", min: 0, max: 80, step: 1, unit: "px" },
|
|
23
|
+
{ label: "Opacity", property: "opacity", cssProperty: "opacity", min: 0, max: 1, step: 0.01, unit: "" },
|
|
24
|
+
{ label: "Width", property: "width", cssProperty: "width", min: 0, max: 1200, step: 1, unit: "px" },
|
|
25
|
+
{ label: "Height", property: "height", cssProperty: "height", min: 0, max: 1200, step: 1, unit: "px" }
|
|
26
|
+
];
|
|
27
|
+
const STYLE_PROPERTIES = [
|
|
28
|
+
"color",
|
|
29
|
+
"backgroundColor",
|
|
30
|
+
"backgroundImage",
|
|
31
|
+
"backgroundPosition",
|
|
32
|
+
"backgroundSize",
|
|
33
|
+
"fontFamily",
|
|
34
|
+
"fontSize",
|
|
35
|
+
"lineHeight",
|
|
36
|
+
"letterSpacing",
|
|
37
|
+
"paddingInline",
|
|
38
|
+
"paddingBlock",
|
|
39
|
+
"marginInline",
|
|
40
|
+
"marginBlock",
|
|
41
|
+
"borderRadius",
|
|
42
|
+
"opacity",
|
|
43
|
+
"width",
|
|
44
|
+
"height"
|
|
45
|
+
];
|
|
46
|
+
export function EditableRegion({ id, as, children, ...props }) {
|
|
47
|
+
const Component = (as ?? "span");
|
|
48
|
+
return (_jsx(Component, { "data-chisel-id": id, ...props, children: children }));
|
|
49
|
+
}
|
|
50
|
+
export function Chisel({ enabled = process.env.NODE_ENV !== "production", position = "right", projectName = "Next.js app" }) {
|
|
51
|
+
const [hovered, setHovered] = React.useState(null);
|
|
52
|
+
const [selected, setSelected] = React.useState(null);
|
|
53
|
+
const [selectedElements, setSelectedElements] = React.useState([]);
|
|
54
|
+
const [hoverRect, setHoverRect] = React.useState(null);
|
|
55
|
+
const [selectedRects, setSelectedRects] = React.useState([]);
|
|
56
|
+
const [copied, setCopied] = React.useState(false);
|
|
57
|
+
const [versions, setVersions] = React.useState(() => [createVersion(1)]);
|
|
58
|
+
const [activeVersionId, setActiveVersionId] = React.useState("version-1");
|
|
59
|
+
const [versionsOpen, setVersionsOpen] = React.useState(false);
|
|
60
|
+
const snapshots = React.useRef(new WeakMap());
|
|
61
|
+
const baselines = React.useRef(new Map());
|
|
62
|
+
const routeKey = React.useRef("");
|
|
63
|
+
const hoveredRef = React.useRef(null);
|
|
64
|
+
const selectedRef = React.useRef(null);
|
|
65
|
+
const selectedElementsRef = React.useRef([]);
|
|
66
|
+
const hoverFrame = React.useRef(null);
|
|
67
|
+
const rectFrame = React.useRef(null);
|
|
68
|
+
React.useEffect(() => {
|
|
69
|
+
if (!enabled)
|
|
70
|
+
return;
|
|
71
|
+
const clearHover = () => {
|
|
72
|
+
if (!hoveredRef.current)
|
|
73
|
+
return;
|
|
74
|
+
hoveredRef.current = null;
|
|
75
|
+
setHovered(null);
|
|
76
|
+
setHoverRect(null);
|
|
77
|
+
};
|
|
78
|
+
const scheduleHoverRect = (target) => {
|
|
79
|
+
if (hoverFrame.current !== null)
|
|
80
|
+
window.cancelAnimationFrame(hoverFrame.current);
|
|
81
|
+
hoverFrame.current = window.requestAnimationFrame(() => {
|
|
82
|
+
hoverFrame.current = null;
|
|
83
|
+
if (hoveredRef.current !== target)
|
|
84
|
+
return;
|
|
85
|
+
setHoverRect(rectToJSON(target.getBoundingClientRect()));
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
const scheduleRects = () => {
|
|
89
|
+
if (rectFrame.current !== null)
|
|
90
|
+
return;
|
|
91
|
+
rectFrame.current = window.requestAnimationFrame(() => {
|
|
92
|
+
rectFrame.current = null;
|
|
93
|
+
const currentHovered = hoveredRef.current;
|
|
94
|
+
const currentSelected = selectedRef.current;
|
|
95
|
+
const currentSelectedElements = selectedElementsRef.current;
|
|
96
|
+
setHoverRect((current) => (currentHovered ? rectToJSON(currentHovered.getBoundingClientRect()) : current));
|
|
97
|
+
setSelectedRects(currentSelectedElements.map((element) => rectToJSON(element.getBoundingClientRect())));
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
const onMove = (event) => {
|
|
101
|
+
const target = event.target;
|
|
102
|
+
if (!(target instanceof HTMLElement) || isEditorElement(target)) {
|
|
103
|
+
clearHover();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (target === hoveredRef.current)
|
|
107
|
+
return;
|
|
108
|
+
hoveredRef.current = target;
|
|
109
|
+
setHovered(target);
|
|
110
|
+
scheduleHoverRect(target);
|
|
111
|
+
};
|
|
112
|
+
const onClick = (event) => {
|
|
113
|
+
const target = event.target;
|
|
114
|
+
if (!(target instanceof HTMLElement) || isEditorElement(target))
|
|
115
|
+
return;
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
event.stopPropagation();
|
|
118
|
+
selectElements(event.shiftKey ? toggleElementSelection(selectedElementsRef.current, target) : [target]);
|
|
119
|
+
};
|
|
120
|
+
const onKeyDown = (event) => {
|
|
121
|
+
const target = event.target;
|
|
122
|
+
if (target instanceof HTMLElement && isEditorElement(target))
|
|
123
|
+
return;
|
|
124
|
+
if (event.key === "Escape") {
|
|
125
|
+
selectedRef.current = null;
|
|
126
|
+
selectedElementsRef.current = [];
|
|
127
|
+
setSelected(null);
|
|
128
|
+
setSelectedElements([]);
|
|
129
|
+
setSelectedRects([]);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
document.addEventListener("mousemove", onMove, true);
|
|
133
|
+
document.addEventListener("click", onClick, true);
|
|
134
|
+
document.addEventListener("keydown", onKeyDown);
|
|
135
|
+
window.addEventListener("resize", scheduleRects);
|
|
136
|
+
window.addEventListener("scroll", scheduleRects, true);
|
|
137
|
+
return () => {
|
|
138
|
+
if (hoverFrame.current !== null)
|
|
139
|
+
window.cancelAnimationFrame(hoverFrame.current);
|
|
140
|
+
if (rectFrame.current !== null)
|
|
141
|
+
window.cancelAnimationFrame(rectFrame.current);
|
|
142
|
+
document.removeEventListener("mousemove", onMove, true);
|
|
143
|
+
document.removeEventListener("click", onClick, true);
|
|
144
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
145
|
+
window.removeEventListener("resize", scheduleRects);
|
|
146
|
+
window.removeEventListener("scroll", scheduleRects, true);
|
|
147
|
+
};
|
|
148
|
+
}, [enabled]);
|
|
149
|
+
React.useEffect(() => {
|
|
150
|
+
selectedRef.current = selected;
|
|
151
|
+
}, [selected]);
|
|
152
|
+
React.useEffect(() => {
|
|
153
|
+
selectedElementsRef.current = selectedElements;
|
|
154
|
+
}, [selectedElements]);
|
|
155
|
+
React.useEffect(() => {
|
|
156
|
+
if (!selectedElements.length)
|
|
157
|
+
return;
|
|
158
|
+
let frame = null;
|
|
159
|
+
const scheduleSelectedRect = () => {
|
|
160
|
+
if (frame !== null)
|
|
161
|
+
return;
|
|
162
|
+
frame = window.requestAnimationFrame(() => {
|
|
163
|
+
frame = null;
|
|
164
|
+
setSelectedRects(selectedElements.map((element) => rectToJSON(element.getBoundingClientRect())));
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
if (typeof ResizeObserver === "undefined") {
|
|
168
|
+
const id = window.setInterval(scheduleSelectedRect, 750);
|
|
169
|
+
return () => {
|
|
170
|
+
if (frame !== null)
|
|
171
|
+
window.cancelAnimationFrame(frame);
|
|
172
|
+
window.clearInterval(id);
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const observer = new ResizeObserver(scheduleSelectedRect);
|
|
176
|
+
for (const element of selectedElements)
|
|
177
|
+
observer.observe(element);
|
|
178
|
+
return () => {
|
|
179
|
+
if (frame !== null)
|
|
180
|
+
window.cancelAnimationFrame(frame);
|
|
181
|
+
observer.disconnect();
|
|
182
|
+
};
|
|
183
|
+
}, [selected, selectedElements]);
|
|
184
|
+
React.useEffect(() => {
|
|
185
|
+
if (!enabled)
|
|
186
|
+
return;
|
|
187
|
+
const key = getRouteStorageKey();
|
|
188
|
+
routeKey.current = key;
|
|
189
|
+
const storedVersions = readStoredVersions(key);
|
|
190
|
+
const nextVersions = storedVersions.length ? storedVersions : [createVersion(1)];
|
|
191
|
+
setVersions(nextVersions);
|
|
192
|
+
setActiveVersionId(nextVersions[0].id);
|
|
193
|
+
window.setTimeout(() => applyVersionToDom(nextVersions[0], baselines.current), 0);
|
|
194
|
+
}, [enabled]);
|
|
195
|
+
React.useEffect(() => {
|
|
196
|
+
if (!enabled || !routeKey.current)
|
|
197
|
+
return;
|
|
198
|
+
window.localStorage.setItem(routeKey.current, JSON.stringify(versions));
|
|
199
|
+
}, [enabled, versions]);
|
|
200
|
+
if (!enabled)
|
|
201
|
+
return null;
|
|
202
|
+
const selectionCount = selectedElements.length;
|
|
203
|
+
const snapshot = selected ? snapshots.current.get(selected) : undefined;
|
|
204
|
+
const canEditText = Boolean(selectionCount <= 1 && selected && selected.children.length === 0);
|
|
205
|
+
const sourceHint = selected ? getSourceHint(selected) : undefined;
|
|
206
|
+
const selector = selected ? getBestSelector(selected) : "";
|
|
207
|
+
const changedValues = selected && snapshot ? getChanges(selected, snapshot) : [];
|
|
208
|
+
const activeVersion = versions.find((version) => version.id === activeVersionId) ?? versions[0] ?? createVersion(1);
|
|
209
|
+
const selectedComputedStyle = selected ? getComputedStyle(selected) : null;
|
|
210
|
+
const getSelectedTargets = () => (selectedElements.length ? selectedElements : selected ? [selected] : []);
|
|
211
|
+
function selectElements(elements) {
|
|
212
|
+
for (const element of elements)
|
|
213
|
+
ensureSnapshot(element, snapshots.current);
|
|
214
|
+
const nextSelected = elements[elements.length - 1] ?? null;
|
|
215
|
+
selectedElementsRef.current = elements;
|
|
216
|
+
selectedRef.current = nextSelected;
|
|
217
|
+
setSelectedElements(elements);
|
|
218
|
+
setSelected(nextSelected);
|
|
219
|
+
setSelectedRects(elements.map((element) => rectToJSON(element.getBoundingClientRect())));
|
|
220
|
+
}
|
|
221
|
+
const refreshSelectedRects = () => {
|
|
222
|
+
setSelectedRects(selectedElements.map((element) => rectToJSON(element.getBoundingClientRect())));
|
|
223
|
+
};
|
|
224
|
+
const addVersion = () => {
|
|
225
|
+
const nextVersion = {
|
|
226
|
+
id: `version-${Date.now()}`,
|
|
227
|
+
name: `Version ${versions.length + 1}`,
|
|
228
|
+
elements: cloneVersionElements(activeVersion.elements)
|
|
229
|
+
};
|
|
230
|
+
setVersions((current) => [...current, nextVersion]);
|
|
231
|
+
setActiveVersionId(nextVersion.id);
|
|
232
|
+
setVersionsOpen(false);
|
|
233
|
+
applyVersionToDom(nextVersion, baselines.current);
|
|
234
|
+
refreshSelectedRects();
|
|
235
|
+
};
|
|
236
|
+
const selectVersion = (version) => {
|
|
237
|
+
setActiveVersionId(version.id);
|
|
238
|
+
setVersionsOpen(false);
|
|
239
|
+
applyVersionToDom(version, baselines.current);
|
|
240
|
+
refreshSelectedRects();
|
|
241
|
+
};
|
|
242
|
+
const applyNumeric = (control, value) => {
|
|
243
|
+
const next = control.unit === "px" ? `${value}px` : String(value);
|
|
244
|
+
applyStyleValue(control, next);
|
|
245
|
+
};
|
|
246
|
+
const applyStyleValue = (control, value) => {
|
|
247
|
+
const targets = getSelectedTargets();
|
|
248
|
+
if (!targets.length)
|
|
249
|
+
return;
|
|
250
|
+
for (const element of targets) {
|
|
251
|
+
ensureElementBaseline(element, getElementKey(element), baselines.current);
|
|
252
|
+
element.style[control.property] = value;
|
|
253
|
+
recordElementChange(element, { styles: { [control.property]: value } });
|
|
254
|
+
}
|
|
255
|
+
refreshSelectedRects();
|
|
256
|
+
};
|
|
257
|
+
const applyColor = (property, value) => {
|
|
258
|
+
const targets = getSelectedTargets();
|
|
259
|
+
if (!targets.length)
|
|
260
|
+
return;
|
|
261
|
+
for (const element of targets) {
|
|
262
|
+
ensureElementBaseline(element, getElementKey(element), baselines.current);
|
|
263
|
+
if (property === "backgroundColor" && isBackgroundImageValue(value)) {
|
|
264
|
+
element.style.backgroundImage = value;
|
|
265
|
+
if (value.startsWith("url(")) {
|
|
266
|
+
element.style.backgroundPosition = "center";
|
|
267
|
+
element.style.backgroundSize = "cover";
|
|
268
|
+
recordElementChange(element, { styles: { backgroundImage: value, backgroundPosition: "center", backgroundSize: "cover" } });
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
recordElementChange(element, { styles: { backgroundImage: value } });
|
|
272
|
+
}
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
element.style[property] = value;
|
|
276
|
+
if (property === "backgroundColor") {
|
|
277
|
+
element.style.backgroundImage = "";
|
|
278
|
+
element.style.backgroundPosition = "";
|
|
279
|
+
element.style.backgroundSize = "";
|
|
280
|
+
recordElementChange(element, { styles: { backgroundColor: value, backgroundImage: "", backgroundPosition: "", backgroundSize: "" } });
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
recordElementChange(element, { styles: { [property]: value } });
|
|
284
|
+
}
|
|
285
|
+
refreshSelectedRects();
|
|
286
|
+
};
|
|
287
|
+
const applyFontFamily = (value) => {
|
|
288
|
+
const targets = getSelectedTargets();
|
|
289
|
+
if (!targets.length)
|
|
290
|
+
return;
|
|
291
|
+
for (const element of targets) {
|
|
292
|
+
ensureElementBaseline(element, getElementKey(element), baselines.current);
|
|
293
|
+
element.style.fontFamily = value;
|
|
294
|
+
recordElementChange(element, { styles: { fontFamily: value } });
|
|
295
|
+
}
|
|
296
|
+
refreshSelectedRects();
|
|
297
|
+
};
|
|
298
|
+
const previewFontFamily = (value) => {
|
|
299
|
+
const targets = getSelectedTargets();
|
|
300
|
+
if (!targets.length)
|
|
301
|
+
return;
|
|
302
|
+
for (const element of targets) {
|
|
303
|
+
ensureElementBaseline(element, getElementKey(element), baselines.current);
|
|
304
|
+
element.style.fontFamily = value;
|
|
305
|
+
}
|
|
306
|
+
refreshSelectedRects();
|
|
307
|
+
};
|
|
308
|
+
const applyText = (value) => {
|
|
309
|
+
if (!selected || !canEditText)
|
|
310
|
+
return;
|
|
311
|
+
ensureElementBaseline(selected, getElementKey(selected), baselines.current);
|
|
312
|
+
selected.textContent = value;
|
|
313
|
+
recordElementChange(selected, { text: value });
|
|
314
|
+
refreshSelectedRects();
|
|
315
|
+
};
|
|
316
|
+
const resetSelected = () => {
|
|
317
|
+
const targets = getSelectedTargets();
|
|
318
|
+
if (!targets.length)
|
|
319
|
+
return;
|
|
320
|
+
for (const element of targets) {
|
|
321
|
+
const elementSnapshot = snapshots.current.get(element);
|
|
322
|
+
const baseline = baselines.current.get(getElementKey(element));
|
|
323
|
+
for (const property of STYLE_PROPERTIES) {
|
|
324
|
+
element.style[property] = baseline?.styles[property] ?? "";
|
|
325
|
+
}
|
|
326
|
+
if (element === selected && canEditText)
|
|
327
|
+
element.textContent = baseline?.text ?? elementSnapshot?.text ?? "";
|
|
328
|
+
removeElementChange(element);
|
|
329
|
+
}
|
|
330
|
+
refreshSelectedRects();
|
|
331
|
+
};
|
|
332
|
+
function recordElementChange(element, patch) {
|
|
333
|
+
const key = getElementKey(element);
|
|
334
|
+
ensureElementBaseline(element, key, baselines.current);
|
|
335
|
+
setVersions((current) => current.map((version) => {
|
|
336
|
+
if (version.id !== activeVersionId)
|
|
337
|
+
return version;
|
|
338
|
+
const existing = version.elements[key];
|
|
339
|
+
return {
|
|
340
|
+
...version,
|
|
341
|
+
elements: {
|
|
342
|
+
...version.elements,
|
|
343
|
+
[key]: {
|
|
344
|
+
selector: getBestSelector(element),
|
|
345
|
+
sourceHint: getSourceHint(element),
|
|
346
|
+
text: patch.text ?? existing?.text,
|
|
347
|
+
styles: {
|
|
348
|
+
...(existing?.styles ?? {}),
|
|
349
|
+
...(patch.styles ?? {})
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
function removeElementChange(element) {
|
|
357
|
+
const key = getElementKey(element);
|
|
358
|
+
setVersions((current) => current.map((version) => {
|
|
359
|
+
if (version.id !== activeVersionId)
|
|
360
|
+
return version;
|
|
361
|
+
const { [key]: _removed, ...elements } = version.elements;
|
|
362
|
+
return { ...version, elements };
|
|
363
|
+
}));
|
|
364
|
+
}
|
|
365
|
+
const copyPrompt = async () => {
|
|
366
|
+
const prompt = getPrompt();
|
|
367
|
+
if (!prompt)
|
|
368
|
+
return;
|
|
369
|
+
await navigator.clipboard.writeText(prompt);
|
|
370
|
+
setCopied(true);
|
|
371
|
+
window.setTimeout(() => setCopied(false), 1600);
|
|
372
|
+
};
|
|
373
|
+
function getPrompt() {
|
|
374
|
+
const selectedRect = selected ? rectToJSON(selected.getBoundingClientRect()) : undefined;
|
|
375
|
+
if (Object.keys(activeVersion.elements).length)
|
|
376
|
+
return buildPageAgentPrompt({ projectName, version: activeVersion, baselines: baselines.current });
|
|
377
|
+
if (!selected || !snapshot || !selectedRect)
|
|
378
|
+
return undefined;
|
|
379
|
+
return buildAgentPrompt({
|
|
380
|
+
projectName,
|
|
381
|
+
element: selected,
|
|
382
|
+
snapshot,
|
|
383
|
+
rect: selectedRect,
|
|
384
|
+
selector,
|
|
385
|
+
sourceHint,
|
|
386
|
+
changes: changedValues
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
return (_jsxs("div", { "data-chisel-root": "true", className: "chisel-root", children: [_jsx(ChiselStyles, {}), hoverRect && hovered && !selectedElements.includes(hovered) ? _jsx(Box, { rect: hoverRect, tone: "hover" }) : null, selectedRects.map((rect, index) => (_jsx(Box, { rect: rect, tone: "selected" }, `${rect.left}-${rect.top}-${index}`))), _jsx("aside", { className: `chisel-panel chisel-panel-${position}`, "aria-label": "Chisel editor", children: _jsx("div", { className: "chisel-panel-wrapper", children: _jsx("div", { className: "chisel-panel-inner", children: _jsxs("div", { className: "chisel-folder chisel-folder-root", children: [_jsx("header", { className: "chisel-folder-header chisel-panel-header", children: _jsxs("div", { className: "chisel-panel-toolbar", children: [_jsx("button", { className: "chisel-toolbar-add", type: "button", "aria-label": "Add version", onClick: addVersion, children: "+" }), _jsxs("div", { className: "chisel-preset-manager", children: [_jsxs("button", { className: "chisel-preset-trigger", type: "button", "aria-expanded": versionsOpen, "aria-haspopup": "listbox", onClick: () => setVersionsOpen((open) => !open), children: [_jsx("span", { className: "chisel-preset-trigger-label", children: activeVersion.name }), _jsx("span", { className: "chisel-chevron", "aria-hidden": "true" })] }), versionsOpen ? (_jsx("div", { className: "chisel-preset-dropdown", role: "listbox", "aria-label": "Page versions", children: _jsx("div", { className: "chisel-preset-list", children: versions.map((version) => (_jsxs("button", { className: "chisel-preset-item", type: "button", role: "option", "aria-selected": version.id === activeVersion.id, "data-active": version.id === activeVersion.id, onClick: () => selectVersion(version), children: [_jsx("span", { className: "chisel-preset-name", children: version.name }), _jsx("span", { className: "chisel-preset-count", children: Object.keys(version.elements).length })] }, version.id))) }) })) : null] }), _jsx("button", { className: "chisel-toolbar-add", type: "button", onClick: () => {
|
|
390
|
+
selectedRef.current = null;
|
|
391
|
+
selectedElementsRef.current = [];
|
|
392
|
+
setSelected(null);
|
|
393
|
+
setSelectedElements([]);
|
|
394
|
+
setSelectedRects([]);
|
|
395
|
+
}, "aria-label": "Clear selection", children: "\u00D7" })] }) }), selected && snapshot ? (_jsxs("div", { className: "chisel-content", children: [_jsxs("div", { className: "chisel-text-control", children: [_jsx("span", { className: "chisel-text-label", children: selectionCount > 1 ? "Selection" : "Element" }), _jsx("code", { className: "chisel-text-code", children: selectionCount > 1 ? `${selectionCount} elements` : selected.tagName.toLowerCase() })] }), _jsxs("div", { className: "chisel-text-control chisel-selector-row", children: [_jsx("span", { className: "chisel-text-label", children: "Selector" }), _jsx("code", { className: "chisel-selector", children: selector })] }), _jsxs("div", { className: sourceHint ? "chisel-text-control" : "chisel-text-control chisel-warning-row", children: [_jsx("span", { className: "chisel-text-label", children: "Source" }), _jsx("span", { className: sourceHint ? "chisel-text-value" : "chisel-warning", children: sourceHint ? `Source hint: ${sourceHint}` : "Low-confidence source mapping" })] }), _jsxs("div", { className: "chisel-folder", children: [_jsx("div", { className: "chisel-folder-header", children: _jsx("div", { className: "chisel-folder-header-top", children: _jsx("div", { className: "chisel-folder-title-row", children: _jsx("span", { className: "chisel-folder-title", children: "Content" }) }) }) }), _jsx("div", { className: "chisel-folder-content", children: _jsxs("div", { className: "chisel-folder-inner", children: [_jsxs("label", { className: "chisel-text-control chisel-textarea-control", children: [_jsx("span", { className: "chisel-text-label", children: "Text" }), _jsx("textarea", { className: "chisel-text-input", value: selected.textContent ?? "", onChange: (event) => applyText(event.target.value), disabled: !canEditText, rows: 3 })] }), !canEditText ? _jsx("div", { className: "chisel-note-row", children: selectionCount > 1 ? "Text editing is enabled for one element at a time." : "Text editing is enabled for simple text elements." }) : null] }) })] }), _jsxs("div", { className: "chisel-folder", children: [_jsx("div", { className: "chisel-folder-header", children: _jsx("div", { className: "chisel-folder-header-top", children: _jsx("div", { className: "chisel-folder-title-row", children: _jsx("span", { className: "chisel-folder-title", children: "Color" }) }) }) }), _jsx("div", { className: "chisel-folder-content", children: _jsxs("div", { className: "chisel-folder-inner", children: [_jsx(ColorControl, { label: "Text", value: selected.style.color || toHex(selectedComputedStyle?.color ?? "#000000"), onChange: (value) => applyColor("color", value) }), _jsx(ColorControl, { label: "Fill", value: selected.style.backgroundImage ||
|
|
396
|
+
(selectedComputedStyle?.backgroundImage && selectedComputedStyle.backgroundImage !== "none" ? selectedComputedStyle.backgroundImage : selected.style.backgroundColor || toHex(selectedComputedStyle?.backgroundColor ?? "#ffffff")), onChange: (value) => applyColor("backgroundColor", value), supportsBackgroundImages: true })] }) })] }), _jsxs("div", { className: "chisel-folder", children: [_jsx("div", { className: "chisel-folder-header", children: _jsx("div", { className: "chisel-folder-header-top", children: _jsx("div", { className: "chisel-folder-title-row", children: _jsx("span", { className: "chisel-folder-title", children: "Style" }) }) }) }), _jsx("div", { className: "chisel-folder-content", children: _jsxs("div", { className: "chisel-folder-inner", children: [_jsx(FontFamilyControl, { value: selectedComputedStyle?.fontFamily ?? "", onChange: applyFontFamily, onPreview: previewFontFamily }), NUMERIC_CONTROLS.map((control) => (_jsx(SliderControl, { control: control, computedStyle: selectedComputedStyle, element: selected, onSlide: (value) => applyNumeric(control, value), onValueChange: (value) => applyStyleValue(control, value) }, control.cssProperty)))] }) })] }), _jsxs("div", { className: "chisel-folder", children: [_jsx("div", { className: "chisel-folder-header", children: _jsxs("div", { className: "chisel-folder-header-top", children: [_jsx("div", { className: "chisel-folder-title-row", children: _jsx("span", { className: "chisel-folder-title", children: "Changes" }) }), _jsx("span", { className: "chisel-folder-count", children: changedValues.length })] }) }), _jsx("div", { className: "chisel-folder-content", children: _jsx("div", { className: "chisel-folder-inner", children: changedValues.length ? (changedValues.map((change) => (_jsxs("div", { className: "chisel-text-control", children: [_jsx("span", { className: "chisel-text-label", children: change.property }), _jsx("strong", { className: "chisel-text-value", children: String(change.after) })] }, change.property)))) : (_jsx("div", { className: "chisel-note-row", children: "No preview changes yet." })) }) })] }), _jsxs("div", { className: "chisel-actions", children: [_jsx("button", { className: "chisel-button", type: "button", onClick: resetSelected, children: "Reset" }), _jsx("button", { className: "chisel-button", type: "button", onClick: copyPrompt, children: copied ? "Copied" : "Copy agent prompt" })] })] })) : (_jsxs("div", { className: "chisel-empty", children: [_jsx("div", { className: "chisel-note-row", children: "Select any visible element to edit its text and style preview." }), _jsx("button", { className: "chisel-button", type: "button", children: "Next" })] }))] }) }) }) })] }));
|
|
397
|
+
}
|
|
398
|
+
function Box({ rect, tone }) {
|
|
399
|
+
return (_jsx("div", { className: `chisel-box chisel-box-${tone}`, style: {
|
|
400
|
+
transform: `translate3d(${rect.left}px, ${rect.top}px, 0)`,
|
|
401
|
+
width: rect.width,
|
|
402
|
+
height: rect.height
|
|
403
|
+
} }));
|
|
404
|
+
}
|
|
405
|
+
function FontFamilyControl({ value, onChange, onPreview }) {
|
|
406
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
407
|
+
const rootRef = React.useRef(null);
|
|
408
|
+
const options = React.useMemo(() => getFontFamilyOptions(value), [value]);
|
|
409
|
+
const selectedOption = getMatchingFontOption(value, options);
|
|
410
|
+
const selectedLabel = selectedOption?.label ?? getFontFamilyLabel(value) ?? "Font";
|
|
411
|
+
React.useEffect(() => {
|
|
412
|
+
if (!isOpen)
|
|
413
|
+
return;
|
|
414
|
+
const closeOnOutsidePointer = (event) => {
|
|
415
|
+
const target = event.target;
|
|
416
|
+
if (target instanceof Node && rootRef.current?.contains(target))
|
|
417
|
+
return;
|
|
418
|
+
onPreview(value);
|
|
419
|
+
setIsOpen(false);
|
|
420
|
+
};
|
|
421
|
+
const closeOnEscape = (event) => {
|
|
422
|
+
if (event.key !== "Escape")
|
|
423
|
+
return;
|
|
424
|
+
onPreview(value);
|
|
425
|
+
setIsOpen(false);
|
|
426
|
+
};
|
|
427
|
+
document.addEventListener("pointerdown", closeOnOutsidePointer, true);
|
|
428
|
+
document.addEventListener("keydown", closeOnEscape);
|
|
429
|
+
return () => {
|
|
430
|
+
document.removeEventListener("pointerdown", closeOnOutsidePointer, true);
|
|
431
|
+
document.removeEventListener("keydown", closeOnEscape);
|
|
432
|
+
};
|
|
433
|
+
}, [isOpen, onPreview, value]);
|
|
434
|
+
const openMenu = () => {
|
|
435
|
+
setIsOpen((open) => {
|
|
436
|
+
if (open) {
|
|
437
|
+
onPreview(value);
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
return true;
|
|
441
|
+
});
|
|
442
|
+
};
|
|
443
|
+
const closeMenu = () => {
|
|
444
|
+
onPreview(value);
|
|
445
|
+
setIsOpen(false);
|
|
446
|
+
};
|
|
447
|
+
const selectFont = (option) => {
|
|
448
|
+
onChange(option.value);
|
|
449
|
+
setIsOpen(false);
|
|
450
|
+
};
|
|
451
|
+
return (_jsxs("div", { className: "chisel-font-control", ref: rootRef, children: [_jsx("span", { className: "chisel-font-label", children: "Font" }), _jsxs("div", { className: "chisel-font-picker", children: [_jsxs("button", { className: "chisel-font-trigger", type: "button", "aria-expanded": isOpen, "aria-haspopup": "listbox", onClick: openMenu, children: [_jsx("span", { className: "chisel-font-trigger-label", children: selectedLabel }), _jsx("span", { className: "chisel-chevron", "aria-hidden": "true" })] }), isOpen ? (_jsx("div", { className: "chisel-preset-dropdown chisel-font-dropdown", role: "listbox", "aria-label": "Font family", onMouseLeave: closeMenu, children: _jsx("div", { className: "chisel-preset-list chisel-font-list", children: options.map((option) => (_jsx("button", { className: "chisel-preset-item chisel-font-item", "data-active": isMatchingFontFamily(value, option.value), onClick: () => selectFont(option), onFocus: () => onPreview(option.value), onPointerEnter: () => onPreview(option.value), role: "option", style: { fontFamily: option.value }, type: "button", children: _jsx("span", { className: "chisel-preset-name", children: option.label }) }, option.value))) }) })) : null] })] }));
|
|
452
|
+
}
|
|
453
|
+
function ColorControl({ label, value, onChange, supportsBackgroundImages = false }) {
|
|
454
|
+
const parsedGradient = parseGradientValue(value);
|
|
455
|
+
const parsedImageUrl = parseImageUrlValue(value);
|
|
456
|
+
const initialMode = supportsBackgroundImages && parsedGradient ? "Gradient" : supportsBackgroundImages && parsedImageUrl !== undefined ? "Image" : "Solid";
|
|
457
|
+
const initialDraft = parsedGradient?.start ?? (parsedImageUrl !== undefined ? "#FFFFFF" : getColorDisplayValue(value));
|
|
458
|
+
const [mode, setMode] = React.useState(initialMode);
|
|
459
|
+
const [format, setFormat] = React.useState(() => inferColorFormat(initialDraft));
|
|
460
|
+
const [draft, setDraft] = React.useState(initialDraft);
|
|
461
|
+
const [gradientStart, setGradientStart] = React.useState(parsedGradient?.start ?? initialDraft);
|
|
462
|
+
const [gradientEnd, setGradientEnd] = React.useState(parsedGradient?.end ?? "#FFFFFF");
|
|
463
|
+
const [activeGradientStop, setActiveGradientStop] = React.useState("start");
|
|
464
|
+
const [imageUrl, setImageUrl] = React.useState(parsedImageUrl ?? "");
|
|
465
|
+
const [imageFileName, setImageFileName] = React.useState("");
|
|
466
|
+
const [isInvalid, setIsInvalid] = React.useState(false);
|
|
467
|
+
const [isPickerOpen, setIsPickerOpen] = React.useState(false);
|
|
468
|
+
const pickerRef = React.useRef(null);
|
|
469
|
+
const currentColorValue = mode === "Gradient" ? (activeGradientStop === "start" ? gradientStart : gradientEnd) : draft;
|
|
470
|
+
const currentHex = toHex(currentColorValue).toUpperCase();
|
|
471
|
+
const currentAlpha = getColorAlpha(currentColorValue);
|
|
472
|
+
const currentHsv = React.useMemo(() => rgbToHsv(hexToRgb(currentHex)), [currentHex]);
|
|
473
|
+
const summaryValue = mode === "Gradient" ? "Gradient" : mode === "Image" ? "Image" : draft;
|
|
474
|
+
React.useEffect(() => {
|
|
475
|
+
const nextGradient = parseGradientValue(value);
|
|
476
|
+
const nextImageUrl = parseImageUrlValue(value);
|
|
477
|
+
if (supportsBackgroundImages && nextGradient) {
|
|
478
|
+
const nextDraft = activeGradientStop === "end" ? nextGradient.end : nextGradient.start;
|
|
479
|
+
setMode("Gradient");
|
|
480
|
+
setGradientStart(nextGradient.start);
|
|
481
|
+
setGradientEnd(nextGradient.end);
|
|
482
|
+
setDraft(nextDraft);
|
|
483
|
+
setFormat(inferColorFormat(nextDraft));
|
|
484
|
+
setIsInvalid(false);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (supportsBackgroundImages && nextImageUrl !== undefined) {
|
|
488
|
+
setMode("Image");
|
|
489
|
+
setImageUrl(nextImageUrl);
|
|
490
|
+
setDraft("#FFFFFF");
|
|
491
|
+
setIsInvalid(false);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const displayValue = getColorDisplayValue(value);
|
|
495
|
+
setMode("Solid");
|
|
496
|
+
setDraft((currentDraft) => (currentDraft && toHex(currentDraft) === toHex(displayValue) ? currentDraft : displayValue));
|
|
497
|
+
setFormat(inferColorFormat(displayValue));
|
|
498
|
+
setIsInvalid(false);
|
|
499
|
+
}, [activeGradientStop, supportsBackgroundImages, value]);
|
|
500
|
+
React.useEffect(() => {
|
|
501
|
+
if (!isPickerOpen)
|
|
502
|
+
return;
|
|
503
|
+
const closeOnOutsidePointer = (event) => {
|
|
504
|
+
const target = event.target;
|
|
505
|
+
if (target instanceof Node && pickerRef.current?.contains(target))
|
|
506
|
+
return;
|
|
507
|
+
setIsPickerOpen(false);
|
|
508
|
+
};
|
|
509
|
+
document.addEventListener("pointerdown", closeOnOutsidePointer, true);
|
|
510
|
+
return () => document.removeEventListener("pointerdown", closeOnOutsidePointer, true);
|
|
511
|
+
}, [isPickerOpen]);
|
|
512
|
+
const applyColorValue = (nextValue) => {
|
|
513
|
+
const normalized = normalizeColorInput(nextValue);
|
|
514
|
+
if (!normalized) {
|
|
515
|
+
setIsInvalid(nextValue.trim().length > 0);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const formatted = normalizeHexColor(normalized) ?? normalized;
|
|
519
|
+
setFormat(inferColorFormat(formatted));
|
|
520
|
+
setDraft(formatted);
|
|
521
|
+
setIsInvalid(false);
|
|
522
|
+
if (mode === "Gradient") {
|
|
523
|
+
applyGradientStop(activeGradientStop, formatted);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
onChange(formatted);
|
|
527
|
+
};
|
|
528
|
+
const applyFormattedColor = (nextHex, nextAlpha = currentAlpha, nextFormat = format) => {
|
|
529
|
+
const formatted = formatColorValue(nextHex, nextAlpha, nextFormat);
|
|
530
|
+
setDraft(formatted);
|
|
531
|
+
setIsInvalid(false);
|
|
532
|
+
if (mode === "Gradient") {
|
|
533
|
+
applyGradientStop(activeGradientStop, formatted);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
onChange(formatted);
|
|
537
|
+
};
|
|
538
|
+
const applySaturationValue = (event, rect) => {
|
|
539
|
+
const saturation = clamp(((event.clientX - rect.left) / rect.width) * 100, 0, 100);
|
|
540
|
+
const brightness = clamp((1 - (event.clientY - rect.top) / rect.height) * 100, 0, 100);
|
|
541
|
+
applyFormattedColor(hsvToHex(currentHsv.h, saturation, brightness));
|
|
542
|
+
};
|
|
543
|
+
const applyHue = (event, rect) => {
|
|
544
|
+
const hue = clamp(((event.clientX - rect.left) / rect.width) * 360, 0, 360);
|
|
545
|
+
applyFormattedColor(hsvToHex(hue, currentHsv.s, currentHsv.v));
|
|
546
|
+
};
|
|
547
|
+
const applyAlpha = (event, rect) => {
|
|
548
|
+
const alpha = clamp((event.clientX - rect.left) / rect.width, 0, 1);
|
|
549
|
+
applyFormattedColor(currentHex, alpha);
|
|
550
|
+
};
|
|
551
|
+
const selectFormat = (nextFormat) => {
|
|
552
|
+
setFormat(nextFormat);
|
|
553
|
+
applyFormattedColor(currentHex, currentAlpha, nextFormat);
|
|
554
|
+
};
|
|
555
|
+
const selectMode = (nextMode) => {
|
|
556
|
+
if (nextMode !== "Solid" && !supportsBackgroundImages)
|
|
557
|
+
return;
|
|
558
|
+
setMode(nextMode);
|
|
559
|
+
if (nextMode === "Solid")
|
|
560
|
+
onChange(draft);
|
|
561
|
+
if (nextMode === "Gradient") {
|
|
562
|
+
const nextStart = gradientStart || draft;
|
|
563
|
+
setGradientStart(nextStart);
|
|
564
|
+
setActiveGradientStop("start");
|
|
565
|
+
setDraft(nextStart);
|
|
566
|
+
onChange(getGradientValue(nextStart, gradientEnd));
|
|
567
|
+
}
|
|
568
|
+
if (nextMode === "Image" && imageUrl.trim())
|
|
569
|
+
onChange(getImageValue(imageUrl));
|
|
570
|
+
};
|
|
571
|
+
const selectGradientStop = (stop) => {
|
|
572
|
+
const nextDraft = stop === "start" ? gradientStart : gradientEnd;
|
|
573
|
+
setActiveGradientStop(stop);
|
|
574
|
+
setDraft(nextDraft);
|
|
575
|
+
setFormat(inferColorFormat(nextDraft));
|
|
576
|
+
setIsInvalid(false);
|
|
577
|
+
};
|
|
578
|
+
const applyGradientStop = (stop, nextValue) => {
|
|
579
|
+
const nextStart = stop === "start" ? nextValue : gradientStart;
|
|
580
|
+
const nextEnd = stop === "end" ? nextValue : gradientEnd;
|
|
581
|
+
setGradientStart(nextStart);
|
|
582
|
+
setGradientEnd(nextEnd);
|
|
583
|
+
onChange(getGradientValue(nextStart, nextEnd));
|
|
584
|
+
};
|
|
585
|
+
const applyGradientStopValue = (stop, nextValue) => {
|
|
586
|
+
const normalized = normalizeColorInput(nextValue);
|
|
587
|
+
if (stop === "start")
|
|
588
|
+
setGradientStart(nextValue);
|
|
589
|
+
else
|
|
590
|
+
setGradientEnd(nextValue);
|
|
591
|
+
setActiveGradientStop(stop);
|
|
592
|
+
setDraft(nextValue);
|
|
593
|
+
if (!normalized) {
|
|
594
|
+
setIsInvalid(nextValue.trim().length > 0);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const formatted = normalizeHexColor(normalized) ?? normalized;
|
|
598
|
+
setFormat(inferColorFormat(formatted));
|
|
599
|
+
setDraft(formatted);
|
|
600
|
+
setIsInvalid(false);
|
|
601
|
+
applyGradientStop(stop, formatted);
|
|
602
|
+
};
|
|
603
|
+
const applyImageUrl = (nextValue) => {
|
|
604
|
+
setImageUrl(nextValue);
|
|
605
|
+
setImageFileName("");
|
|
606
|
+
if (nextValue.trim())
|
|
607
|
+
onChange(getImageValue(nextValue));
|
|
608
|
+
};
|
|
609
|
+
const applyImageFile = (event) => {
|
|
610
|
+
const file = event.currentTarget.files?.[0];
|
|
611
|
+
if (!file)
|
|
612
|
+
return;
|
|
613
|
+
setImageFileName(file.name);
|
|
614
|
+
const reader = new FileReader();
|
|
615
|
+
reader.onload = () => {
|
|
616
|
+
if (typeof reader.result !== "string")
|
|
617
|
+
return;
|
|
618
|
+
setImageUrl(reader.result);
|
|
619
|
+
onChange(getImageValue(reader.result));
|
|
620
|
+
};
|
|
621
|
+
reader.readAsDataURL(file);
|
|
622
|
+
};
|
|
623
|
+
return (_jsxs("div", { className: "chisel-color-control", "data-picker-open": isPickerOpen, ref: pickerRef, children: [_jsx("span", { className: "chisel-color-label", children: label }), _jsxs("span", { className: "chisel-color-inputs", children: [_jsx("button", { className: "chisel-color-swatch", "aria-expanded": isPickerOpen, "aria-label": `${label} color picker`, onClick: () => setIsPickerOpen((open) => !open), style: { background: mode === "Gradient" ? getGradientValue(gradientStart, gradientEnd) : mode === "Image" && imageUrl ? getImageValue(imageUrl) : draft }, type: "button" }), _jsx("input", { className: "chisel-color-value-input", "aria-label": `${label} color value`, autoCapitalize: "off", autoComplete: "off", autoCorrect: "off", "data-invalid": isInvalid, readOnly: mode !== "Solid", spellCheck: false, style: { width: `${Math.max(7.5, Math.min(summaryValue.length + 0.5, 18))}ch` }, value: summaryValue, onBlur: () => {
|
|
624
|
+
if (mode === "Solid")
|
|
625
|
+
applyColorValue(draft);
|
|
626
|
+
}, onChange: (event) => {
|
|
627
|
+
if (mode !== "Solid")
|
|
628
|
+
return;
|
|
629
|
+
const nextDraft = event.target.value;
|
|
630
|
+
setDraft(nextDraft);
|
|
631
|
+
applyColorValue(nextDraft);
|
|
632
|
+
}, onKeyDown: (event) => {
|
|
633
|
+
if (event.key === "Enter") {
|
|
634
|
+
event.preventDefault();
|
|
635
|
+
applyColorValue(draft);
|
|
636
|
+
event.currentTarget.blur();
|
|
637
|
+
}
|
|
638
|
+
if (event.key === "Escape") {
|
|
639
|
+
event.preventDefault();
|
|
640
|
+
setDraft(initialDraft);
|
|
641
|
+
setIsInvalid(false);
|
|
642
|
+
setIsPickerOpen(false);
|
|
643
|
+
event.currentTarget.blur();
|
|
644
|
+
}
|
|
645
|
+
} })] }), isPickerOpen ? (_jsxs("div", { className: "chisel-color-popover", role: "dialog", "aria-label": `${label} custom color picker`, children: [_jsxs("div", { className: "chisel-color-tabs", children: [COLOR_MODES.map((colorMode) => (_jsx("button", { className: "chisel-color-tab", "data-active": mode === colorMode, "aria-disabled": colorMode !== "Solid" && !supportsBackgroundImages, onClick: () => selectMode(colorMode), type: "button", children: colorMode }, colorMode))), _jsx("button", { className: "chisel-color-close", "aria-label": "Close color picker", onClick: () => setIsPickerOpen(false), type: "button", children: "\u00D7" })] }), mode !== "Image" ? (_jsxs(_Fragment, { children: [mode === "Gradient" ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "chisel-color-gradient-preview", style: { backgroundImage: getGradientValue(gradientStart, gradientEnd) } }), _jsxs("div", { className: "chisel-color-stop-buttons", role: "group", "aria-label": `${label} gradient endpoints`, children: [_jsxs("button", { className: "chisel-color-stop-button", "data-active": activeGradientStop === "start", "aria-label": `${label} gradient From`, onClick: () => selectGradientStop("start"), type: "button", children: [_jsx("span", { className: "chisel-color-stop-swatch", style: { background: gradientStart } }), _jsx("span", { children: "From" })] }), _jsxs("button", { className: "chisel-color-stop-button", "data-active": activeGradientStop === "end", "aria-label": `${label} gradient To`, onClick: () => selectGradientStop("end"), type: "button", children: [_jsx("span", { className: "chisel-color-stop-swatch", style: { background: gradientEnd } }), _jsx("span", { children: "To" })] })] })] })) : null, _jsx("div", { className: "chisel-color-field", onPointerDown: (event) => {
|
|
646
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
647
|
+
applySaturationValue(event, rect);
|
|
648
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
649
|
+
}, onPointerMove: (event) => {
|
|
650
|
+
if (!(event.buttons & 1))
|
|
651
|
+
return;
|
|
652
|
+
applySaturationValue(event, event.currentTarget.getBoundingClientRect());
|
|
653
|
+
}, style: { backgroundColor: `hsl(${currentHsv.h} 100% 50%)` }, children: _jsx("span", { className: "chisel-color-field-cursor", style: {
|
|
654
|
+
left: `clamp(6px, ${currentHsv.s}%, calc(100% - 6px))`,
|
|
655
|
+
top: `clamp(6px, ${100 - currentHsv.v}%, calc(100% - 6px))`,
|
|
656
|
+
backgroundColor: currentHex
|
|
657
|
+
} }) }), _jsx("div", { className: "chisel-color-hue", onPointerDown: (event) => {
|
|
658
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
659
|
+
applyHue(event, rect);
|
|
660
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
661
|
+
}, onPointerMove: (event) => {
|
|
662
|
+
if (!(event.buttons & 1))
|
|
663
|
+
return;
|
|
664
|
+
applyHue(event, event.currentTarget.getBoundingClientRect());
|
|
665
|
+
}, children: _jsx("span", { className: "chisel-color-hue-cursor", style: { left: `clamp(4px, ${(currentHsv.h / 360) * 100}%, calc(100% - 4px))` } }) }), _jsxs("div", { className: "chisel-color-alpha", onPointerDown: (event) => {
|
|
666
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
667
|
+
applyAlpha(event, rect);
|
|
668
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
669
|
+
}, onPointerMove: (event) => {
|
|
670
|
+
if (!(event.buttons & 1))
|
|
671
|
+
return;
|
|
672
|
+
applyAlpha(event, event.currentTarget.getBoundingClientRect());
|
|
673
|
+
}, style: { "--chisel-alpha-color": currentHex }, children: [_jsx("span", { className: "chisel-color-alpha-fill", style: { width: `${currentAlpha * 100}%` } }), _jsx("span", { className: "chisel-color-alpha-cursor", style: { left: `clamp(8px, ${currentAlpha * 100}%, calc(100% - 8px))`, backgroundColor: colorWithAlpha(currentHex, currentAlpha) } })] }), _jsxs("div", { className: "chisel-color-format-row", children: [_jsx("select", { className: "chisel-color-format-select", "aria-label": `${label} color format`, value: format, onChange: (event) => selectFormat(event.target.value), children: COLOR_FORMATS.map((option) => (_jsx("option", { value: option, children: option }, option))) }), _jsx("input", { className: "chisel-color-format-input", "aria-label": `${label} formatted color value`, autoCapitalize: "off", autoComplete: "off", autoCorrect: "off", "data-invalid": isInvalid, spellCheck: false, value: draft, onBlur: () => applyColorValue(draft), onChange: (event) => {
|
|
674
|
+
const nextDraft = event.target.value;
|
|
675
|
+
setDraft(nextDraft);
|
|
676
|
+
applyColorValue(nextDraft);
|
|
677
|
+
} })] })] })) : null, mode === "Gradient" ? (_jsxs(_Fragment, { children: [_jsxs("label", { className: "chisel-color-extra-row", children: [_jsx("span", { children: "From" }), _jsx("input", { "aria-label": `${label} gradient From value`, value: gradientStart, onFocus: () => selectGradientStop("start"), onChange: (event) => applyGradientStopValue("start", event.target.value) })] }), _jsxs("label", { className: "chisel-color-extra-row", children: [_jsx("span", { children: "To" }), _jsx("input", { "aria-label": `${label} gradient To value`, value: gradientEnd, onFocus: () => selectGradientStop("end"), onChange: (event) => applyGradientStopValue("end", event.target.value) })] })] })) : null, mode === "Image" ? (_jsxs(_Fragment, { children: [_jsxs("label", { className: "chisel-color-extra-row", children: [_jsx("span", { children: "URL" }), _jsx("input", { "aria-label": `${label} image URL`, value: imageUrl, onChange: (event) => applyImageUrl(event.target.value), placeholder: "https://..." })] }), _jsxs("label", { className: "chisel-color-upload-row", children: [_jsx("span", { children: "Upload" }), _jsx("span", { className: "chisel-color-upload-name", children: imageFileName || "Choose image" }), _jsx("input", { "aria-label": `${label} image upload`, accept: "image/*", type: "file", onChange: applyImageFile })] })] })) : null] })) : null] }));
|
|
678
|
+
}
|
|
679
|
+
function SliderControl({ control, computedStyle, element, onSlide, onValueChange }) {
|
|
680
|
+
const computed = computedStyle ?? getComputedStyle(element);
|
|
681
|
+
const raw = computed[control.property];
|
|
682
|
+
const parsed = parseFloat(raw);
|
|
683
|
+
const fallback = control.property === "width" ? element.getBoundingClientRect().width : control.property === "height" ? element.getBoundingClientRect().height : control.min;
|
|
684
|
+
const value = Number.isFinite(parsed) ? parsed : fallback;
|
|
685
|
+
const clamped = clamp(value, control.min, control.max);
|
|
686
|
+
const percent = getSliderPercent(clamped, control.min, control.max);
|
|
687
|
+
const authoredValue = element.style[control.property];
|
|
688
|
+
const displayValue = authoredValue || `${formatNumber(value)}${control.unit}`;
|
|
689
|
+
const inferredUnit = getInferredUnit(displayValue, control);
|
|
690
|
+
const [draft, setDraft] = React.useState(displayValue);
|
|
691
|
+
const [isEditing, setIsEditing] = React.useState(false);
|
|
692
|
+
const [isInvalid, setIsInvalid] = React.useState(false);
|
|
693
|
+
const [unitMenuOpen, setUnitMenuOpen] = React.useState(false);
|
|
694
|
+
const inputRef = React.useRef(null);
|
|
695
|
+
const dragState = React.useRef(null);
|
|
696
|
+
const suppressClickFocus = React.useRef(false);
|
|
697
|
+
const unitOptions = React.useMemo(() => getSupportedUnits(control), [control]);
|
|
698
|
+
const handleStyle = {
|
|
699
|
+
left: `max(5px, ${percent}% - 9px)`,
|
|
700
|
+
"--chisel-handle-opacity": percent < 18 ? "0.18" : "1",
|
|
701
|
+
"--chisel-handle-scale": percent < 18 ? "0.25" : "1"
|
|
702
|
+
};
|
|
703
|
+
React.useEffect(() => {
|
|
704
|
+
if (!isEditing) {
|
|
705
|
+
setDraft(displayValue);
|
|
706
|
+
setIsInvalid(false);
|
|
707
|
+
}
|
|
708
|
+
}, [displayValue, isEditing]);
|
|
709
|
+
const commitDraft = (nextDraft = draft) => {
|
|
710
|
+
const normalized = normalizeStyleInput(nextDraft, control, element, inferredUnit);
|
|
711
|
+
if (!normalized) {
|
|
712
|
+
setIsInvalid(true);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
setDraft(normalized);
|
|
716
|
+
setIsInvalid(false);
|
|
717
|
+
onValueChange(normalized);
|
|
718
|
+
};
|
|
719
|
+
const focusValueInput = () => {
|
|
720
|
+
inputRef.current?.focus();
|
|
721
|
+
inputRef.current?.select();
|
|
722
|
+
};
|
|
723
|
+
const applyPointerValue = (clientX, rect) => {
|
|
724
|
+
const nextValue = getSliderValueFromPointer(clientX, rect, control);
|
|
725
|
+
setDraft(`${formatNumber(nextValue)}${control.unit}`);
|
|
726
|
+
setIsInvalid(false);
|
|
727
|
+
onSlide(nextValue);
|
|
728
|
+
};
|
|
729
|
+
const openUnitMenu = () => {
|
|
730
|
+
if (unitOptions.length <= 1)
|
|
731
|
+
return;
|
|
732
|
+
setUnitMenuOpen(true);
|
|
733
|
+
focusValueInput();
|
|
734
|
+
};
|
|
735
|
+
const applyUnit = (unit) => {
|
|
736
|
+
const nextValue = convertStyleInputToUnit(draft, unit, control, element, inferredUnit);
|
|
737
|
+
if (!nextValue) {
|
|
738
|
+
setIsInvalid(true);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
setDraft(nextValue);
|
|
742
|
+
setIsInvalid(false);
|
|
743
|
+
onValueChange(nextValue);
|
|
744
|
+
setUnitMenuOpen(false);
|
|
745
|
+
inputRef.current?.focus();
|
|
746
|
+
};
|
|
747
|
+
return (_jsxs("div", { className: "chisel-slider", "data-units-open": unitMenuOpen, onDoubleClick: (event) => {
|
|
748
|
+
event.preventDefault();
|
|
749
|
+
event.stopPropagation();
|
|
750
|
+
openUnitMenu();
|
|
751
|
+
}, onClick: () => {
|
|
752
|
+
if (suppressClickFocus.current) {
|
|
753
|
+
suppressClickFocus.current = false;
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
window.setTimeout(focusValueInput, 0);
|
|
757
|
+
}, onMouseUp: () => {
|
|
758
|
+
if (!suppressClickFocus.current)
|
|
759
|
+
window.setTimeout(focusValueInput, 0);
|
|
760
|
+
}, onPointerCancel: (event) => {
|
|
761
|
+
if (dragState.current?.pointerId !== event.pointerId)
|
|
762
|
+
return;
|
|
763
|
+
dragState.current = null;
|
|
764
|
+
}, onPointerDown: (event) => {
|
|
765
|
+
event.preventDefault();
|
|
766
|
+
dragState.current = {
|
|
767
|
+
pointerId: event.pointerId,
|
|
768
|
+
startX: event.clientX,
|
|
769
|
+
rect: event.currentTarget.getBoundingClientRect(),
|
|
770
|
+
dragging: false
|
|
771
|
+
};
|
|
772
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
773
|
+
window.setTimeout(focusValueInput, 0);
|
|
774
|
+
}, onPointerMove: (event) => {
|
|
775
|
+
const state = dragState.current;
|
|
776
|
+
if (!state || state.pointerId !== event.pointerId)
|
|
777
|
+
return;
|
|
778
|
+
if (!state.dragging && Math.abs(event.clientX - state.startX) < 4)
|
|
779
|
+
return;
|
|
780
|
+
state.dragging = true;
|
|
781
|
+
inputRef.current?.blur();
|
|
782
|
+
event.preventDefault();
|
|
783
|
+
applyPointerValue(event.clientX, state.rect);
|
|
784
|
+
}, onPointerUp: (event) => {
|
|
785
|
+
const state = dragState.current;
|
|
786
|
+
if (!state || state.pointerId !== event.pointerId)
|
|
787
|
+
return;
|
|
788
|
+
if (state.dragging) {
|
|
789
|
+
suppressClickFocus.current = true;
|
|
790
|
+
window.setTimeout(() => {
|
|
791
|
+
suppressClickFocus.current = false;
|
|
792
|
+
}, 250);
|
|
793
|
+
applyPointerValue(event.clientX, state.rect);
|
|
794
|
+
}
|
|
795
|
+
if (!state.dragging)
|
|
796
|
+
window.setTimeout(focusValueInput, 0);
|
|
797
|
+
dragState.current = null;
|
|
798
|
+
}, children: [_jsx("span", { className: "chisel-slider-label", children: control.label }), _jsx("span", { className: "chisel-slider-hashmarks", "aria-hidden": "true", children: Array.from({ length: 9 }, (_, index) => (_jsx("span", { className: "chisel-slider-hashmark", style: { left: `${(index + 1) * 10}%` } }, index))) }), _jsx("span", { className: "chisel-slider-fill", style: { width: `${percent}%` } }), _jsx("span", { className: "chisel-slider-handle", style: handleStyle }), _jsx("input", { type: "range", min: control.min, max: control.max, step: control.step, value: clamped, onChange: (event) => onSlide(Number(event.target.value)) }), _jsx("input", { ref: inputRef, className: "chisel-slider-value-input", "aria-label": `${control.label} value`, autoCapitalize: "off", autoComplete: "off", autoCorrect: "off", "data-invalid": isInvalid, spellCheck: false, value: draft, onBlur: () => {
|
|
799
|
+
commitDraft();
|
|
800
|
+
setIsEditing(false);
|
|
801
|
+
setUnitMenuOpen(false);
|
|
802
|
+
}, onChange: (event) => {
|
|
803
|
+
const nextDraft = event.target.value;
|
|
804
|
+
setDraft(nextDraft);
|
|
805
|
+
const normalized = normalizeStyleInput(nextDraft, control, element, inferredUnit);
|
|
806
|
+
if (!normalized) {
|
|
807
|
+
setIsInvalid(nextDraft.trim().length > 0);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
setIsInvalid(false);
|
|
811
|
+
onValueChange(normalized);
|
|
812
|
+
if (normalized !== nextDraft && isCompleteUnitConversion(nextDraft))
|
|
813
|
+
setDraft(normalized);
|
|
814
|
+
}, onFocus: (event) => {
|
|
815
|
+
setIsEditing(true);
|
|
816
|
+
event.currentTarget.select();
|
|
817
|
+
}, onMouseDown: (event) => {
|
|
818
|
+
window.setTimeout(() => {
|
|
819
|
+
event.currentTarget.focus();
|
|
820
|
+
event.currentTarget.select();
|
|
821
|
+
}, 0);
|
|
822
|
+
}, onMouseUp: (event) => {
|
|
823
|
+
window.setTimeout(() => {
|
|
824
|
+
event.currentTarget.focus();
|
|
825
|
+
event.currentTarget.select();
|
|
826
|
+
}, 0);
|
|
827
|
+
}, onDoubleClick: (event) => {
|
|
828
|
+
event.preventDefault();
|
|
829
|
+
event.stopPropagation();
|
|
830
|
+
openUnitMenu();
|
|
831
|
+
}, onKeyDown: (event) => {
|
|
832
|
+
if (event.key === "Enter") {
|
|
833
|
+
event.preventDefault();
|
|
834
|
+
commitDraft();
|
|
835
|
+
event.currentTarget.blur();
|
|
836
|
+
}
|
|
837
|
+
if (isArrowStepKey(event.key)) {
|
|
838
|
+
const nextValue = stepStyleInput(draft, control, element, inferredUnit, event.shiftKey ? 10 : 1, isIncrementKey(event.key));
|
|
839
|
+
if (!nextValue)
|
|
840
|
+
return;
|
|
841
|
+
event.preventDefault();
|
|
842
|
+
setDraft(nextValue);
|
|
843
|
+
setIsInvalid(false);
|
|
844
|
+
onValueChange(nextValue);
|
|
845
|
+
}
|
|
846
|
+
if (event.key === "Escape") {
|
|
847
|
+
event.preventDefault();
|
|
848
|
+
setDraft(displayValue);
|
|
849
|
+
setIsInvalid(false);
|
|
850
|
+
setUnitMenuOpen(false);
|
|
851
|
+
event.currentTarget.blur();
|
|
852
|
+
}
|
|
853
|
+
} }), unitMenuOpen ? (_jsx("div", { className: "chisel-unit-menu", role: "listbox", "aria-label": `${control.label} units`, children: unitOptions.map((unit) => (_jsx("button", { className: "chisel-unit-option", "data-active": unit === inferredUnit, onClick: (event) => {
|
|
854
|
+
event.preventDefault();
|
|
855
|
+
applyUnit(unit);
|
|
856
|
+
}, onPointerDown: (event) => {
|
|
857
|
+
event.preventDefault();
|
|
858
|
+
event.stopPropagation();
|
|
859
|
+
}, role: "option", type: "button", children: unit || "unitless" }, unit || "unitless"))) })) : null] }));
|
|
860
|
+
}
|
|
861
|
+
function ChiselStyles() {
|
|
862
|
+
return (_jsx("style", { children: `
|
|
863
|
+
.chisel-root, .chisel-root * { box-sizing: border-box; }
|
|
864
|
+
.chisel-root { --chisel-surface: rgba(0, 0, 0, 0.04); --chisel-surface-hover: rgba(0, 0, 0, 0.08); --chisel-surface-active: rgba(0, 0, 0, 0.1); --chisel-surface-subtle: rgba(0, 0, 0, 0.06); --chisel-text-root: #000; --chisel-text-section: rgba(0, 0, 0, 0.65); --chisel-text-label: rgba(0, 0, 0, 0.6); --chisel-text-focus: #000; --chisel-text-primary: rgba(0, 0, 0, 0.9); --chisel-text-secondary: rgba(0, 0, 0, 0.55); --chisel-text-tertiary: rgba(0, 0, 0, 0.35); --chisel-border: rgba(0, 0, 0, 0.1); --chisel-border-hover: rgba(0, 0, 0, 0.15); --chisel-glass-bg: rgba(250, 250, 250, 0.88); --chisel-radius: 8px; --chisel-row-height: 36px; --chisel-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); --chisel-motion: 150ms cubic-bezier(0.2, 0.8, 0.2, 1); position: fixed; inset: 0; z-index: 2147483647; pointer-events: none; font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", sans-serif; color: var(--chisel-text-primary); letter-spacing: 0; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
|
|
865
|
+
.chisel-box { position: fixed; left: 0; top: 0; border-radius: 0; pointer-events: none; transition: none; }
|
|
866
|
+
.chisel-box-hover { border: 1px solid #9b2cff; background: rgba(155, 44, 255, 0.08); }
|
|
867
|
+
.chisel-box-selected { border: 2px solid #9b2cff; box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.92), 0 0 0 4px rgba(155, 44, 255, 0.18); }
|
|
868
|
+
.chisel-panel { position: fixed; top: 16px; max-height: calc(100vh - 32px); width: 280px; pointer-events: auto; overflow: visible; }
|
|
869
|
+
.chisel-panel-right { right: 16px; }
|
|
870
|
+
.chisel-panel-left { left: 16px; }
|
|
871
|
+
.chisel-panel-wrapper { display: inline-flex; flex-direction: column; width: 100%; }
|
|
872
|
+
.chisel-panel-inner { width: 280px; max-height: calc(100vh - 32px); overflow-y: auto; padding: 10px 12px 0; border: 1px solid var(--chisel-border); border-radius: 14px; background: var(--chisel-glass-bg); box-shadow: var(--chisel-shadow); -webkit-backdrop-filter: blur(18px) saturate(1.08); backdrop-filter: blur(18px) saturate(1.08); transform: translateZ(0); -ms-overflow-style: none; scrollbar-width: none; }
|
|
873
|
+
.chisel-panel-inner::-webkit-scrollbar { display: none; }
|
|
874
|
+
.chisel-folder { border-bottom: 1px solid var(--chisel-surface-subtle); margin-bottom: 8px; padding-bottom: 8px; }
|
|
875
|
+
.chisel-folder-root { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
|
|
876
|
+
.chisel-panel-header { border-bottom: 1px solid var(--chisel-surface-subtle); margin-bottom: 12px; padding-bottom: 6px; }
|
|
877
|
+
.chisel-folder-header { user-select: none; }
|
|
878
|
+
.chisel-folder-header-top { display: flex; align-items: center; justify-content: space-between; height: var(--chisel-row-height); padding: 0; }
|
|
879
|
+
.chisel-folder-title-row { display: flex; align-items: center; gap: 6px; min-width: 0; flex: 1; overflow: hidden; white-space: nowrap; }
|
|
880
|
+
.chisel-folder-title { color: var(--chisel-text-section); font-size: 13px; font-weight: 600; letter-spacing: -0.01em; transform: translateY(-0.5px); }
|
|
881
|
+
.chisel-folder-title-root { color: var(--chisel-text-root); font-size: 15px; font-weight: 600; transform: translateZ(0); }
|
|
882
|
+
.chisel-folder-count { color: var(--chisel-text-tertiary); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 12px; font-weight: 500; }
|
|
883
|
+
.chisel-folder-content { position: relative; }
|
|
884
|
+
.chisel-folder-inner, .chisel-content, .chisel-empty { display: flex; flex-direction: column; gap: 6px; padding-bottom: 10px; }
|
|
885
|
+
.chisel-folder:not(.chisel-folder-root) { border-top: 1px solid var(--chisel-surface-subtle); border-bottom: 1px solid var(--chisel-surface-subtle); margin-top: 4px; margin-bottom: 4px; padding-bottom: 0; }
|
|
886
|
+
.chisel-folder:not(.chisel-folder-root) + .chisel-folder:not(.chisel-folder-root) { border-top: none; margin-top: -10px; }
|
|
887
|
+
.chisel-panel-toolbar { display: flex; align-items: center; gap: 6px; height: var(--chisel-row-height); min-width: 0; margin-bottom: 6px; overflow: visible; }
|
|
888
|
+
.chisel-toolbar-add { width: var(--chisel-row-height); height: var(--chisel-row-height); flex-shrink: 0; display: flex; align-items: center; justify-content: center; padding: 0; border: none; border-radius: var(--chisel-radius); background: var(--chisel-surface); color: var(--chisel-text-label); font: inherit; font-size: 16px; font-weight: 500; cursor: pointer; transition: background var(--chisel-motion), color var(--chisel-motion), transform var(--chisel-motion); }
|
|
889
|
+
.chisel-toolbar-add:hover, .chisel-preset-trigger:hover, .chisel-button:hover { background: var(--chisel-surface-hover); color: var(--chisel-text-primary); transform: translateY(-1px); }
|
|
890
|
+
.chisel-toolbar-add:active, .chisel-preset-trigger:active, .chisel-button:active { transform: translateY(0) scale(0.96); }
|
|
891
|
+
.chisel-preset-manager { position: relative; flex: 1; min-width: 0; }
|
|
892
|
+
.chisel-preset-trigger { width: 100%; min-width: 0; height: var(--chisel-row-height); display: flex; align-items: center; justify-content: space-between; padding: 0 12px; border: none; border-radius: var(--chisel-radius); background: var(--chisel-surface); color: var(--chisel-text-label); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; transition: background var(--chisel-motion), color var(--chisel-motion), transform var(--chisel-motion); overflow: hidden; }
|
|
893
|
+
.chisel-preset-trigger[aria-expanded="true"] { background: var(--chisel-surface-active); }
|
|
894
|
+
.chisel-preset-trigger-label, .chisel-font-trigger-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
895
|
+
.chisel-chevron { width: 7px; height: 7px; flex-shrink: 0; margin-left: 8px; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; opacity: 0.72; transform: rotate(45deg) translateY(-2px); transition: transform var(--chisel-motion), opacity var(--chisel-motion); }
|
|
896
|
+
.chisel-preset-trigger[aria-expanded="true"] .chisel-chevron, .chisel-font-trigger[aria-expanded="true"] .chisel-chevron { opacity: 1; transform: rotate(225deg) translate(-1px, -1px); }
|
|
897
|
+
.chisel-preset-dropdown { position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 2147483647; padding: 4px; border: 1px solid var(--chisel-border); border-radius: 12px; background: #fff; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); animation: chisel-pop-in var(--chisel-motion); transform-origin: top center; }
|
|
898
|
+
.chisel-preset-list { display: flex; flex-direction: column; gap: 2px; }
|
|
899
|
+
.chisel-preset-item { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 10px; border: none; border-radius: 8px; background: transparent; color: var(--chisel-text-label); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; transition: background var(--chisel-motion), color var(--chisel-motion), transform var(--chisel-motion); }
|
|
900
|
+
.chisel-preset-item:hover { background: var(--chisel-surface-hover); transform: translateX(2px); }
|
|
901
|
+
.chisel-preset-item[data-active="true"] { background: var(--chisel-surface-active); color: var(--chisel-text-primary); }
|
|
902
|
+
.chisel-preset-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
903
|
+
.chisel-preset-count { color: var(--chisel-text-tertiary); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 11px; }
|
|
904
|
+
.chisel-text-control, .chisel-color-control, .chisel-font-control { min-height: var(--chisel-row-height); display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 12px; border-radius: var(--chisel-radius); background: var(--chisel-surface); }
|
|
905
|
+
.chisel-text-label, .chisel-color-label, .chisel-font-label, .chisel-slider-label { color: var(--chisel-text-label); flex-shrink: 0; font-size: 13px; font-weight: 500; line-height: 17px; transform: translateY(-0.5px); }
|
|
906
|
+
.chisel-text-code, .chisel-text-value, .chisel-selector, .chisel-warning, .chisel-color-value-input, .chisel-slider-value-input { min-width: 0; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--chisel-text-label); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 13px; font-weight: 500; text-align: right; }
|
|
907
|
+
.chisel-selector-row { min-height: var(--chisel-row-height); }
|
|
908
|
+
.chisel-selector { max-width: 168px; line-height: 1.25; }
|
|
909
|
+
.chisel-warning { color: #805b12; font-family: inherit; }
|
|
910
|
+
.chisel-textarea-control { height: auto; align-items: flex-start; padding-top: 10px; padding-bottom: 10px; }
|
|
911
|
+
.chisel-text-input { min-width: 0; width: 100%; flex: 1; resize: vertical; border: none; outline: none; background: transparent; color: var(--chisel-text-label); text-align: right; font: inherit; font-size: 13px; font-weight: 500; line-height: 1.35; }
|
|
912
|
+
.chisel-text-input::selection, .chisel-color-value-input::selection, .chisel-slider-value-input::selection { background: rgba(155, 44, 255, 0.22); color: var(--chisel-text-focus); }
|
|
913
|
+
.chisel-text-input:focus { color: var(--chisel-text-focus); }
|
|
914
|
+
.chisel-text-input:disabled { color: var(--chisel-text-tertiary); cursor: not-allowed; }
|
|
915
|
+
.chisel-note-row { min-height: var(--chisel-row-height); display: flex; align-items: center; padding: 9px 12px; border-radius: var(--chisel-radius); background: var(--chisel-surface); color: var(--chisel-text-tertiary); font-size: 13px; font-weight: 500; line-height: 1.35; }
|
|
916
|
+
.chisel-button:disabled { color: var(--chisel-text-tertiary); cursor: not-allowed; transform: none; }
|
|
917
|
+
.chisel-font-control { position: relative; overflow: visible; cursor: default; }
|
|
918
|
+
.chisel-font-picker { min-width: 0; flex: 0 1 150px; }
|
|
919
|
+
.chisel-font-trigger { width: 100%; min-width: 0; height: 28px; display: flex; align-items: center; justify-content: flex-end; padding: 0; border: none; border-radius: 0; background: transparent; color: var(--chisel-text-label); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; transition: color var(--chisel-motion); }
|
|
920
|
+
.chisel-font-trigger:hover, .chisel-font-trigger[aria-expanded="true"] { color: var(--chisel-text-primary); }
|
|
921
|
+
.chisel-font-dropdown { left: 0; right: 0; width: 100%; max-height: 262px; overflow: hidden; }
|
|
922
|
+
.chisel-font-list { max-height: 252px; overflow-y: auto; padding-right: 1px; -ms-overflow-style: none; scrollbar-width: none; }
|
|
923
|
+
.chisel-font-list::-webkit-scrollbar { display: none; }
|
|
924
|
+
.chisel-font-item { justify-content: flex-start; text-align: left; font-size: 14px; }
|
|
925
|
+
.chisel-color-control { position: relative; overflow: visible; flex-wrap: wrap; }
|
|
926
|
+
.chisel-color-control[data-picker-open="true"] { z-index: 25; padding-bottom: 0; }
|
|
927
|
+
.chisel-color-inputs { display: inline-flex; align-items: center; gap: 8px; min-width: 0; flex: 0 0 auto; justify-content: flex-end; }
|
|
928
|
+
.chisel-color-swatch { position: relative; width: 20px; height: 20px; flex-shrink: 0; overflow: hidden; padding: 0; border: 1px solid var(--chisel-border-hover); border-radius: 5px; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.34); cursor: pointer; transition: transform var(--chisel-motion), box-shadow var(--chisel-motion); }
|
|
929
|
+
.chisel-color-control:hover .chisel-color-swatch { transform: scale(1.1); }
|
|
930
|
+
.chisel-color-swatch:active { transform: scale(0.96); }
|
|
931
|
+
.chisel-color-value-input { width: 7.5ch; max-width: 150px; padding: 0; border: 0; outline: none; background: transparent; font-variant-numeric: tabular-nums; text-align: right; }
|
|
932
|
+
.chisel-color-value-input:focus { color: var(--chisel-text-focus); }
|
|
933
|
+
.chisel-color-value-input[data-invalid="true"] { color: #8a1f1f; }
|
|
934
|
+
.chisel-color-popover { flex: 0 0 calc(100% + 24px); width: calc(100% + 24px); display: flex; flex-direction: column; gap: 8px; margin: 4px -12px 0; padding: 6px 12px 0; border-top: 1px solid var(--chisel-surface-subtle); animation: chisel-pop-in var(--chisel-motion); }
|
|
935
|
+
.chisel-color-tabs { height: 32px; display: flex; align-items: flex-start; gap: 14px; border-bottom: 1px solid var(--chisel-surface-subtle); }
|
|
936
|
+
.chisel-color-tab, .chisel-color-close, .chisel-color-saved-row button { border: none; background: transparent; color: var(--chisel-text-label); font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; }
|
|
937
|
+
.chisel-color-tab { height: 32px; padding: 0; border-bottom: 2px solid transparent; }
|
|
938
|
+
.chisel-color-tab[data-active="true"] { color: var(--chisel-text-primary); border-bottom-color: var(--chisel-text-primary); }
|
|
939
|
+
.chisel-color-tab[aria-disabled="true"] { color: var(--chisel-text-tertiary); cursor: default; }
|
|
940
|
+
.chisel-color-close { margin-left: auto; font-size: 18px; line-height: 1; color: var(--chisel-text-tertiary); }
|
|
941
|
+
.chisel-color-close:hover, .chisel-color-saved-row button:hover { color: var(--chisel-text-primary); }
|
|
942
|
+
.chisel-color-field { position: relative; height: 132px; overflow: hidden; border-radius: 9px; cursor: crosshair; touch-action: none; background-image: linear-gradient(90deg, #fff, rgba(255, 255, 255, 0)), linear-gradient(0deg, #000, rgba(0, 0, 0, 0)); }
|
|
943
|
+
.chisel-color-field-cursor { position: absolute; width: 12px; height: 12px; border: 2px solid #fff; border-radius: 999px; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35); transform: translate(-50%, -50%); pointer-events: none; }
|
|
944
|
+
.chisel-color-hue { position: relative; height: 12px; border-radius: 999px; background: linear-gradient(90deg, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00); cursor: pointer; touch-action: none; }
|
|
945
|
+
.chisel-color-hue-cursor { position: absolute; top: 50%; width: 8px; height: 16px; border: 2px solid #fff; border-radius: 999px; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.28); transform: translate(-50%, -50%); pointer-events: none; }
|
|
946
|
+
.chisel-color-alpha { position: relative; height: 12px; overflow: visible; border-radius: 999px; background-color: #fff; background-image: linear-gradient(45deg, rgba(0,0,0,0.12) 25%, transparent 25%), linear-gradient(-45deg, rgba(0,0,0,0.12) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, rgba(0,0,0,0.12) 75%), linear-gradient(-45deg, transparent 75%, rgba(0,0,0,0.12) 75%); background-size: 8px 8px; background-position: 0 0, 0 4px, 4px -4px, -4px 0; cursor: pointer; touch-action: none; }
|
|
947
|
+
.chisel-color-alpha::after { content: ""; position: absolute; inset: 0; border-radius: inherit; background: linear-gradient(90deg, transparent, var(--chisel-alpha-color)); pointer-events: none; }
|
|
948
|
+
.chisel-color-alpha-fill { position: absolute; inset: 0 auto 0 0; border-radius: inherit; background: color-mix(in srgb, var(--chisel-alpha-color), transparent 18%); pointer-events: none; }
|
|
949
|
+
.chisel-color-alpha-cursor { position: absolute; top: 50%; z-index: 2; width: 16px; height: 16px; border: 2px solid #fff; border-radius: 999px; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.22); transform: translate(-50%, -50%); pointer-events: none; }
|
|
950
|
+
.chisel-color-gradient-preview { height: 28px; border: 1px solid var(--chisel-border); border-radius: var(--chisel-radius); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.28); }
|
|
951
|
+
.chisel-color-stop-buttons { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; }
|
|
952
|
+
.chisel-color-stop-button { min-width: 0; height: 32px; display: flex; align-items: center; justify-content: center; gap: 7px; padding: 0 9px; border: none; border-radius: var(--chisel-radius); background: var(--chisel-surface); color: var(--chisel-text-label); font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; transition: background var(--chisel-motion), color var(--chisel-motion), transform var(--chisel-motion); }
|
|
953
|
+
.chisel-color-stop-button:hover { background: var(--chisel-surface-hover); color: var(--chisel-text-primary); transform: translateY(-1px); }
|
|
954
|
+
.chisel-color-stop-button[data-active="true"] { background: var(--chisel-surface-active); color: var(--chisel-text-primary); }
|
|
955
|
+
.chisel-color-stop-swatch { width: 14px; height: 14px; flex-shrink: 0; overflow: hidden; border: 1px solid var(--chisel-border-hover); border-radius: 4px; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.28); }
|
|
956
|
+
.chisel-color-format-row { display: grid; grid-template-columns: 76px minmax(0, 1fr); gap: 6px; }
|
|
957
|
+
.chisel-color-format-select, .chisel-color-format-input { height: 32px; min-width: 0; border: none; border-radius: var(--chisel-radius); outline: none; background: var(--chisel-surface); color: var(--chisel-text-label); font: inherit; font-size: 13px; font-weight: 500; }
|
|
958
|
+
.chisel-color-format-select { padding: 0 8px; cursor: pointer; }
|
|
959
|
+
.chisel-color-format-input { padding: 0 9px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
|
|
960
|
+
.chisel-color-format-input:focus, .chisel-color-format-select:focus { color: var(--chisel-text-focus); background: var(--chisel-surface-hover); }
|
|
961
|
+
.chisel-color-format-input[data-invalid="true"] { color: #8a1f1f; }
|
|
962
|
+
.chisel-color-extra-row, .chisel-color-upload-row { position: relative; min-height: 32px; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 0 9px; border-radius: var(--chisel-radius); background: var(--chisel-surface); color: var(--chisel-text-label); font-size: 13px; font-weight: 500; }
|
|
963
|
+
.chisel-color-extra-row span, .chisel-color-upload-row span:first-child { flex-shrink: 0; }
|
|
964
|
+
.chisel-color-extra-row input { min-width: 0; flex: 1; border: none; outline: none; background: transparent; color: var(--chisel-text-label); font: inherit; text-align: right; }
|
|
965
|
+
.chisel-color-extra-row input:focus { color: var(--chisel-text-focus); }
|
|
966
|
+
.chisel-color-upload-row { cursor: pointer; transition: background var(--chisel-motion), color var(--chisel-motion), transform var(--chisel-motion); }
|
|
967
|
+
.chisel-color-upload-row:hover { background: var(--chisel-surface-hover); color: var(--chisel-text-primary); transform: translateY(-1px); }
|
|
968
|
+
.chisel-color-upload-row input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
|
|
969
|
+
.chisel-color-upload-name { min-width: 0; overflow: hidden; text-align: right; text-overflow: ellipsis; white-space: nowrap; color: var(--chisel-text-tertiary); }
|
|
970
|
+
.chisel-slider { position: relative; height: var(--chisel-row-height); display: block; overflow: hidden; border-radius: var(--chisel-radius); background: var(--chisel-surface); cursor: pointer; user-select: none; touch-action: none; }
|
|
971
|
+
.chisel-slider[data-units-open="true"] { z-index: 20; overflow: visible; }
|
|
972
|
+
.chisel-slider input[type="range"] { position: absolute; inset: 0; z-index: 3; width: 100%; height: 100%; opacity: 0; pointer-events: none; cursor: pointer; }
|
|
973
|
+
.chisel-slider-label { position: absolute; top: 50%; left: 10px; pointer-events: none; transform: translateY(calc(-50% - 0.5px)); z-index: 2; }
|
|
974
|
+
.chisel-slider-value-input { position: absolute; inset: 0; z-index: 4; width: 100%; max-width: none; height: 100%; padding: 0 10px 0 92px; border: 1px solid transparent; border-radius: var(--chisel-radius); outline: none; background: transparent; font-variant-numeric: tabular-nums; cursor: pointer; }
|
|
975
|
+
.chisel-slider-value-input:focus { border-color: var(--chisel-border-hover); background: transparent; color: var(--chisel-text-focus); cursor: text; }
|
|
976
|
+
.chisel-slider-value-input[data-invalid="true"] { border-color: rgba(170, 48, 48, 0.38); background: rgba(170, 48, 48, 0.08); color: #8a1f1f; }
|
|
977
|
+
.chisel-slider-hashmarks { position: absolute; inset: 0; pointer-events: none; }
|
|
978
|
+
.chisel-slider-hashmark { position: absolute; top: 50%; width: 1px; height: 8px; border-radius: 999px; background: transparent; transform: translate(-50%, -50%); transition: background 0.2s; }
|
|
979
|
+
.chisel-slider:hover .chisel-slider-hashmark, .chisel-slider:focus-within .chisel-slider-hashmark { background: rgba(20, 20, 20, 0.12); }
|
|
980
|
+
.chisel-slider-fill { position: absolute; top: 0; bottom: 0; left: 0; background: var(--chisel-surface-active); pointer-events: none; transition: background 0.15s; }
|
|
981
|
+
.chisel-slider-handle { position: absolute; top: 50%; z-index: 2; width: 3px; height: 20px; border-radius: 999px; background: var(--chisel-text-primary); pointer-events: none; opacity: 0; transform: translateY(-50%) scaleX(var(--chisel-handle-scale, 1)); transition: opacity 0.15s, transform 0.15s; }
|
|
982
|
+
.chisel-slider:hover .chisel-slider-handle { opacity: var(--chisel-handle-opacity, 1); }
|
|
983
|
+
.chisel-unit-menu { position: absolute; top: calc(100% + 4px); right: 6px; z-index: 30; display: grid; grid-template-columns: repeat(4, minmax(34px, 1fr)); gap: 2px; width: 174px; padding: 4px; border: 1px solid var(--chisel-border); border-radius: 10px; background: rgba(255, 255, 255, 0.96); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.1); animation: chisel-pop-in var(--chisel-motion); cursor: default; }
|
|
984
|
+
.chisel-unit-option { min-width: 0; height: 26px; border: none; border-radius: 7px; background: transparent; color: var(--chisel-text-label); font: inherit; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 11px; font-weight: 600; cursor: pointer; transition: background var(--chisel-motion), color var(--chisel-motion), transform var(--chisel-motion); }
|
|
985
|
+
.chisel-unit-option:hover { background: var(--chisel-surface-hover); color: var(--chisel-text-primary); transform: scale(1.04); }
|
|
986
|
+
.chisel-unit-option[data-active="true"] { background: var(--chisel-surface-active); color: var(--chisel-text-primary); }
|
|
987
|
+
.chisel-actions { display: flex; flex-direction: column; gap: 6px; padding-bottom: 10px; }
|
|
988
|
+
.chisel-button { width: 100%; min-height: var(--chisel-row-height); border: none; border-radius: var(--chisel-radius); background: var(--chisel-surface); color: var(--chisel-text-secondary); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; transition: background var(--chisel-motion), color var(--chisel-motion), transform var(--chisel-motion); }
|
|
989
|
+
@keyframes chisel-pop-in { from { opacity: 0; transform: translateY(-3px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } }
|
|
990
|
+
@media (prefers-reduced-motion: reduce) {
|
|
991
|
+
.chisel-root, .chisel-root * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
|
|
992
|
+
}
|
|
993
|
+
@media (max-width: 760px) {
|
|
994
|
+
.chisel-panel { top: auto; left: 10px; right: 10px; bottom: 10px; width: auto; max-height: 58vh; }
|
|
995
|
+
.chisel-panel-inner { width: 100%; max-height: 58vh; }
|
|
996
|
+
}
|
|
997
|
+
` }));
|
|
998
|
+
}
|
|
999
|
+
function ensureSnapshot(element, snapshots) {
|
|
1000
|
+
if (snapshots.has(element))
|
|
1001
|
+
return;
|
|
1002
|
+
const computed = getComputedStyle(element);
|
|
1003
|
+
const styles = {};
|
|
1004
|
+
for (const property of STYLE_PROPERTIES) {
|
|
1005
|
+
styles[property] = computed[property];
|
|
1006
|
+
}
|
|
1007
|
+
snapshots.set(element, {
|
|
1008
|
+
text: element.textContent ?? "",
|
|
1009
|
+
styles
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
function createVersion(index) {
|
|
1013
|
+
return {
|
|
1014
|
+
id: `version-${index}`,
|
|
1015
|
+
name: `Version ${index}`,
|
|
1016
|
+
elements: {}
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
function cloneVersionElements(elements) {
|
|
1020
|
+
return Object.fromEntries(Object.entries(elements).map(([key, change]) => [
|
|
1021
|
+
key,
|
|
1022
|
+
{
|
|
1023
|
+
...change,
|
|
1024
|
+
styles: { ...change.styles }
|
|
1025
|
+
}
|
|
1026
|
+
]));
|
|
1027
|
+
}
|
|
1028
|
+
function getRouteStorageKey() {
|
|
1029
|
+
return `vse:versions:${window.location.pathname}${window.location.search}`;
|
|
1030
|
+
}
|
|
1031
|
+
function readStoredVersions(key) {
|
|
1032
|
+
try {
|
|
1033
|
+
const raw = window.localStorage.getItem(key);
|
|
1034
|
+
if (!raw)
|
|
1035
|
+
return [];
|
|
1036
|
+
const parsed = JSON.parse(raw);
|
|
1037
|
+
if (!Array.isArray(parsed))
|
|
1038
|
+
return [];
|
|
1039
|
+
return parsed.filter(isVersionState);
|
|
1040
|
+
}
|
|
1041
|
+
catch {
|
|
1042
|
+
return [];
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
function isVersionState(value) {
|
|
1046
|
+
if (!value || typeof value !== "object")
|
|
1047
|
+
return false;
|
|
1048
|
+
const version = value;
|
|
1049
|
+
return typeof version.id === "string" && typeof version.name === "string" && Boolean(version.elements) && typeof version.elements === "object";
|
|
1050
|
+
}
|
|
1051
|
+
function getElementKey(element) {
|
|
1052
|
+
return getSourceHint(element) ?? getBestSelector(element);
|
|
1053
|
+
}
|
|
1054
|
+
function ensureElementBaseline(element, key, baselines) {
|
|
1055
|
+
if (baselines.has(key))
|
|
1056
|
+
return;
|
|
1057
|
+
const styles = {};
|
|
1058
|
+
for (const property of STYLE_PROPERTIES) {
|
|
1059
|
+
styles[property] = element.style[property] ?? "";
|
|
1060
|
+
}
|
|
1061
|
+
baselines.set(key, {
|
|
1062
|
+
selector: getBestSelector(element),
|
|
1063
|
+
sourceHint: getSourceHint(element),
|
|
1064
|
+
text: element.textContent ?? "",
|
|
1065
|
+
styles
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
function applyVersionToDom(version, baselines) {
|
|
1069
|
+
for (const [key, baseline] of baselines) {
|
|
1070
|
+
const element = findVersionElement(baseline);
|
|
1071
|
+
if (!element)
|
|
1072
|
+
continue;
|
|
1073
|
+
element.textContent = baseline.text;
|
|
1074
|
+
for (const property of STYLE_PROPERTIES) {
|
|
1075
|
+
element.style[property] = baseline.styles[property] ?? "";
|
|
1076
|
+
}
|
|
1077
|
+
baselines.set(key, baseline);
|
|
1078
|
+
}
|
|
1079
|
+
for (const [key, change] of Object.entries(version.elements)) {
|
|
1080
|
+
const element = findVersionElement(change);
|
|
1081
|
+
if (!element)
|
|
1082
|
+
continue;
|
|
1083
|
+
ensureElementBaseline(element, key, baselines);
|
|
1084
|
+
if (typeof change.text === "string")
|
|
1085
|
+
element.textContent = change.text;
|
|
1086
|
+
for (const [property, value] of Object.entries(change.styles)) {
|
|
1087
|
+
element.style[property] = value;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
function findVersionElement(change) {
|
|
1092
|
+
const hintedSelector = change.sourceHint
|
|
1093
|
+
? `[data-chisel-id="${cssEscape(change.sourceHint)}"], [data-ai-edit="${cssEscape(change.sourceHint)}"], [data-component="${cssEscape(change.sourceHint)}"]`
|
|
1094
|
+
: undefined;
|
|
1095
|
+
return queryHTMLElement(hintedSelector) ?? queryHTMLElement(change.selector);
|
|
1096
|
+
}
|
|
1097
|
+
function queryHTMLElement(selector) {
|
|
1098
|
+
if (!selector)
|
|
1099
|
+
return undefined;
|
|
1100
|
+
try {
|
|
1101
|
+
const element = document.querySelector(selector);
|
|
1102
|
+
return element instanceof HTMLElement ? element : undefined;
|
|
1103
|
+
}
|
|
1104
|
+
catch {
|
|
1105
|
+
return undefined;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function toggleElementSelection(current, target) {
|
|
1109
|
+
return current.includes(target) ? current.filter((element) => element !== target) : [...current, target];
|
|
1110
|
+
}
|
|
1111
|
+
function getChanges(element, snapshot) {
|
|
1112
|
+
const computed = getComputedStyle(element);
|
|
1113
|
+
const changes = [];
|
|
1114
|
+
const currentText = element.textContent ?? "";
|
|
1115
|
+
if (currentText !== snapshot.text) {
|
|
1116
|
+
changes.push({ property: "textContent", before: snapshot.text, after: currentText });
|
|
1117
|
+
}
|
|
1118
|
+
for (const property of STYLE_PROPERTIES) {
|
|
1119
|
+
const before = snapshot.styles[property];
|
|
1120
|
+
const authoredAfter = element.style[property] ?? "";
|
|
1121
|
+
const after = authoredAfter || computed[property];
|
|
1122
|
+
if (normalizeStyleValue(before) !== normalizeStyleValue(after)) {
|
|
1123
|
+
changes.push({ property, before, after });
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return changes;
|
|
1127
|
+
}
|
|
1128
|
+
function buildAgentPrompt({ projectName, element, snapshot, rect, selector, sourceHint, changes }) {
|
|
1129
|
+
const intent = {
|
|
1130
|
+
route: window.location.href,
|
|
1131
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
1132
|
+
element: {
|
|
1133
|
+
tagName: element.tagName.toLowerCase(),
|
|
1134
|
+
selector,
|
|
1135
|
+
ancestry: getAncestry(element),
|
|
1136
|
+
boundingRect: rect,
|
|
1137
|
+
textBefore: snapshot.text,
|
|
1138
|
+
textAfter: element.textContent ?? "",
|
|
1139
|
+
sourceHint
|
|
1140
|
+
},
|
|
1141
|
+
changes
|
|
1142
|
+
};
|
|
1143
|
+
const lowConfidence = !sourceHint && !selector.includes("[data-chisel-id") && !selector.includes("#");
|
|
1144
|
+
return [
|
|
1145
|
+
`You are editing ${projectName}. Update the source files for this Next.js app to match the visual edit intent below.`,
|
|
1146
|
+
"",
|
|
1147
|
+
"Do not ship a broad global CSS override. Find the component, JSX, CSS module, Tailwind classes, or style object that owns the selected element and make the smallest source-level change.",
|
|
1148
|
+
lowConfidence ? "Selector confidence is low. Use the route, visible text, DOM ancestry, and nearby structure to find the matching source component before editing." : "A source hint is available. Prefer it when locating the component.",
|
|
1149
|
+
"",
|
|
1150
|
+
"Edit intent:",
|
|
1151
|
+
"```json",
|
|
1152
|
+
JSON.stringify(intent, null, 2),
|
|
1153
|
+
"```",
|
|
1154
|
+
"",
|
|
1155
|
+
"Acceptance criteria:",
|
|
1156
|
+
"- The selected element visually matches the requested text/style changes on the same route.",
|
|
1157
|
+
"- Existing responsive behavior is preserved.",
|
|
1158
|
+
"- No editor-only attributes or temporary inline preview styles are added unless they already belong to the app source."
|
|
1159
|
+
].join("\n");
|
|
1160
|
+
}
|
|
1161
|
+
function buildPageAgentPrompt({ projectName, version, baselines }) {
|
|
1162
|
+
const elements = Object.entries(version.elements).map(([key, change]) => {
|
|
1163
|
+
const baseline = baselines.get(key);
|
|
1164
|
+
const element = findVersionElement(change);
|
|
1165
|
+
return {
|
|
1166
|
+
selector: change.selector,
|
|
1167
|
+
sourceHint: change.sourceHint,
|
|
1168
|
+
tagName: element?.tagName.toLowerCase(),
|
|
1169
|
+
ancestry: element ? getAncestry(element) : undefined,
|
|
1170
|
+
textBefore: baseline?.text,
|
|
1171
|
+
textAfter: change.text,
|
|
1172
|
+
styleChanges: Object.fromEntries(Object.entries(change.styles).map(([property, after]) => [property, { before: baseline?.styles[property] ?? null, after }]))
|
|
1173
|
+
};
|
|
1174
|
+
});
|
|
1175
|
+
return [
|
|
1176
|
+
`You are editing ${projectName}. Update the source files for this Next.js app to match all page-level visual edit intents below.`,
|
|
1177
|
+
"",
|
|
1178
|
+
"Do not ship broad global CSS overrides. Find the owning components, JSX, CSS modules, Tailwind classes, or style objects and make the smallest source-level changes.",
|
|
1179
|
+
"",
|
|
1180
|
+
"Edit intent:",
|
|
1181
|
+
"```json",
|
|
1182
|
+
JSON.stringify({
|
|
1183
|
+
route: window.location.href,
|
|
1184
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
1185
|
+
elements
|
|
1186
|
+
}, null, 2),
|
|
1187
|
+
"```",
|
|
1188
|
+
"",
|
|
1189
|
+
"Acceptance criteria:",
|
|
1190
|
+
"- All changed elements visually match the requested page edits on the same route.",
|
|
1191
|
+
"- Existing responsive behavior is preserved.",
|
|
1192
|
+
"- No editor-only attributes or temporary inline preview styles are added unless they already belong to the app source."
|
|
1193
|
+
].join("\n");
|
|
1194
|
+
}
|
|
1195
|
+
function getSourceHint(element) {
|
|
1196
|
+
return element.dataset.chiselId ?? element.dataset.aiEdit ?? element.dataset.component ?? findClosestSourceHint(element);
|
|
1197
|
+
}
|
|
1198
|
+
function findClosestSourceHint(element) {
|
|
1199
|
+
const parent = element.closest("[data-chisel-id], [data-ai-edit], [data-component]");
|
|
1200
|
+
if (!(parent instanceof HTMLElement) || parent === element)
|
|
1201
|
+
return undefined;
|
|
1202
|
+
return parent.dataset.chiselId ?? parent.dataset.aiEdit ?? parent.dataset.component;
|
|
1203
|
+
}
|
|
1204
|
+
function getBestSelector(element) {
|
|
1205
|
+
const hint = getSourceHint(element);
|
|
1206
|
+
if (hint)
|
|
1207
|
+
return `[data-chisel-id="${cssEscape(hint)}"], [data-ai-edit="${cssEscape(hint)}"], [data-component="${cssEscape(hint)}"]`;
|
|
1208
|
+
if (element.id)
|
|
1209
|
+
return `#${cssEscape(element.id)}`;
|
|
1210
|
+
const parts = [];
|
|
1211
|
+
let current = element;
|
|
1212
|
+
while (current && current !== document.body && parts.length < 5) {
|
|
1213
|
+
let part = current.tagName.toLowerCase();
|
|
1214
|
+
const classNames = Array.from(current.classList).filter(Boolean).slice(0, 2);
|
|
1215
|
+
if (classNames.length)
|
|
1216
|
+
part += classNames.map((name) => `.${cssEscape(name)}`).join("");
|
|
1217
|
+
const parent = current.parentElement;
|
|
1218
|
+
if (parent) {
|
|
1219
|
+
const tagName = current.tagName;
|
|
1220
|
+
const siblings = Array.from(parent.children).filter((child) => child.tagName === tagName);
|
|
1221
|
+
if (siblings.length > 1)
|
|
1222
|
+
part += `:nth-of-type(${siblings.indexOf(current) + 1})`;
|
|
1223
|
+
}
|
|
1224
|
+
parts.unshift(part);
|
|
1225
|
+
current = parent;
|
|
1226
|
+
}
|
|
1227
|
+
return parts.join(" > ");
|
|
1228
|
+
}
|
|
1229
|
+
function getAncestry(element) {
|
|
1230
|
+
const ancestry = [];
|
|
1231
|
+
let current = element;
|
|
1232
|
+
while (current && current !== document.body && ancestry.length < 6) {
|
|
1233
|
+
const text = (current.textContent ?? "").trim().replace(/\s+/g, " ").slice(0, 80);
|
|
1234
|
+
ancestry.push(`${current.tagName.toLowerCase()}${current.id ? `#${current.id}` : ""}${text ? ` "${text}"` : ""}`);
|
|
1235
|
+
current = current.parentElement;
|
|
1236
|
+
}
|
|
1237
|
+
return ancestry;
|
|
1238
|
+
}
|
|
1239
|
+
function rectToJSON(rect) {
|
|
1240
|
+
return {
|
|
1241
|
+
x: round(rect.x),
|
|
1242
|
+
y: round(rect.y),
|
|
1243
|
+
width: round(rect.width),
|
|
1244
|
+
height: round(rect.height),
|
|
1245
|
+
top: round(rect.top),
|
|
1246
|
+
right: round(rect.right),
|
|
1247
|
+
bottom: round(rect.bottom),
|
|
1248
|
+
left: round(rect.left)
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
function isEditorElement(element) {
|
|
1252
|
+
return Boolean(element.closest("[data-chisel-root='true']"));
|
|
1253
|
+
}
|
|
1254
|
+
function cssEscape(value) {
|
|
1255
|
+
if (typeof CSS !== "undefined" && CSS.escape)
|
|
1256
|
+
return CSS.escape(value);
|
|
1257
|
+
return value.replace(/["\\#.:,[\]>~+*^$|= ]/g, "\\$&");
|
|
1258
|
+
}
|
|
1259
|
+
function toHex(color) {
|
|
1260
|
+
const hex = normalizeHexColor(color);
|
|
1261
|
+
if (hex)
|
|
1262
|
+
return hex;
|
|
1263
|
+
const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
|
|
1264
|
+
if (match) {
|
|
1265
|
+
return `#${[match[1], match[2], match[3]]
|
|
1266
|
+
.map((value) => Number(value).toString(16).padStart(2, "0"))
|
|
1267
|
+
.join("")}`;
|
|
1268
|
+
}
|
|
1269
|
+
const computed = computeCSSColor(color);
|
|
1270
|
+
if (!computed || computed === color)
|
|
1271
|
+
return "#ffffff";
|
|
1272
|
+
const computedMatch = computed.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
|
|
1273
|
+
if (!computedMatch)
|
|
1274
|
+
return "#ffffff";
|
|
1275
|
+
return `#${[computedMatch[1], computedMatch[2], computedMatch[3]]
|
|
1276
|
+
.map((value) => Number(value).toString(16).padStart(2, "0"))
|
|
1277
|
+
.join("")}`;
|
|
1278
|
+
}
|
|
1279
|
+
function computeCSSColor(color) {
|
|
1280
|
+
if (typeof document === "undefined")
|
|
1281
|
+
return undefined;
|
|
1282
|
+
const probe = document.createElement("span");
|
|
1283
|
+
probe.style.color = color;
|
|
1284
|
+
if (!probe.style.color)
|
|
1285
|
+
return undefined;
|
|
1286
|
+
probe.style.position = "absolute";
|
|
1287
|
+
probe.style.visibility = "hidden";
|
|
1288
|
+
document.body.appendChild(probe);
|
|
1289
|
+
const computed = getComputedStyle(probe).color;
|
|
1290
|
+
probe.remove();
|
|
1291
|
+
return computed;
|
|
1292
|
+
}
|
|
1293
|
+
function normalizeHexColor(color) {
|
|
1294
|
+
const trimmed = color.trim();
|
|
1295
|
+
const shortHex = trimmed.match(/^#([0-9a-f]{3})$/i);
|
|
1296
|
+
if (shortHex) {
|
|
1297
|
+
return `#${shortHex[1]
|
|
1298
|
+
.split("")
|
|
1299
|
+
.map((value) => `${value}${value}`)
|
|
1300
|
+
.join("")}`.toUpperCase();
|
|
1301
|
+
}
|
|
1302
|
+
if (/^#[0-9a-f]{6}$/i.test(trimmed))
|
|
1303
|
+
return trimmed.toUpperCase();
|
|
1304
|
+
return undefined;
|
|
1305
|
+
}
|
|
1306
|
+
function hexToRgb(hex) {
|
|
1307
|
+
const normalized = normalizeHexColor(hex) ?? "#FFFFFF";
|
|
1308
|
+
const value = normalized.slice(1);
|
|
1309
|
+
return {
|
|
1310
|
+
r: Number.parseInt(value.slice(0, 2), 16),
|
|
1311
|
+
g: Number.parseInt(value.slice(2, 4), 16),
|
|
1312
|
+
b: Number.parseInt(value.slice(4, 6), 16)
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
function rgbToHsv({ r, g, b }) {
|
|
1316
|
+
const red = r / 255;
|
|
1317
|
+
const green = g / 255;
|
|
1318
|
+
const blue = b / 255;
|
|
1319
|
+
const max = Math.max(red, green, blue);
|
|
1320
|
+
const min = Math.min(red, green, blue);
|
|
1321
|
+
const delta = max - min;
|
|
1322
|
+
let hue = 0;
|
|
1323
|
+
if (delta !== 0) {
|
|
1324
|
+
if (max === red)
|
|
1325
|
+
hue = ((green - blue) / delta) % 6;
|
|
1326
|
+
else if (max === green)
|
|
1327
|
+
hue = (blue - red) / delta + 2;
|
|
1328
|
+
else
|
|
1329
|
+
hue = (red - green) / delta + 4;
|
|
1330
|
+
}
|
|
1331
|
+
hue = Math.round(hue * 60);
|
|
1332
|
+
if (hue < 0)
|
|
1333
|
+
hue += 360;
|
|
1334
|
+
return {
|
|
1335
|
+
h: hue,
|
|
1336
|
+
s: max === 0 ? 0 : round((delta / max) * 100),
|
|
1337
|
+
v: round(max * 100)
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
function rgbToHsl({ r, g, b }) {
|
|
1341
|
+
const red = r / 255;
|
|
1342
|
+
const green = g / 255;
|
|
1343
|
+
const blue = b / 255;
|
|
1344
|
+
const max = Math.max(red, green, blue);
|
|
1345
|
+
const min = Math.min(red, green, blue);
|
|
1346
|
+
const lightness = (max + min) / 2;
|
|
1347
|
+
const delta = max - min;
|
|
1348
|
+
if (delta === 0)
|
|
1349
|
+
return { h: 0, s: 0, l: Math.round(lightness * 100) };
|
|
1350
|
+
const saturation = delta / (1 - Math.abs(2 * lightness - 1));
|
|
1351
|
+
let hue = 0;
|
|
1352
|
+
if (max === red)
|
|
1353
|
+
hue = ((green - blue) / delta) % 6;
|
|
1354
|
+
else if (max === green)
|
|
1355
|
+
hue = (blue - red) / delta + 2;
|
|
1356
|
+
else
|
|
1357
|
+
hue = (red - green) / delta + 4;
|
|
1358
|
+
hue = Math.round(hue * 60);
|
|
1359
|
+
if (hue < 0)
|
|
1360
|
+
hue += 360;
|
|
1361
|
+
return { h: hue, s: Math.round(saturation * 100), l: Math.round(lightness * 100) };
|
|
1362
|
+
}
|
|
1363
|
+
function hsvToHex(hue, saturation, brightness) {
|
|
1364
|
+
const chroma = (brightness / 100) * (saturation / 100);
|
|
1365
|
+
const x = chroma * (1 - Math.abs(((hue / 60) % 2) - 1));
|
|
1366
|
+
const match = brightness / 100 - chroma;
|
|
1367
|
+
const sector = Math.floor(hue / 60) % 6;
|
|
1368
|
+
const channels = [
|
|
1369
|
+
[chroma, x, 0],
|
|
1370
|
+
[x, chroma, 0],
|
|
1371
|
+
[0, chroma, x],
|
|
1372
|
+
[0, x, chroma],
|
|
1373
|
+
[x, 0, chroma],
|
|
1374
|
+
[chroma, 0, x]
|
|
1375
|
+
][sector] ?? [chroma, 0, x];
|
|
1376
|
+
return `#${channels
|
|
1377
|
+
.map((channel) => Math.round((channel + match) * 255).toString(16).padStart(2, "0"))
|
|
1378
|
+
.join("")}`.toUpperCase();
|
|
1379
|
+
}
|
|
1380
|
+
function normalizeStyleValue(value) {
|
|
1381
|
+
return (value ?? "").replace(/\s+/g, " ").trim();
|
|
1382
|
+
}
|
|
1383
|
+
function getFontFamilyLabel(value) {
|
|
1384
|
+
const firstFamily = value
|
|
1385
|
+
.split(",")[0]
|
|
1386
|
+
?.replace(/^["']|["']$/g, "")
|
|
1387
|
+
.trim();
|
|
1388
|
+
return firstFamily;
|
|
1389
|
+
}
|
|
1390
|
+
function getFontFamilyOptions(value) {
|
|
1391
|
+
const options = [];
|
|
1392
|
+
const seen = new Set();
|
|
1393
|
+
const addOption = (option) => {
|
|
1394
|
+
const key = getFontFamilyLabel(option.value)?.toLowerCase() ?? option.label.toLowerCase();
|
|
1395
|
+
if (seen.has(key))
|
|
1396
|
+
return;
|
|
1397
|
+
seen.add(key);
|
|
1398
|
+
options.push(option);
|
|
1399
|
+
};
|
|
1400
|
+
for (const option of FONT_FAMILY_OPTIONS)
|
|
1401
|
+
addOption(option);
|
|
1402
|
+
if (value && !options.some((option) => isMatchingFontFamily(value, option.value))) {
|
|
1403
|
+
const label = getFontFamilyLabel(value);
|
|
1404
|
+
if (label)
|
|
1405
|
+
options.unshift({ label, value });
|
|
1406
|
+
}
|
|
1407
|
+
return options;
|
|
1408
|
+
}
|
|
1409
|
+
function getMatchingFontOption(value, options) {
|
|
1410
|
+
return options.find((option) => isMatchingFontFamily(value, option.value));
|
|
1411
|
+
}
|
|
1412
|
+
function isMatchingFontFamily(firstValue, secondValue) {
|
|
1413
|
+
const first = getFontFamilyLabel(firstValue)?.toLowerCase();
|
|
1414
|
+
const second = getFontFamilyLabel(secondValue)?.toLowerCase();
|
|
1415
|
+
if (first && second && first === second)
|
|
1416
|
+
return true;
|
|
1417
|
+
return normalizeStyleValue(firstValue) === normalizeStyleValue(secondValue);
|
|
1418
|
+
}
|
|
1419
|
+
function normalizeColorInput(value) {
|
|
1420
|
+
const trimmed = value.trim();
|
|
1421
|
+
if (!trimmed)
|
|
1422
|
+
return undefined;
|
|
1423
|
+
if (isSupportedCSSValue("color", trimmed))
|
|
1424
|
+
return trimmed;
|
|
1425
|
+
return undefined;
|
|
1426
|
+
}
|
|
1427
|
+
function getColorDisplayValue(value) {
|
|
1428
|
+
return normalizeHexColor(value) ?? value;
|
|
1429
|
+
}
|
|
1430
|
+
function parseGradientValue(value) {
|
|
1431
|
+
const trimmed = value.trim();
|
|
1432
|
+
if (!trimmed.toLowerCase().startsWith("linear-gradient(") || !trimmed.endsWith(")"))
|
|
1433
|
+
return undefined;
|
|
1434
|
+
const content = trimmed.slice(trimmed.indexOf("(") + 1, -1);
|
|
1435
|
+
const parts = splitCSSFunctionArgs(content);
|
|
1436
|
+
if (parts.length < 3)
|
|
1437
|
+
return undefined;
|
|
1438
|
+
return {
|
|
1439
|
+
start: parts[1].trim(),
|
|
1440
|
+
end: parts.slice(2).join(", ").trim()
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
function parseImageUrlValue(value) {
|
|
1444
|
+
const trimmed = value.trim();
|
|
1445
|
+
if (!trimmed.toLowerCase().startsWith("url(") || !trimmed.endsWith(")"))
|
|
1446
|
+
return undefined;
|
|
1447
|
+
let content = trimmed.slice(trimmed.indexOf("(") + 1, -1).trim();
|
|
1448
|
+
const quote = content[0];
|
|
1449
|
+
if ((quote === "\"" || quote === "'") && content.endsWith(quote)) {
|
|
1450
|
+
content = content.slice(1, -1);
|
|
1451
|
+
}
|
|
1452
|
+
return content.replace(/\\(["'\\])/g, "$1");
|
|
1453
|
+
}
|
|
1454
|
+
function splitCSSFunctionArgs(value) {
|
|
1455
|
+
const parts = [];
|
|
1456
|
+
let current = "";
|
|
1457
|
+
let depth = 0;
|
|
1458
|
+
let quote;
|
|
1459
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1460
|
+
const char = value[index];
|
|
1461
|
+
const previous = value[index - 1];
|
|
1462
|
+
if (quote) {
|
|
1463
|
+
current += char;
|
|
1464
|
+
if (char === quote && previous !== "\\")
|
|
1465
|
+
quote = undefined;
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
if (char === "\"" || char === "'") {
|
|
1469
|
+
quote = char;
|
|
1470
|
+
current += char;
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
if (char === "(")
|
|
1474
|
+
depth += 1;
|
|
1475
|
+
if (char === ")")
|
|
1476
|
+
depth = Math.max(0, depth - 1);
|
|
1477
|
+
if (char === "," && depth === 0) {
|
|
1478
|
+
parts.push(current.trim());
|
|
1479
|
+
current = "";
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
current += char;
|
|
1483
|
+
}
|
|
1484
|
+
if (current.trim())
|
|
1485
|
+
parts.push(current.trim());
|
|
1486
|
+
return parts;
|
|
1487
|
+
}
|
|
1488
|
+
function inferColorFormat(value) {
|
|
1489
|
+
const normalized = value.trim().toLowerCase();
|
|
1490
|
+
if (normalized.startsWith("hsl"))
|
|
1491
|
+
return "HSL";
|
|
1492
|
+
if (normalized.startsWith("rgba"))
|
|
1493
|
+
return "RGBA";
|
|
1494
|
+
if (normalized.startsWith("rgb"))
|
|
1495
|
+
return "RGB";
|
|
1496
|
+
return "Hex";
|
|
1497
|
+
}
|
|
1498
|
+
function formatColorValue(hex, alpha, format) {
|
|
1499
|
+
const normalizedHex = (normalizeHexColor(hex) ?? toHex(hex)).toUpperCase();
|
|
1500
|
+
const { r, g, b } = hexToRgb(normalizedHex);
|
|
1501
|
+
if (format === "RGB")
|
|
1502
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
1503
|
+
if (format === "RGBA")
|
|
1504
|
+
return colorWithAlpha(normalizedHex, alpha);
|
|
1505
|
+
if (format === "HSL") {
|
|
1506
|
+
const { h, s, l } = rgbToHsl({ r, g, b });
|
|
1507
|
+
return alpha < 1 ? `hsl(${h} ${s}% ${l}% / ${formatNumber(alpha)})` : `hsl(${h} ${s}% ${l}%)`;
|
|
1508
|
+
}
|
|
1509
|
+
return normalizedHex;
|
|
1510
|
+
}
|
|
1511
|
+
function getColorAlpha(value) {
|
|
1512
|
+
const rgba = computeCSSColor(value) ?? value;
|
|
1513
|
+
const match = rgba.match(/rgba?\([^,]+,\s*[^,]+,\s*[^,]+(?:,\s*([^)]+))?\)/i);
|
|
1514
|
+
if (!match?.[1])
|
|
1515
|
+
return 1;
|
|
1516
|
+
const alpha = Number.parseFloat(match[1]);
|
|
1517
|
+
return Number.isFinite(alpha) ? clamp(alpha, 0, 1) : 1;
|
|
1518
|
+
}
|
|
1519
|
+
function colorWithAlpha(hex, alpha) {
|
|
1520
|
+
const { r, g, b } = hexToRgb(hex);
|
|
1521
|
+
return `rgba(${r}, ${g}, ${b}, ${formatNumber(alpha)})`;
|
|
1522
|
+
}
|
|
1523
|
+
function getGradientValue(start, end) {
|
|
1524
|
+
return `linear-gradient(135deg, ${start}, ${end})`;
|
|
1525
|
+
}
|
|
1526
|
+
function getImageValue(url) {
|
|
1527
|
+
return `url("${url.trim().replace(/["\\]/g, "\\$&")}")`;
|
|
1528
|
+
}
|
|
1529
|
+
function isBackgroundImageValue(value) {
|
|
1530
|
+
return value.startsWith("linear-gradient(") || value.startsWith("url(");
|
|
1531
|
+
}
|
|
1532
|
+
function normalizeStyleInput(value, control, element, inferredUnit = control.unit) {
|
|
1533
|
+
const trimmed = value.trim();
|
|
1534
|
+
if (!trimmed)
|
|
1535
|
+
return undefined;
|
|
1536
|
+
const converted = convertUnitExpression(trimmed, element);
|
|
1537
|
+
const candidate = converted ?? inferUnitForBareNumber(trimmed, control, inferredUnit);
|
|
1538
|
+
if (isSupportedCSSValue(control.cssProperty, candidate))
|
|
1539
|
+
return candidate;
|
|
1540
|
+
return undefined;
|
|
1541
|
+
}
|
|
1542
|
+
function isSupportedCSSValue(property, value) {
|
|
1543
|
+
if (typeof CSS === "undefined" || typeof CSS.supports !== "function")
|
|
1544
|
+
return true;
|
|
1545
|
+
return CSS.supports(property, value);
|
|
1546
|
+
}
|
|
1547
|
+
function isCompleteUnitConversion(value) {
|
|
1548
|
+
return /^-?(?:\d+|\d*\.\d+)(px|rem|em)(px|rem|em)$/i.test(value.trim());
|
|
1549
|
+
}
|
|
1550
|
+
function getSupportedUnits(control) {
|
|
1551
|
+
const units = [];
|
|
1552
|
+
if (isSupportedCSSValue(control.cssProperty, "1"))
|
|
1553
|
+
units.push("");
|
|
1554
|
+
for (const unit of CSS_NUMERIC_UNITS) {
|
|
1555
|
+
if (isSupportedCSSValue(control.cssProperty, `1${unit}`))
|
|
1556
|
+
units.push(unit);
|
|
1557
|
+
}
|
|
1558
|
+
return units;
|
|
1559
|
+
}
|
|
1560
|
+
function convertStyleInputToUnit(value, targetUnit, control, element, inferredUnit) {
|
|
1561
|
+
const normalized = normalizeStyleInput(value, control, element, inferredUnit);
|
|
1562
|
+
if (!normalized)
|
|
1563
|
+
return undefined;
|
|
1564
|
+
const parsed = parseNumericCSSValue(normalized);
|
|
1565
|
+
if (!parsed)
|
|
1566
|
+
return undefined;
|
|
1567
|
+
if (parsed.unit === targetUnit)
|
|
1568
|
+
return normalized;
|
|
1569
|
+
const pxValue = numericValueToPx(parsed.value, parsed.unit, control, element);
|
|
1570
|
+
if (pxValue === undefined)
|
|
1571
|
+
return undefined;
|
|
1572
|
+
const nextAmount = pxToNumericValue(pxValue, targetUnit, control, element);
|
|
1573
|
+
if (nextAmount === undefined)
|
|
1574
|
+
return undefined;
|
|
1575
|
+
const candidate = targetUnit ? `${formatNumber(round(nextAmount))}${targetUnit}` : formatNumber(round(nextAmount));
|
|
1576
|
+
return isSupportedCSSValue(control.cssProperty, candidate) ? candidate : undefined;
|
|
1577
|
+
}
|
|
1578
|
+
function parseNumericCSSValue(value) {
|
|
1579
|
+
const match = value.trim().match(/^(-?(?:\d+|\d*\.\d+))([a-z%]*)$/i);
|
|
1580
|
+
if (!match)
|
|
1581
|
+
return undefined;
|
|
1582
|
+
const numericValue = Number(match[1]);
|
|
1583
|
+
if (!Number.isFinite(numericValue))
|
|
1584
|
+
return undefined;
|
|
1585
|
+
return { value: numericValue, unit: match[2].toLowerCase() };
|
|
1586
|
+
}
|
|
1587
|
+
function numericValueToPx(value, unit, control, element) {
|
|
1588
|
+
if (!unit) {
|
|
1589
|
+
if (control.cssProperty === "line-height")
|
|
1590
|
+
return value * getElementFontSize(element);
|
|
1591
|
+
if (control.cssProperty === "opacity")
|
|
1592
|
+
return value;
|
|
1593
|
+
return undefined;
|
|
1594
|
+
}
|
|
1595
|
+
const unitSize = getUnitSizeInPx(unit, element, control);
|
|
1596
|
+
return unitSize === undefined ? undefined : value * unitSize;
|
|
1597
|
+
}
|
|
1598
|
+
function pxToNumericValue(pxValue, unit, control, element) {
|
|
1599
|
+
if (!unit) {
|
|
1600
|
+
if (control.cssProperty === "line-height")
|
|
1601
|
+
return pxValue / getElementFontSize(element);
|
|
1602
|
+
if (control.cssProperty === "opacity")
|
|
1603
|
+
return pxValue;
|
|
1604
|
+
return undefined;
|
|
1605
|
+
}
|
|
1606
|
+
const unitSize = getUnitSizeInPx(unit, element, control);
|
|
1607
|
+
return unitSize === undefined || unitSize === 0 ? undefined : pxValue / unitSize;
|
|
1608
|
+
}
|
|
1609
|
+
function convertUnitExpression(value, element) {
|
|
1610
|
+
const match = value.match(/^(-?(?:\d+|\d*\.\d+))(px|rem|em)(px|rem|em)$/i);
|
|
1611
|
+
if (!match)
|
|
1612
|
+
return undefined;
|
|
1613
|
+
const amount = Number(match[1]);
|
|
1614
|
+
const fromUnit = match[2].toLowerCase();
|
|
1615
|
+
const toUnit = match[3].toLowerCase();
|
|
1616
|
+
if (!Number.isFinite(amount) || fromUnit === toUnit)
|
|
1617
|
+
return undefined;
|
|
1618
|
+
const fromSize = getUnitSizeInPx(fromUnit, element);
|
|
1619
|
+
const toSize = getUnitSizeInPx(toUnit, element);
|
|
1620
|
+
if (!fromSize || !toSize)
|
|
1621
|
+
return undefined;
|
|
1622
|
+
const pxValue = amount * fromSize;
|
|
1623
|
+
const converted = pxValue / toSize;
|
|
1624
|
+
return `${formatNumber(round(converted))}${toUnit}`;
|
|
1625
|
+
}
|
|
1626
|
+
function inferUnitForBareNumber(value, control, inferredUnit) {
|
|
1627
|
+
if (!isBareNumber(value) || !inferredUnit)
|
|
1628
|
+
return value;
|
|
1629
|
+
if (isSupportedCSSValue(control.cssProperty, value) && control.unit === "")
|
|
1630
|
+
return value;
|
|
1631
|
+
return `${value}${inferredUnit}`;
|
|
1632
|
+
}
|
|
1633
|
+
function isBareNumber(value) {
|
|
1634
|
+
return /^-?(?:\d+|\d*\.\d+)$/.test(value);
|
|
1635
|
+
}
|
|
1636
|
+
function getInferredUnit(value, control) {
|
|
1637
|
+
const match = value.trim().match(/(-?(?:\d+|\d*\.\d+))([a-z%]+)$/i);
|
|
1638
|
+
return match?.[2] ?? control.unit;
|
|
1639
|
+
}
|
|
1640
|
+
function isArrowStepKey(key) {
|
|
1641
|
+
return key === "ArrowUp" || key === "ArrowRight" || key === "ArrowDown" || key === "ArrowLeft";
|
|
1642
|
+
}
|
|
1643
|
+
function isIncrementKey(key) {
|
|
1644
|
+
return key === "ArrowUp" || key === "ArrowRight";
|
|
1645
|
+
}
|
|
1646
|
+
function stepStyleInput(value, control, element, inferredUnit, step, increment) {
|
|
1647
|
+
const match = value.trim().match(/^(-?(?:\d+|\d*\.\d+))([a-z%]*)$/i);
|
|
1648
|
+
if (!match)
|
|
1649
|
+
return undefined;
|
|
1650
|
+
const current = Number(match[1]);
|
|
1651
|
+
if (!Number.isFinite(current))
|
|
1652
|
+
return undefined;
|
|
1653
|
+
const unit = match[2] || inferredUnit;
|
|
1654
|
+
const next = round(current + (increment ? step : -step));
|
|
1655
|
+
const candidate = unit ? `${formatNumber(next)}${unit}` : formatNumber(next);
|
|
1656
|
+
return normalizeStyleInput(candidate, control, element, unit);
|
|
1657
|
+
}
|
|
1658
|
+
function getUnitSizeInPx(unit, element, control) {
|
|
1659
|
+
if (unit === "px")
|
|
1660
|
+
return 1;
|
|
1661
|
+
if (unit === "rem") {
|
|
1662
|
+
const rootSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
1663
|
+
return Number.isFinite(rootSize) && rootSize > 0 ? rootSize : 16;
|
|
1664
|
+
}
|
|
1665
|
+
if (unit === "em") {
|
|
1666
|
+
const base = control?.cssProperty === "font-size" ? element.parentElement ?? element : element;
|
|
1667
|
+
const emSize = parseFloat(getComputedStyle(base).fontSize);
|
|
1668
|
+
return Number.isFinite(emSize) && emSize > 0 ? emSize : getUnitSizeInPx("rem", element);
|
|
1669
|
+
}
|
|
1670
|
+
return undefined;
|
|
1671
|
+
}
|
|
1672
|
+
function getElementFontSize(element) {
|
|
1673
|
+
const fontSize = parseFloat(getComputedStyle(element).fontSize);
|
|
1674
|
+
return Number.isFinite(fontSize) && fontSize > 0 ? fontSize : 16;
|
|
1675
|
+
}
|
|
1676
|
+
function clamp(value, min, max) {
|
|
1677
|
+
return Math.min(max, Math.max(min, value));
|
|
1678
|
+
}
|
|
1679
|
+
function getSliderPercent(value, min, max) {
|
|
1680
|
+
if (max <= min)
|
|
1681
|
+
return 0;
|
|
1682
|
+
return clamp(((value - min) / (max - min)) * 100, 0, 100);
|
|
1683
|
+
}
|
|
1684
|
+
function getSliderValueFromPointer(clientX, rect, control) {
|
|
1685
|
+
const percent = rect.width > 0 ? clamp((clientX - rect.left) / rect.width, 0, 1) : 0;
|
|
1686
|
+
const raw = control.min + percent * (control.max - control.min);
|
|
1687
|
+
return roundToStep(raw, control.step, control.min);
|
|
1688
|
+
}
|
|
1689
|
+
function roundToStep(value, step, min) {
|
|
1690
|
+
if (step <= 0)
|
|
1691
|
+
return value;
|
|
1692
|
+
const stepped = Math.round((value - min) / step) * step + min;
|
|
1693
|
+
const decimals = String(step).includes(".") ? String(step).split(".")[1].length : 0;
|
|
1694
|
+
return Number(stepped.toFixed(decimals));
|
|
1695
|
+
}
|
|
1696
|
+
function round(value) {
|
|
1697
|
+
return Math.round(value * 100) / 100;
|
|
1698
|
+
}
|
|
1699
|
+
function formatNumber(value) {
|
|
1700
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
1701
|
+
}
|