@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.
@@ -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
  };