@jxsuite/studio 0.6.2 → 0.7.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
+ }
@@ -331,5 +331,118 @@ export function createDevServerPlatform() {
331
331
  const { schema } = await res.json();
332
332
  return schema;
333
333
  },
334
+
335
+ // ─── Git operations ──────────────────────────────────────────────────
336
+
337
+ async gitStatus() {
338
+ const res = await fetch("/__studio/git/status");
339
+ if (!res.ok) throw new Error(await res.text());
340
+ return await res.json();
341
+ },
342
+
343
+ async gitBranches() {
344
+ const res = await fetch("/__studio/git/branches");
345
+ if (!res.ok) throw new Error(await res.text());
346
+ return await res.json();
347
+ },
348
+
349
+ /** @param {number} [limit] */
350
+ async gitLog(limit) {
351
+ const q = limit ? `?limit=${limit}` : "";
352
+ const res = await fetch(`/__studio/git/log${q}`);
353
+ if (!res.ok) throw new Error(await res.text());
354
+ return await res.json();
355
+ },
356
+
357
+ /** @param {string[]} files */
358
+ async gitStage(files) {
359
+ const res = await fetch("/__studio/git/stage", {
360
+ method: "POST",
361
+ headers: { "Content-Type": "application/json" },
362
+ body: JSON.stringify({ files }),
363
+ });
364
+ if (!res.ok) throw new Error((await res.json()).error);
365
+ return await res.json();
366
+ },
367
+
368
+ /** @param {string[]} files */
369
+ async gitUnstage(files) {
370
+ const res = await fetch("/__studio/git/unstage", {
371
+ method: "POST",
372
+ headers: { "Content-Type": "application/json" },
373
+ body: JSON.stringify({ files }),
374
+ });
375
+ if (!res.ok) throw new Error((await res.json()).error);
376
+ return await res.json();
377
+ },
378
+
379
+ /** @param {string} message */
380
+ async gitCommit(message) {
381
+ const res = await fetch("/__studio/git/commit", {
382
+ method: "POST",
383
+ headers: { "Content-Type": "application/json" },
384
+ body: JSON.stringify({ message }),
385
+ });
386
+ if (!res.ok) throw new Error((await res.json()).error);
387
+ return await res.json();
388
+ },
389
+
390
+ async gitPush() {
391
+ const res = await fetch("/__studio/git/push", { method: "POST" });
392
+ if (!res.ok) throw new Error((await res.json()).error);
393
+ return await res.json();
394
+ },
395
+
396
+ async gitPull() {
397
+ const res = await fetch("/__studio/git/pull", { method: "POST" });
398
+ if (!res.ok) throw new Error((await res.json()).error);
399
+ return await res.json();
400
+ },
401
+
402
+ async gitFetch() {
403
+ const res = await fetch("/__studio/git/fetch", { method: "POST" });
404
+ if (!res.ok) throw new Error((await res.json()).error);
405
+ return await res.json();
406
+ },
407
+
408
+ /** @param {string} branch */
409
+ async gitCheckout(branch) {
410
+ const res = await fetch("/__studio/git/checkout", {
411
+ method: "POST",
412
+ headers: { "Content-Type": "application/json" },
413
+ body: JSON.stringify({ branch }),
414
+ });
415
+ if (!res.ok) throw new Error((await res.json()).error);
416
+ return await res.json();
417
+ },
418
+
419
+ /** @param {string} name */
420
+ async gitCreateBranch(name) {
421
+ const res = await fetch("/__studio/git/create-branch", {
422
+ method: "POST",
423
+ headers: { "Content-Type": "application/json" },
424
+ body: JSON.stringify({ name }),
425
+ });
426
+ if (!res.ok) throw new Error((await res.json()).error);
427
+ return await res.json();
428
+ },
429
+
430
+ /** @param {string} path */
431
+ async gitDiff(path) {
432
+ const res = await fetch(`/__studio/git/diff?path=${encodeURIComponent(path)}`);
433
+ if (!res.ok) throw new Error(await res.text());
434
+ return await res.json();
435
+ },
436
+
437
+ /** @param {string[]} files */
438
+ async gitDiscard(files) {
439
+ const res = await fetch("/__studio/git/discard", {
440
+ method: "POST",
441
+ headers: { "Content-Type": "application/json" },
442
+ body: JSON.stringify({ files }),
443
+ });
444
+ if (!res.ok) throw new Error((await res.json()).error);
445
+ return await res.json();
446
+ },
334
447
  };
335
448
  }
package/src/state.js CHANGED
@@ -221,12 +221,19 @@ export function createState(doc) {
221
221
  styleSections: {}, // { layout: true, ... } — section open/closed state
222
222
  inspectorSections: {}, // { identity: true, ... } — properties panel section open/closed state
223
223
  styleShorthands: {}, // { padding: true, ... } — shorthand expand/collapse state
224
+ styleFilter: "", // free-text filter for CSS property names
225
+ styleFilterActive: false, // true = show only props with values set
224
226
  editingFunction: null, // null | { type: 'def', defName } | { type: 'event', path, eventKey }
225
227
  stylebookSelection: null, // tag name string, e.g. "h1"
226
228
  stylebookTab: "elements", // "elements" | "variables"
227
229
  stylebookFilter: "", // search filter text
228
230
  stylebookCustomizedOnly: false, // show only customized elements
229
231
  settingsTab: "stylebook", // "stylebook" | "definitions" | "collections"
232
+ gitStatus: null, // { branch, ahead, behind, files: [] }
233
+ gitBranches: null, // { current, branches: [] }
234
+ gitCommitMessage: "", // commit message input
235
+ gitLoading: false, // loading indicator during async ops
236
+ gitError: null, // error message string
230
237
  },
231
238
  };
232
239
  }