@jxsuite/studio 0.6.2 → 0.8.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.
@@ -0,0 +1,193 @@
1
+ /** Style utilities — pure CSS helper functions used by the style panel. */
2
+
3
+ import { getState, getNodeAtPath } from "../store.js";
4
+ import { camelToKebab } from "../utils/studio-utils.js";
5
+ import cssMeta from "../../data/css-meta.json";
6
+
7
+ /** @type {Map<string, string>} */
8
+ let cssInitialMap = new Map();
9
+
10
+ /** Initialise cssInitialMap from webdata — call once during bootstrap. */
11
+ export function initCssData(/** @type {any} */ webdata) {
12
+ cssInitialMap = new Map(/** @type {any} */ (webdata.cssProps));
13
+ }
14
+
15
+ /** Get the CSS initial-value map (populated by initCssData). */
16
+ export function getCssInitialMap() {
17
+ return cssInitialMap;
18
+ }
19
+
20
+ // ─── Condition helpers ──────────────────────────────────────────────────────
21
+
22
+ /** @param {any} cond @param {any} styles */
23
+ export function conditionPasses(cond, styles) {
24
+ const val = styles[cond.prop] ?? "";
25
+ if (cond.values.length === 0) return val !== "" && val !== "initial";
26
+ return cond.values.includes(val);
27
+ }
28
+
29
+ /** @param {any} entry @param {any} styles */
30
+ export function allConditionsPass(entry, styles) {
31
+ return (entry.$show ?? []).every((/** @type {any} */ c) => conditionPasses(c, styles));
32
+ }
33
+
34
+ // ─── Auto-open sections ─────────────────────────────────────────────────────
35
+
36
+ /** @param {any} node @param {any} currentSections */
37
+ export function autoOpenSections(node, currentSections) {
38
+ const style = node.style || {};
39
+ const result = { ...currentSections };
40
+ for (const prop of Object.keys(style)) {
41
+ if (typeof style[prop] === "object") continue;
42
+ const entry = /** @type {Record<string, any>} */ (cssMeta.$defs)[prop];
43
+ const section = entry?.$section ?? "other";
44
+ if (!result[section]) result[section] = true;
45
+ }
46
+ return result;
47
+ }
48
+
49
+ // ─── Shorthand expand/compress ──────────────────────────────────────────────
50
+
51
+ /** Get longhands for a shorthand property from css-meta */
52
+ export function getLonghands(/** @type {any} */ shorthandProp) {
53
+ const entry = /** @type {Record<string, any>} */ (cssMeta.$defs)[shorthandProp];
54
+ if (entry?.$longhands) {
55
+ return entry.$longhands
56
+ .map((/** @type {string} */ name) => ({
57
+ name,
58
+ entry: /** @type {Record<string, any>} */ (cssMeta.$defs)[name] || { $order: 0 },
59
+ }))
60
+ .sort((/** @type {any} */ a, /** @type {any} */ b) => a.entry.$order - b.entry.$order);
61
+ }
62
+ const result = [];
63
+ for (const [name, e] of /** @type {[string, any][]} */ (Object.entries(cssMeta.$defs))) {
64
+ if (e.$shorthand === shorthandProp) result.push({ name, entry: e });
65
+ }
66
+ result.sort((a, b) => a.entry.$order - b.entry.$order);
67
+ return result;
68
+ }
69
+
70
+ /**
71
+ * Expand a CSS shorthand value into individual longhand values following the standard 1–4 value
72
+ * TRBL pattern.
73
+ */
74
+ export function expandShorthand(/** @type {string} */ shortVal, /** @type {number} */ count) {
75
+ if (!shortVal) return Array(count).fill("");
76
+ const parts = shortVal.trim().split(/\s+/);
77
+ if (count !== 4 || parts.length === 0) return Array(count).fill("");
78
+ if (parts.length === 1) return [parts[0], parts[0], parts[0], parts[0]];
79
+ if (parts.length === 2) return [parts[0], parts[1], parts[0], parts[1]];
80
+ if (parts.length === 3) return [parts[0], parts[1], parts[2], parts[1]];
81
+ return [parts[0], parts[1], parts[2], parts[3]];
82
+ }
83
+
84
+ /** Compress 4 TRBL values back into the shortest valid CSS shorthand string. */
85
+ export function compressShorthand(/** @type {string[]} */ vals) {
86
+ const [t, r, b, l] = vals;
87
+ if (t === r && r === b && b === l) return t;
88
+ if (t === b && r === l) return `${t} ${r}`;
89
+ if (r === l) return `${t} ${r} ${b}`;
90
+ return `${t} ${r} ${b} ${l}`;
91
+ }
92
+
93
+ // ─── Border-side shorthand parsing ──────────────────────────────────────────
94
+
95
+ export const BORDER_STYLES = new Set([
96
+ "none",
97
+ "solid",
98
+ "dashed",
99
+ "dotted",
100
+ "double",
101
+ "groove",
102
+ "ridge",
103
+ "inset",
104
+ "outset",
105
+ "hidden",
106
+ ]);
107
+
108
+ /**
109
+ * Parse a border-side shorthand value into [width, style, color].
110
+ *
111
+ * @param {string} value
112
+ * @returns {string[]}
113
+ */
114
+ export function expandBorderSide(value) {
115
+ if (!value) return ["", "", ""];
116
+ const tokens = [];
117
+ let current = "";
118
+ let depth = 0;
119
+ for (const ch of value.trim()) {
120
+ if (ch === "(") depth++;
121
+ if (ch === ")") depth--;
122
+ if (ch === " " && depth === 0) {
123
+ if (current) tokens.push(current);
124
+ current = "";
125
+ } else {
126
+ current += ch;
127
+ }
128
+ }
129
+ if (current) tokens.push(current);
130
+
131
+ let width = "";
132
+ let style = "";
133
+ let color = "";
134
+
135
+ for (const tok of tokens) {
136
+ if (!style && BORDER_STYLES.has(tok)) {
137
+ style = tok;
138
+ } else if (!width && /^[\d.]/.test(tok)) {
139
+ width = tok;
140
+ } else {
141
+ color = color ? `${color} ${tok}` : tok;
142
+ }
143
+ }
144
+
145
+ return [width, style, color];
146
+ }
147
+
148
+ /**
149
+ * Recompose border-side longhand values into a shorthand string.
150
+ *
151
+ * @param {string[]} vals
152
+ * @returns {string}
153
+ */
154
+ export function compressBorderSide(/** @type {string[]} */ vals) {
155
+ return vals.filter((v) => v && v.trim()).join(" ");
156
+ }
157
+
158
+ // ─── Font helpers ───────────────────────────────────────────────────────────
159
+
160
+ /** Extract --font-* CSS custom properties from the document root style. */
161
+ export function getFontVars() {
162
+ const S = getState();
163
+ const style = S.document?.style;
164
+ if (!style) return [];
165
+ const vars = [];
166
+ for (const [k, v] of Object.entries(style)) {
167
+ if (k.startsWith("--font") && (typeof v === "string" || typeof v === "number")) {
168
+ vars.push({ name: k, value: String(v) });
169
+ }
170
+ }
171
+ return vars;
172
+ }
173
+
174
+ /** Typography CSS properties that should preview their values in-menu */
175
+ export const TYPO_PREVIEW_PROPS = new Set([
176
+ "fontStyle",
177
+ "fontVariant",
178
+ "textTransform",
179
+ "textDecoration",
180
+ ]);
181
+
182
+ /** Resolve the current font family for typography preview (handles var() references) */
183
+ export function currentFontFamily() {
184
+ const S = getState();
185
+ const node = S.selection ? getNodeAtPath(S.document, S.selection) : null;
186
+ const raw = node?.style?.fontFamily;
187
+ if (!raw) return "";
188
+ const m = typeof raw === "string" && raw.match(/^var\((--[^)]+)\)$/);
189
+ if (m) return S.document?.style?.[m[1]] || "";
190
+ return raw;
191
+ }
192
+
193
+ export { cssMeta, camelToKebab };
@@ -0,0 +1,103 @@
1
+ /** Stylebook layers panel — shows element/variable tree when in stylebook (settings) mode. */
2
+
3
+ import { html, nothing } from "lit-html";
4
+ import { getState } from "../store.js";
5
+ import { componentRegistry } from "../files/components.js";
6
+
7
+ /**
8
+ * @param {any} rootStyle
9
+ * @param {string} tag
10
+ */
11
+ function hasTagStyle(rootStyle, tag) {
12
+ const s = rootStyle[`& ${tag}`];
13
+ return s && typeof s === "object" && Object.keys(s).length > 0;
14
+ }
15
+
16
+ /**
17
+ * @param {{ selectStylebookTag: (tag: string) => void; stylebookMeta: any }} ctx
18
+ * @returns {import("lit-html").TemplateResult}
19
+ */
20
+ export function renderStylebookLayersTemplate(ctx) {
21
+ const S = getState();
22
+ const rootStyle = S.document?.style || {};
23
+ const selectedTag = S.ui.stylebookSelection;
24
+
25
+ if (S.ui.stylebookTab === "elements") {
26
+ /**
27
+ * @param {any} entry
28
+ * @param {number} depth
29
+ * @returns {any}
30
+ */
31
+ const renderEntryRow = (entry, depth = 0) => {
32
+ const tag = entry.tag;
33
+ const uniqueChildren = entry.children
34
+ ? [...new Map(entry.children.map((/** @type {any} */ c) => [c.tag, c])).values()]
35
+ : [];
36
+ return html`
37
+ <div
38
+ class="layer-row${tag === selectedTag ? " selected" : ""}"
39
+ style="padding-left:${8 + depth * 16}px"
40
+ @click=${(/** @type {any} */ e) => {
41
+ e.stopPropagation();
42
+ ctx.selectStylebookTag(tag);
43
+ }}
44
+ >
45
+ <span class="layer-tag">${tag}</span>
46
+ <span
47
+ class="layer-label"
48
+ style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1"
49
+ >${entry.text || `<${tag}>`}</span
50
+ >
51
+ ${hasTagStyle(rootStyle, tag)
52
+ ? html`<span
53
+ style="width:6px;height:6px;border-radius:50%;background:var(--accent);flex-shrink:0"
54
+ ></span>`
55
+ : nothing}
56
+ </div>
57
+ ${uniqueChildren.map((/** @type {any} */ child) => renderEntryRow(child, depth + 1))}
58
+ `;
59
+ };
60
+
61
+ /** @type {any[]} */
62
+ const elementRows = [];
63
+ for (const section of ctx.stylebookMeta.$sections) {
64
+ for (const entry of /** @type {any[]} */ (section.elements)) {
65
+ elementRows.push(renderEntryRow(entry, 0));
66
+ }
67
+ }
68
+ const compRows = componentRegistry.map(
69
+ /** @param {any} comp */ (comp) => html`
70
+ <div
71
+ class="layer-row${comp.tagName === selectedTag ? " selected" : ""}"
72
+ @click=${() => ctx.selectStylebookTag(comp.tagName)}
73
+ >
74
+ <span class="layer-tag component-tag" style="background:var(--accent)">⬡</span>
75
+ <span class="layer-label">${comp.tagName}</span>
76
+ </div>
77
+ `,
78
+ );
79
+ return html`${elementRows}${compRows}`;
80
+ } else {
81
+ const style = rootStyle;
82
+ const vars = Object.entries(style).filter(([k]) => k.startsWith("--"));
83
+ if (vars.length === 0) {
84
+ return html`<div style="padding:16px;text-align:center;color:var(--fg-dim);font-size:12px">
85
+ No variables defined
86
+ </div>`;
87
+ }
88
+ return html`${vars.map(
89
+ ([k, v]) => html`
90
+ <div class="layer-row">
91
+ <span class="layer-tag" style="font-size:10px;font-family:'SF Mono','Fira Code',monospace"
92
+ >var</span
93
+ >
94
+ <span class="layer-label">${k}</span>
95
+ <span
96
+ style="font-size:11px;color:var(--fg-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:80px"
97
+ >${String(v)}</span
98
+ >
99
+ </div>
100
+ `,
101
+ )}`;
102
+ }
103
+ }