@jxsuite/studio 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/studio.js +4289 -2290
- package/dist/studio.js.map +31 -20
- package/package.json +2 -2
- package/src/panels/overlays.js +9 -1
- package/src/panels/right-panel.js +27 -1
- package/src/panels/statusbar.js +15 -1
- package/src/panels/toolbar.js +7 -1
- package/src/store.js +35 -0
- package/src/studio.js +96 -258
- package/src/utils/canvas-media.js +151 -0
- package/src/view.js +23 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/** Canvas media/breakpoint utilities — pure functions extracted for testability. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Classify $media entries into size breakpoints (get a canvas each) and feature queries (rendered
|
|
5
|
+
* as toolbar toggles).
|
|
6
|
+
*
|
|
7
|
+
* @param {Record<string, string> | null | undefined} mediaDef
|
|
8
|
+
* @returns {{
|
|
9
|
+
* sizeBreakpoints: { name: string; query: string; width: number; type: string }[];
|
|
10
|
+
* featureQueries: { name: string; query: string }[];
|
|
11
|
+
* baseWidth: number;
|
|
12
|
+
* }}
|
|
13
|
+
*/
|
|
14
|
+
export function parseMediaEntries(mediaDef) {
|
|
15
|
+
if (!mediaDef) return { sizeBreakpoints: [], featureQueries: [], baseWidth: 320 };
|
|
16
|
+
const sizes = [],
|
|
17
|
+
features = [];
|
|
18
|
+
let baseWidth = 320;
|
|
19
|
+
for (const [name, query] of Object.entries(mediaDef)) {
|
|
20
|
+
if (name === "--") {
|
|
21
|
+
const wm = String(query).match(/^(\d+)\s*px$/);
|
|
22
|
+
baseWidth = wm ? parseFloat(wm[1]) : 320;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const minMatch = query.match(/min-width:\s*([\d.]+)px/);
|
|
26
|
+
const maxMatch = query.match(/max-width:\s*([\d.]+)px/);
|
|
27
|
+
if (minMatch) sizes.push({ name, query, width: parseFloat(minMatch[1]), type: "min" });
|
|
28
|
+
else if (maxMatch) sizes.push({ name, query, width: parseFloat(maxMatch[1]), type: "max" });
|
|
29
|
+
else features.push({ name, query });
|
|
30
|
+
}
|
|
31
|
+
sizes.sort((a, b) => (a.type === "min" ? a.width - b.width : b.width - a.width));
|
|
32
|
+
return { sizeBreakpoints: sizes, featureQueries: features, baseWidth };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Compute which named breakpoints are active at a given canvas width.
|
|
37
|
+
*
|
|
38
|
+
* @param {{ name: string; width: number; type: string }[]} sizeBreakpoints
|
|
39
|
+
* @param {number} canvasWidth
|
|
40
|
+
* @returns {Set<string>}
|
|
41
|
+
*/
|
|
42
|
+
export function activeBreakpointsForWidth(sizeBreakpoints, canvasWidth) {
|
|
43
|
+
const active = new Set();
|
|
44
|
+
for (const bp of sizeBreakpoints) {
|
|
45
|
+
if (bp.type === "min" && canvasWidth >= bp.width) active.add(bp.name);
|
|
46
|
+
else if (bp.type === "max" && canvasWidth <= bp.width) active.add(bp.name);
|
|
47
|
+
}
|
|
48
|
+
return active;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Apply styles to a canvas element, including active media overrides. Base (flat) styles applied
|
|
53
|
+
* first, then matching media overrides in source order.
|
|
54
|
+
*
|
|
55
|
+
* @param {HTMLElement} el
|
|
56
|
+
* @param {Record<string, any>} styleDef
|
|
57
|
+
* @param {Set<string>} activeBreakpoints
|
|
58
|
+
* @param {Record<string, boolean>} featureToggles
|
|
59
|
+
*/
|
|
60
|
+
export function applyCanvasStyle(el, styleDef, activeBreakpoints, featureToggles) {
|
|
61
|
+
if (!styleDef || typeof styleDef !== "object") return;
|
|
62
|
+
for (const [prop, val] of Object.entries(styleDef)) {
|
|
63
|
+
if (typeof val === "string" || typeof val === "number") {
|
|
64
|
+
try {
|
|
65
|
+
if (prop.startsWith("--")) el.style.setProperty(prop, String(val));
|
|
66
|
+
else /** @type {any} */ (el.style)[prop] = val;
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const [key, val] of Object.entries(styleDef)) {
|
|
71
|
+
if (!key.startsWith("@") || typeof val !== "object") continue;
|
|
72
|
+
const mediaName = key.slice(1);
|
|
73
|
+
if (mediaName === "--") continue;
|
|
74
|
+
if (activeBreakpoints.has(mediaName) || featureToggles[mediaName]) {
|
|
75
|
+
for (const [prop, v] of Object.entries(/** @type {any} */ (val))) {
|
|
76
|
+
if (typeof v === "string" || typeof v === "number") {
|
|
77
|
+
try {
|
|
78
|
+
if (prop.startsWith("--")) el.style.setProperty(prop, String(v));
|
|
79
|
+
else /** @type {any} */ (el.style)[prop] = v;
|
|
80
|
+
} catch {}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Scan stylesheets for @media rules matching active breakpoints, collecting the CSS declarations
|
|
89
|
+
* that should be applied as inline overrides per data-jx element.
|
|
90
|
+
*
|
|
91
|
+
* Returns a Map of data-jx uid → Map of CSS property → value.
|
|
92
|
+
*
|
|
93
|
+
* @param {Iterable<CSSStyleSheet>} styleSheets
|
|
94
|
+
* @param {Set<string>} activeBreakpoints
|
|
95
|
+
* @returns {Map<string, Map<string, string>>}
|
|
96
|
+
*/
|
|
97
|
+
export function collectMediaOverrides(styleSheets, activeBreakpoints) {
|
|
98
|
+
/** @type {Map<string, Map<string, string>>} */
|
|
99
|
+
const overrides = new Map();
|
|
100
|
+
if (!activeBreakpoints.size) return overrides;
|
|
101
|
+
|
|
102
|
+
for (const sheet of styleSheets) {
|
|
103
|
+
/** @type {CSSRuleList | null} */
|
|
104
|
+
let rules;
|
|
105
|
+
try {
|
|
106
|
+
rules = sheet.cssRules;
|
|
107
|
+
} catch {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!rules) continue;
|
|
111
|
+
for (let ri = 0; ri < rules.length; ri++) {
|
|
112
|
+
const rule = rules[ri];
|
|
113
|
+
if (!(rule instanceof CSSMediaRule)) continue;
|
|
114
|
+
if (!activeBreakpoints.has(rule.conditionText)) continue;
|
|
115
|
+
for (let mi = 0; mi < rule.cssRules.length; mi++) {
|
|
116
|
+
const mediaRule = rule.cssRules[mi];
|
|
117
|
+
if (!(mediaRule instanceof CSSStyleRule)) continue;
|
|
118
|
+
const selector = mediaRule.selectorText;
|
|
119
|
+
const jxMatch = selector.match(/\[data-jx="([^"]+)"\]/);
|
|
120
|
+
if (!jxMatch) continue;
|
|
121
|
+
const uid = jxMatch[1];
|
|
122
|
+
if (!overrides.has(uid)) overrides.set(uid, new Map());
|
|
123
|
+
const props = /** @type {Map<string, string>} */ (overrides.get(uid));
|
|
124
|
+
for (let i = 0; i < mediaRule.style.length; i++) {
|
|
125
|
+
const prop = mediaRule.style[i];
|
|
126
|
+
props.set(prop, mediaRule.style.getPropertyValue(prop));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return overrides;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Apply collected media overrides to elements within a canvas.
|
|
136
|
+
*
|
|
137
|
+
* @param {Element} canvasEl
|
|
138
|
+
* @param {Map<string, Map<string, string>>} overrides
|
|
139
|
+
*/
|
|
140
|
+
export function applyOverridesToCanvas(canvasEl, overrides) {
|
|
141
|
+
for (const [uid, props] of overrides) {
|
|
142
|
+
const els = canvasEl.querySelectorAll(`[data-jx="${uid}"]`);
|
|
143
|
+
for (const el of els) {
|
|
144
|
+
for (const [prop, val] of props) {
|
|
145
|
+
try {
|
|
146
|
+
/** @type {HTMLElement} */ (el).style.setProperty(prop, val);
|
|
147
|
+
} catch {}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/view.js
CHANGED
|
@@ -42,4 +42,27 @@ export const view = {
|
|
|
42
42
|
// Pseudo-state preview
|
|
43
43
|
forcedStyleTag: null,
|
|
44
44
|
forcedAttrEl: null,
|
|
45
|
+
|
|
46
|
+
// Left panel / elements UI
|
|
47
|
+
elementsCollapsed: new Set(),
|
|
48
|
+
elementsFilter: "",
|
|
49
|
+
|
|
50
|
+
// Drag interaction
|
|
51
|
+
lastDragInput: null,
|
|
52
|
+
_currentDropTargetRow: null,
|
|
53
|
+
layerDragSourceHeight: 0,
|
|
54
|
+
|
|
55
|
+
// Editor state
|
|
56
|
+
savedRange: null,
|
|
57
|
+
_completionRegistered: false,
|
|
58
|
+
|
|
59
|
+
// Canvas / stylebook
|
|
60
|
+
stylebookElToTag: new WeakMap(),
|
|
61
|
+
|
|
62
|
+
// Responsive breakpoints UI
|
|
63
|
+
showAddBreakpointForm: false,
|
|
64
|
+
addBreakpointPreview: "",
|
|
65
|
+
|
|
66
|
+
// Autosave
|
|
67
|
+
autosaveTimer: null,
|
|
45
68
|
};
|