@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.
@@ -11,6 +11,8 @@ import { eventsSidebarTemplate } from "./events-panel.js";
11
11
  import { isCustomElementDoc } from "./signals-panel.js";
12
12
  import { ensureLitState } from "./shared.js";
13
13
  import { isColorPopoverOpen } from "../ui/color-selector.js";
14
+ import { renderStylePanelTemplate } from "./style-panel.js";
15
+ import { renderPropertiesPanelTemplate } from "./properties-panel.js";
14
16
 
15
17
  /** @type {any} */
16
18
  let _ctx = null;
@@ -21,7 +23,7 @@ let _unsub = null;
21
23
  /**
22
24
  * Mount the right panel.
23
25
  *
24
- * @param {any} ctx — { propertiesSidebarTemplate, renderStylePanelTemplate, renderCanvas,
26
+ * @param {any} ctx — { propertiesSidebarTemplate, getCanvasMode, renderCanvas,
25
27
  * updateForcedPseudoPreview }
26
28
  */
27
29
  export function mount(ctx) {
@@ -109,7 +111,7 @@ function rightPanelTemplate() {
109
111
  /** @type {any} */
110
112
  let bodyT = nothing;
111
113
  if (tab === "properties") {
112
- bodyT = _ctx.propertiesSidebarTemplate();
114
+ bodyT = renderPropertiesPanelTemplate({ navigateToComponent: _ctx.navigateToComponent });
113
115
  } else if (tab === "events") {
114
116
  bodyT = eventsSidebarTemplate(S, {
115
117
  isCustomElementDoc: () => isCustomElementDoc(S),
@@ -117,7 +119,7 @@ function rightPanelTemplate() {
117
119
  });
118
120
  } else if (tab === "style") {
119
121
  try {
120
- bodyT = _ctx.renderStylePanelTemplate();
122
+ bodyT = renderStylePanelTemplate({ getCanvasMode: _ctx.getCanvasMode });
121
123
  } catch (/** @type {any} */ e) {
122
124
  console.error("[renderStylePanelTemplate]", e);
123
125
  }
@@ -39,3 +39,77 @@ export function ensureLitState(container) {
39
39
  delete container["_$litPart$"];
40
40
  }
41
41
  }
42
+
43
+ export const unsafeTags = new Set(["script", "style", "link", "iframe", "object", "embed"]);
44
+
45
+ /**
46
+ * Generate a sensible default Jx node for a given tag name.
47
+ *
48
+ * @param {any} tag
49
+ */
50
+ export function defaultDef(tag) {
51
+ /** @type {any} */
52
+ const def = { tagName: tag };
53
+ if (/^h[1-6]$/.test(tag)) def.textContent = "Heading";
54
+ else if (tag === "p") def.textContent = "Paragraph text";
55
+ else if (
56
+ tag === "span" ||
57
+ tag === "strong" ||
58
+ tag === "em" ||
59
+ tag === "small" ||
60
+ tag === "mark" ||
61
+ tag === "code" ||
62
+ tag === "abbr" ||
63
+ tag === "q" ||
64
+ tag === "sub" ||
65
+ tag === "sup" ||
66
+ tag === "time"
67
+ )
68
+ def.textContent = "Text";
69
+ else if (tag === "a") {
70
+ def.textContent = "Link";
71
+ def.attributes = { href: "#" };
72
+ } else if (tag === "button") def.textContent = "Button";
73
+ else if (tag === "label") def.textContent = "Label";
74
+ else if (tag === "legend") def.textContent = "Legend";
75
+ else if (tag === "caption") def.textContent = "Caption";
76
+ else if (tag === "summary") def.textContent = "Summary";
77
+ else if (
78
+ tag === "li" ||
79
+ tag === "dt" ||
80
+ tag === "dd" ||
81
+ tag === "th" ||
82
+ tag === "td" ||
83
+ tag === "option"
84
+ )
85
+ def.textContent = "Item";
86
+ else if (tag === "blockquote") def.textContent = "Quote";
87
+ else if (tag === "pre") def.textContent = "Preformatted text";
88
+ else if (tag === "input") def.attributes = { type: "text", placeholder: "Enter text..." };
89
+ else if (tag === "img") def.attributes = { src: "", alt: "Image" };
90
+ else if (tag === "iframe") def.attributes = { src: "" };
91
+ else if (tag === "select") def.children = [{ tagName: "option", textContent: "Option 1" }];
92
+ else if (tag === "ul" || tag === "ol") def.children = [{ tagName: "li", textContent: "Item" }];
93
+ else if (tag === "dl")
94
+ def.children = [
95
+ { tagName: "dt", textContent: "Term" },
96
+ { tagName: "dd", textContent: "Definition" },
97
+ ];
98
+ else if (tag === "table")
99
+ def.children = [
100
+ {
101
+ tagName: "thead",
102
+ children: [{ tagName: "tr", children: [{ tagName: "th", textContent: "Header" }] }],
103
+ },
104
+ {
105
+ tagName: "tbody",
106
+ children: [{ tagName: "tr", children: [{ tagName: "td", textContent: "Cell" }] }],
107
+ },
108
+ ];
109
+ else if (tag === "details")
110
+ def.children = [
111
+ { tagName: "summary", textContent: "Summary" },
112
+ { tagName: "p", textContent: "Detail content" },
113
+ ];
114
+ return def;
115
+ }
@@ -0,0 +1,176 @@
1
+ /** Style input widgets — keyword, select, combobox, and font renderers for the style panel. */
2
+
3
+ import { html } from "lit-html";
4
+ import { live } from "lit-html/directives/live.js";
5
+ import { getState, update, updateStyle, debouncedStyleCommit } from "../store.js";
6
+ import { widgetForType as _widgetForType } from "../ui/widgets.js";
7
+ import { kebabToLabel, friendlyNameToVar, varDisplayName } from "../utils/studio-utils.js";
8
+ import {
9
+ TYPO_PREVIEW_PROPS,
10
+ currentFontFamily,
11
+ getFontVars,
12
+ getCssInitialMap,
13
+ camelToKebab,
14
+ } from "./style-utils.js";
15
+
16
+ /**
17
+ * Dual-mode keyword input — shared by select (enum) and combobox (examples) widgets.
18
+ *
19
+ * @param {any} options @param {any} prop @param {any} value @param {any} onChange
20
+ */
21
+ export function renderKeywordInput(options, prop, value, onChange) {
22
+ const cssInitialMap = getCssInitialMap();
23
+ const isTypoPreview = TYPO_PREVIEW_PROPS.has(prop) || prop === "fontWeight";
24
+ const font = isTypoPreview ? currentFontFamily() : "";
25
+ const cssProp = isTypoPreview ? camelToKebab(prop) : "";
26
+
27
+ const comboOptions = options.map((/** @type {any} */ v) => {
28
+ const label = v.includes("-")
29
+ ? kebabToLabel(v)
30
+ : v.replace(/^./, (/** @type {any} */ c) => c.toUpperCase());
31
+ const style = isTypoPreview ? `${cssProp}: ${v};${font ? ` font-family: ${font}` : ""}` : "";
32
+ return { value: v, label, style };
33
+ });
34
+
35
+ return html`<jx-value-selector
36
+ size="s"
37
+ .value=${value || ""}
38
+ placeholder=${cssInitialMap.get(prop) || ""}
39
+ .options=${comboOptions}
40
+ @change=${(/** @type {any} */ e) => onChange(e.target.value)}
41
+ @input=${debouncedStyleCommit(`kw:${prop}`, 400, (/** @type {any} */ e) =>
42
+ onChange(e.target.value),
43
+ )}
44
+ ></jx-value-selector>`;
45
+ }
46
+
47
+ /** @param {any} entry @param {any} prop @param {any} value @param {any} onChange */
48
+ export function renderSelectInput(entry, prop, value, onChange) {
49
+ return renderKeywordInput(entry.enum || [], prop, value, onChange);
50
+ }
51
+
52
+ /** @param {any} preset @param {any} onChange */
53
+ function handleFontPresetSelection(preset, onChange) {
54
+ const S = getState();
55
+ const varName = friendlyNameToVar(preset.title, "--font-");
56
+ if (!S.document?.style?.[varName]) {
57
+ update(updateStyle(S, [], varName, preset.value));
58
+ }
59
+ onChange(`var(${varName})`);
60
+ }
61
+
62
+ /** @param {any} val @param {any} presets @param {any} onChange */
63
+ function handleFontSelection(val, presets, onChange) {
64
+ if (!val) return;
65
+ if (val.startsWith("__preset__:")) {
66
+ const title = val.slice("__preset__:".length);
67
+ const preset = presets.find((/** @type {any} */ p) => p.title === title);
68
+ if (preset) handleFontPresetSelection(preset, onChange);
69
+ return;
70
+ }
71
+ if (val.startsWith("--")) {
72
+ onChange(`var(${val})`);
73
+ return;
74
+ }
75
+ const preset = presets.find((/** @type {any} */ p) => p.title === val);
76
+ if (preset) {
77
+ handleFontPresetSelection(preset, onChange);
78
+ return;
79
+ }
80
+ const fontVars = getFontVars();
81
+ const matchedVar = fontVars.find(
82
+ (/** @type {any} */ fv) => varDisplayName(fv.name, "--font-") === val,
83
+ );
84
+ if (matchedVar) {
85
+ onChange(`var(${matchedVar.name})`);
86
+ return;
87
+ }
88
+ onChange(val);
89
+ }
90
+
91
+ /**
92
+ * Build font options array for jx-value-selector.
93
+ *
94
+ * @param {any[]} fontVars @param {any[]} presets
95
+ * @returns {{ value: string; label: string; style: string }[] | { divider: true }[]}
96
+ */
97
+ export function buildFontOptions(fontVars, presets) {
98
+ /** @type {any[]} */
99
+ const opts = fontVars.map((/** @type {any} */ fv) => ({
100
+ value: fv.name,
101
+ label: varDisplayName(fv.name, "--font-"),
102
+ style: `font-family: ${fv.value}`,
103
+ }));
104
+ const unadded = presets.filter(
105
+ (/** @type {any} */ p) =>
106
+ !fontVars.some((/** @type {any} */ fv) => fv.name === friendlyNameToVar(p.title, "--font-")),
107
+ );
108
+ if (unadded.length > 0 && opts.length > 0) opts.push({ divider: true });
109
+ for (const p of unadded) {
110
+ opts.push({
111
+ value: "__preset__:" + p.title,
112
+ label: p.title,
113
+ style: `font-family: ${p.value}`,
114
+ });
115
+ }
116
+ return opts;
117
+ }
118
+
119
+ /** @param {any} entry @param {any} prop @param {any} value @param {any} onChange */
120
+ export function renderComboboxInput(entry, prop, value, onChange) {
121
+ const cssInitialMap = getCssInitialMap();
122
+ const fontVars = prop === "fontFamily" ? getFontVars() : [];
123
+ const presets = entry.presets || [];
124
+ const examples = entry.examples || [];
125
+
126
+ if (prop === "fontFamily") {
127
+ const varMatch = typeof value === "string" && value.match(/^var\((--[^)]+)\)$/);
128
+ const comboValue = varMatch ? varMatch[1] : value || "";
129
+ const fontOptions = buildFontOptions(fontVars, presets);
130
+ return html`<jx-value-selector
131
+ size="s"
132
+ .value=${comboValue}
133
+ placeholder=${cssInitialMap.get("fontFamily") || ""}
134
+ .options=${fontOptions}
135
+ @change=${(/** @type {any} */ e) => handleFontSelection(e.target.value, presets, onChange)}
136
+ @input=${debouncedStyleCommit("combo:fontFamily", 400, (/** @type {any} */ e) =>
137
+ onChange(e.target.value),
138
+ )}
139
+ ></jx-value-selector>`;
140
+ }
141
+
142
+ if (examples.length > 0) {
143
+ return renderKeywordInput(examples, prop, value, onChange);
144
+ }
145
+
146
+ return html`
147
+ <sp-textfield
148
+ size="s"
149
+ placeholder=${cssInitialMap.get(prop) || ""}
150
+ .value=${live(value || "")}
151
+ @input=${debouncedStyleCommit(`combo:${prop}`, 400, (/** @type {any} */ e) =>
152
+ onChange(e.target.value),
153
+ )}
154
+ ></sp-textfield>
155
+ `;
156
+ }
157
+
158
+ /**
159
+ * Style-aware widgetForType — wraps the generic widget renderer with style-specific select/combobox
160
+ * inputs and CSS initial-value placeholders.
161
+ */
162
+ export function widgetForType(
163
+ /** @type {any} */ type,
164
+ /** @type {any} */ entry,
165
+ /** @type {any} */ prop,
166
+ /** @type {any} */ value,
167
+ /** @type {any} */ onCommit,
168
+ /** @type {any} */ opts = {},
169
+ ) {
170
+ const cssInitialMap = getCssInitialMap();
171
+ return _widgetForType(type, entry, prop, value, onCommit, {
172
+ placeholder: opts.placeholder || cssInitialMap.get(prop) || "",
173
+ renderSelect: renderSelectInput,
174
+ renderCombobox: renderComboboxInput,
175
+ });
176
+ }