@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.
- package/dist/studio.js +162163 -151534
- package/dist/studio.js.map +99 -27
- package/package.json +2 -2
- package/src/editor/insertion-helper.js +301 -0
- package/src/editor/slash-menu.js +111 -37
- package/src/markdown/md-convert.js +18 -16
- package/src/panels/activity-bar.js +22 -0
- package/src/panels/block-action-bar.js +436 -0
- package/src/panels/dnd.js +332 -0
- package/src/panels/editors.js +196 -0
- package/src/panels/elements-panel.js +148 -0
- package/src/panels/git-panel.js +280 -0
- package/src/panels/layers-panel.js +270 -0
- package/src/panels/left-panel.js +142 -0
- package/src/panels/properties-panel.js +1340 -0
- package/src/panels/right-panel.js +5 -3
- package/src/panels/shared.js +74 -0
- package/src/panels/style-inputs.js +176 -0
- package/src/panels/style-panel.js +651 -0
- package/src/panels/style-utils.js +193 -0
- package/src/panels/stylebook-layers-panel.js +103 -0
- package/src/panels/stylebook-panel.js +1052 -0
- package/src/platforms/devserver.js +113 -0
- package/src/state.js +7 -0
- package/src/studio.js +92 -4820
- package/src/ui/spectrum.js +4 -0
|
@@ -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
|
+
}
|