@jxsuite/studio 0.0.1 → 0.5.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 +47638 -33445
- package/dist/studio.js.map +449 -344
- package/package.json +44 -33
- package/src/browse/browse.js +414 -0
- package/src/editor/context-menu.js +48 -1
- package/src/editor/convert-to-component.js +208 -0
- package/src/editor/inline-edit.js +33 -6
- package/src/editor/shortcuts.js +6 -1
- package/src/files/components.js +4 -2
- package/src/files/file-ops.js +102 -54
- package/src/files/files.js +22 -8
- package/src/markdown/md-convert.js +309 -11
- package/src/panels/activity-bar.js +3 -0
- package/src/panels/head-panel.js +576 -0
- package/src/panels/overlays.js +125 -0
- package/src/panels/right-panel.js +104 -0
- package/src/panels/shared.js +41 -0
- package/src/panels/signals-panel.js +95 -94
- package/src/panels/toolbar.js +217 -0
- package/src/platforms/devserver.js +58 -16
- package/src/settings/collections-editor.js +428 -0
- package/src/settings/defs-editor.js +418 -0
- package/src/settings/schema-field-ui.js +329 -0
- package/src/state.js +99 -2
- package/src/store.js +77 -41
- package/src/studio.js +1523 -1375
- package/src/ui/button-group.js +91 -0
- package/src/ui/color-selector.js +299 -0
- package/src/ui/field-row.js +47 -0
- package/src/ui/media-picker.js +172 -0
- package/src/ui/panel-resize.js +96 -0
- package/src/ui/spectrum.js +36 -2
- package/src/ui/unit-selector.js +106 -0
- package/src/ui/{jx-styled-combobox.js → value-selector.js} +7 -7
- package/src/ui/widgets.js +106 -0
- package/src/utils/inherited-style.js +54 -0
- package/src/utils/studio-utils.js +32 -0
- package/src/view.js +45 -0
|
@@ -4,10 +4,15 @@
|
|
|
4
4
|
* MdToJx(mdast) → Jx element tree (for loading into the canvas) jxToMd(jx) → mdast (for saving back
|
|
5
5
|
* to markdown)
|
|
6
6
|
*
|
|
7
|
+
* JxDocToMd(doc) → Jx Markdown string (for saving Jx component documents back to .md)
|
|
8
|
+
*
|
|
7
9
|
* Both are pure tree transformations. The remark ecosystem handles all actual parsing and
|
|
8
10
|
* serialization.
|
|
9
11
|
*/
|
|
10
12
|
|
|
13
|
+
import { unified } from "unified";
|
|
14
|
+
import remarkStringify from "remark-stringify";
|
|
15
|
+
import remarkDirective from "remark-directive";
|
|
11
16
|
import { MD_ALL } from "./md-allowlist.js";
|
|
12
17
|
|
|
13
18
|
// ─── mdast → Jx ──────────────────────────────────────────────────────────
|
|
@@ -48,8 +53,6 @@ const MDAST_TAG_MAP = {
|
|
|
48
53
|
export function mdToJx(mdast) {
|
|
49
54
|
if (mdast.type === "root") {
|
|
50
55
|
return {
|
|
51
|
-
tagName: "div",
|
|
52
|
-
$id: "content",
|
|
53
56
|
children: (mdast.children ?? [])
|
|
54
57
|
.filter((/** @type {any} */ n) => n.type !== "yaml" && n.type !== "toml")
|
|
55
58
|
.map(convertMdastNode)
|
|
@@ -221,6 +224,23 @@ function convertDirective(node) {
|
|
|
221
224
|
*
|
|
222
225
|
* @type {Record<string, string>}
|
|
223
226
|
*/
|
|
227
|
+
/** Tags whose content model is inline (phrasing content). */
|
|
228
|
+
const INLINE_CONTENT_TAGS = new Set([
|
|
229
|
+
"p",
|
|
230
|
+
"h1",
|
|
231
|
+
"h2",
|
|
232
|
+
"h3",
|
|
233
|
+
"h4",
|
|
234
|
+
"h5",
|
|
235
|
+
"h6",
|
|
236
|
+
"em",
|
|
237
|
+
"strong",
|
|
238
|
+
"del",
|
|
239
|
+
"a",
|
|
240
|
+
"td",
|
|
241
|
+
"th",
|
|
242
|
+
]);
|
|
243
|
+
|
|
224
244
|
const TAG_MDAST_MAP = {
|
|
225
245
|
h1: "heading",
|
|
226
246
|
h2: "heading",
|
|
@@ -263,6 +283,28 @@ export function jxToMd(jx) {
|
|
|
263
283
|
return { type: "root", children };
|
|
264
284
|
}
|
|
265
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Check if a Jx element has extra properties beyond the standard mdast-compatible ones. Elements
|
|
288
|
+
* with style, event handlers, state bindings, etc. need directive syntax.
|
|
289
|
+
*
|
|
290
|
+
* @param {any} el
|
|
291
|
+
* @returns {boolean}
|
|
292
|
+
*/
|
|
293
|
+
function hasJxProps(el) {
|
|
294
|
+
for (const key of Object.keys(el)) {
|
|
295
|
+
if (
|
|
296
|
+
key === "tagName" ||
|
|
297
|
+
key === "children" ||
|
|
298
|
+
key === "textContent" ||
|
|
299
|
+
key === "innerHTML" ||
|
|
300
|
+
key === "attributes"
|
|
301
|
+
)
|
|
302
|
+
continue;
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
266
308
|
/**
|
|
267
309
|
* Convert a single Jx element to an mdast node.
|
|
268
310
|
*
|
|
@@ -271,16 +313,20 @@ export function jxToMd(jx) {
|
|
|
271
313
|
* @returns {any} Mdast node
|
|
272
314
|
*/
|
|
273
315
|
function convertJxNode(el, isBlock) {
|
|
316
|
+
// Bare string/number text nodes → mdast text nodes
|
|
317
|
+
if (typeof el === "string" || typeof el === "number") {
|
|
318
|
+
return { type: "text", value: String(el) };
|
|
319
|
+
}
|
|
274
320
|
if (!el || typeof el !== "object") return null;
|
|
275
321
|
|
|
276
322
|
const tag = el.tagName ?? "div";
|
|
277
323
|
|
|
278
|
-
// If not in the markdown allowlist, convert to directive
|
|
279
|
-
if (!MD_ALL.has(tag)) {
|
|
324
|
+
// If not in the markdown allowlist or has Jx-specific props, convert to directive
|
|
325
|
+
if (!MD_ALL.has(tag) || hasJxProps(el)) {
|
|
280
326
|
return convertToDirective(el, isBlock);
|
|
281
327
|
}
|
|
282
328
|
|
|
283
|
-
const mdastType = TAG_MDAST_MAP[tag];
|
|
329
|
+
const mdastType = /** @type {Record<string, string>} */ (TAG_MDAST_MAP)[tag];
|
|
284
330
|
if (!mdastType) return null;
|
|
285
331
|
|
|
286
332
|
switch (mdastType) {
|
|
@@ -443,7 +489,42 @@ function blockChildren(el) {
|
|
|
443
489
|
}
|
|
444
490
|
|
|
445
491
|
/**
|
|
446
|
-
*
|
|
492
|
+
* Collect all directive attributes from a Jx element. Merges Jx-specific properties (style, event
|
|
493
|
+
* handlers, etc.) and HTML attributes into a flat dot-path attribute map suitable for
|
|
494
|
+
* remark-directive.
|
|
495
|
+
*
|
|
496
|
+
* @param {any} el
|
|
497
|
+
* @returns {Record<string, string>}
|
|
498
|
+
*/
|
|
499
|
+
function collectDirectiveAttrs(el) {
|
|
500
|
+
/** @type {Record<string, any>} */
|
|
501
|
+
const propsObj = {};
|
|
502
|
+
|
|
503
|
+
for (const [key, value] of Object.entries(el)) {
|
|
504
|
+
if (
|
|
505
|
+
key === "tagName" ||
|
|
506
|
+
key === "children" ||
|
|
507
|
+
key === "textContent" ||
|
|
508
|
+
key === "innerHTML" ||
|
|
509
|
+
key === "attributes"
|
|
510
|
+
)
|
|
511
|
+
continue;
|
|
512
|
+
propsObj[key] = value;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Merge HTML attributes
|
|
516
|
+
if (el.attributes) {
|
|
517
|
+
for (const [key, value] of Object.entries(el.attributes)) {
|
|
518
|
+
propsObj[key] = value;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return collapsePropsToAttrMap(propsObj);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Convert a Jx element to a directive node, preserving all Jx-specific properties as collapsed
|
|
527
|
+
* dot-path directive attributes.
|
|
447
528
|
*
|
|
448
529
|
* @param {any} el
|
|
449
530
|
* @param {boolean} isBlock
|
|
@@ -451,7 +532,7 @@ function blockChildren(el) {
|
|
|
451
532
|
*/
|
|
452
533
|
function convertToDirective(el, isBlock) {
|
|
453
534
|
const tag = el.tagName ?? "div";
|
|
454
|
-
const attrs = el
|
|
535
|
+
const attrs = collectDirectiveAttrs(el);
|
|
455
536
|
|
|
456
537
|
if (!isBlock) {
|
|
457
538
|
// Inline → textDirective
|
|
@@ -479,13 +560,230 @@ function convertToDirective(el, isBlock) {
|
|
|
479
560
|
}
|
|
480
561
|
|
|
481
562
|
// Block with children → containerDirective
|
|
563
|
+
/** @type {any[]} */
|
|
564
|
+
let directiveChildren;
|
|
565
|
+
if (el.textContent != null) {
|
|
566
|
+
directiveChildren = [
|
|
567
|
+
{ type: "paragraph", children: [{ type: "text", value: String(el.textContent) }] },
|
|
568
|
+
];
|
|
569
|
+
} else if (INLINE_CONTENT_TAGS.has(tag)) {
|
|
570
|
+
// Tags with inline content model: wrap all children in a single paragraph
|
|
571
|
+
// so remark serializes them as one continuous inline flow
|
|
572
|
+
const inlineNodes = (el.children ?? [])
|
|
573
|
+
.map((/** @type {any} */ c) => convertJxNode(c, false))
|
|
574
|
+
.filter(Boolean);
|
|
575
|
+
directiveChildren =
|
|
576
|
+
inlineNodes.length > 0 ? [{ type: "paragraph", children: inlineNodes }] : [];
|
|
577
|
+
} else {
|
|
578
|
+
directiveChildren = (el.children ?? [])
|
|
579
|
+
.map((/** @type {any} */ c) => convertJxNode(c, true))
|
|
580
|
+
.filter(Boolean);
|
|
581
|
+
}
|
|
582
|
+
|
|
482
583
|
return {
|
|
483
584
|
type: "containerDirective",
|
|
484
585
|
name: tag,
|
|
485
586
|
attributes: attrs,
|
|
486
|
-
children:
|
|
487
|
-
el.textContent != null
|
|
488
|
-
? [{ type: "paragraph", children: [{ type: "text", value: String(el.textContent) }] }]
|
|
489
|
-
: (el.children ?? []).map((/** @type {any} */ c) => convertJxNode(c, true)).filter(Boolean),
|
|
587
|
+
children: directiveChildren,
|
|
490
588
|
};
|
|
491
589
|
}
|
|
590
|
+
|
|
591
|
+
// ─── Jx Document → Jx Markdown ─────────────────────────────────────────────
|
|
592
|
+
|
|
593
|
+
/** CSS pseudo-class names that need `:` stripped for markdown attributes. */
|
|
594
|
+
const CSS_PSEUDO_NAMES = new Set([
|
|
595
|
+
"hover",
|
|
596
|
+
"focus",
|
|
597
|
+
"active",
|
|
598
|
+
"visited",
|
|
599
|
+
"disabled",
|
|
600
|
+
"checked",
|
|
601
|
+
"valid",
|
|
602
|
+
"invalid",
|
|
603
|
+
"required",
|
|
604
|
+
"empty",
|
|
605
|
+
"first-child",
|
|
606
|
+
"last-child",
|
|
607
|
+
"focus-within",
|
|
608
|
+
"focus-visible",
|
|
609
|
+
"placeholder",
|
|
610
|
+
"selection",
|
|
611
|
+
"before",
|
|
612
|
+
"after",
|
|
613
|
+
]);
|
|
614
|
+
|
|
615
|
+
/** Jx `$`-prefixed keys that become unprefixed in directive attributes. */
|
|
616
|
+
const JX_DOLLAR_KEYS = new Set([
|
|
617
|
+
"$prototype",
|
|
618
|
+
"$ref",
|
|
619
|
+
"$component",
|
|
620
|
+
"$props",
|
|
621
|
+
"$switch",
|
|
622
|
+
"$elements",
|
|
623
|
+
]);
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Convert a Jx JSON document back to Jx Markdown source string.
|
|
627
|
+
*
|
|
628
|
+
* Inverse of `transpileJxMarkdown()` from @jxsuite/parser/transpile. Emits YAML frontmatter from
|
|
629
|
+
* top-level props and uses remark-stringify with remark-directive for the body — standard markdown
|
|
630
|
+
* elements emit as native syntax, Jx-decorated elements emit as directives.
|
|
631
|
+
*
|
|
632
|
+
* @param {any} doc - Jx JSON document
|
|
633
|
+
* @returns {string} Jx Markdown source
|
|
634
|
+
*/
|
|
635
|
+
export function jxDocToMd(doc) {
|
|
636
|
+
const { stringify: stringifyYaml } = yamlImport();
|
|
637
|
+
|
|
638
|
+
/** @type {string[]} */
|
|
639
|
+
const lines = [];
|
|
640
|
+
|
|
641
|
+
// Emit YAML frontmatter
|
|
642
|
+
/** @type {Record<string, any>} */
|
|
643
|
+
const frontmatter = {};
|
|
644
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
645
|
+
if (key === "children") continue;
|
|
646
|
+
frontmatter[key] = value;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (Object.keys(frontmatter).length > 0) {
|
|
650
|
+
lines.push("---");
|
|
651
|
+
lines.push(stringifyYaml(frontmatter).trim());
|
|
652
|
+
lines.push("---");
|
|
653
|
+
lines.push("");
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Convert children to mdast and stringify with remark
|
|
657
|
+
if (Array.isArray(doc.children) && doc.children.length > 0) {
|
|
658
|
+
const mdastChildren = doc.children
|
|
659
|
+
.map((/** @type {any} */ child) => convertJxNode(child, true))
|
|
660
|
+
.filter(Boolean);
|
|
661
|
+
|
|
662
|
+
const mdast = /** @type {any} */ ({ type: "root", children: mdastChildren });
|
|
663
|
+
const md = unified()
|
|
664
|
+
.use(remarkDirective)
|
|
665
|
+
.use(remarkStringify, { bullet: "-", emphasis: "*", strong: "*" })
|
|
666
|
+
.stringify(mdast);
|
|
667
|
+
|
|
668
|
+
lines.push(md);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return (
|
|
672
|
+
lines
|
|
673
|
+
.join("\n")
|
|
674
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
675
|
+
.trim() + "\n"
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Lazy import of yaml stringify — avoids importing at module load.
|
|
681
|
+
*
|
|
682
|
+
* @returns {{ stringify: (v: any) => string }}
|
|
683
|
+
*/
|
|
684
|
+
let _yaml = /** @type {any} */ (null);
|
|
685
|
+
function yamlImport() {
|
|
686
|
+
if (!_yaml) {
|
|
687
|
+
// Dynamic require avoided; use the yaml package already available in studio
|
|
688
|
+
_yaml = { stringify: yamlStringifySimple };
|
|
689
|
+
}
|
|
690
|
+
return _yaml;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Simple YAML stringifier for frontmatter. Handles the subset of YAML needed for Jx frontmatter
|
|
695
|
+
* (scalars, arrays, nested objects).
|
|
696
|
+
*
|
|
697
|
+
* @param {any} value
|
|
698
|
+
* @param {number} indent
|
|
699
|
+
* @returns {string}
|
|
700
|
+
*/
|
|
701
|
+
function yamlStringifySimple(value, indent = 0) {
|
|
702
|
+
if (value === null || value === undefined) return "null";
|
|
703
|
+
if (typeof value === "boolean") return String(value);
|
|
704
|
+
if (typeof value === "number") return String(value);
|
|
705
|
+
if (typeof value === "string") {
|
|
706
|
+
// Quote if it contains special characters
|
|
707
|
+
if (/[:#[\]{}&*!|>'"%@`\n]/.test(value) || value === "" || value.trim() !== value) {
|
|
708
|
+
return JSON.stringify(value);
|
|
709
|
+
}
|
|
710
|
+
return value;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const prefix = " ".repeat(indent);
|
|
714
|
+
|
|
715
|
+
if (Array.isArray(value)) {
|
|
716
|
+
if (value.length === 0) return "[]";
|
|
717
|
+
return value
|
|
718
|
+
.map((item) => {
|
|
719
|
+
const itemStr = yamlStringifySimple(item, indent + 1);
|
|
720
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
721
|
+
// Object items: first key on same line as -, rest indented
|
|
722
|
+
const objLines = itemStr.split("\n");
|
|
723
|
+
return `${prefix}- ${objLines[0]}\n${objLines
|
|
724
|
+
.slice(1)
|
|
725
|
+
.map((l) => `${prefix} ${l}`)
|
|
726
|
+
.join("\n")}`;
|
|
727
|
+
}
|
|
728
|
+
return `${prefix}- ${itemStr}`;
|
|
729
|
+
})
|
|
730
|
+
.join("\n");
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (typeof value === "object") {
|
|
734
|
+
const entries = Object.entries(value);
|
|
735
|
+
if (entries.length === 0) return "{}";
|
|
736
|
+
return entries
|
|
737
|
+
.map(([k, v]) => {
|
|
738
|
+
const valStr = yamlStringifySimple(v, indent + 1);
|
|
739
|
+
if (typeof v === "object" && v !== null) {
|
|
740
|
+
return `${prefix}${k}:\n${valStr}`;
|
|
741
|
+
}
|
|
742
|
+
return `${prefix}${k}: ${valStr}`;
|
|
743
|
+
})
|
|
744
|
+
.join("\n");
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return String(value);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Collapse a Jx props object to a flat directive attribute map. Applies key mapping: strips `$`
|
|
752
|
+
* from Jx keywords, `:` from pseudo-classes, `@` from media queries.
|
|
753
|
+
*
|
|
754
|
+
* @param {Record<string, any>} propsObj
|
|
755
|
+
* @returns {Record<string, string>}
|
|
756
|
+
*/
|
|
757
|
+
function collapsePropsToAttrMap(propsObj) {
|
|
758
|
+
/** @type {Record<string, string>} */
|
|
759
|
+
const result = {};
|
|
760
|
+
|
|
761
|
+
function walk(/** @type {Record<string, any>} */ obj, /** @type {string} */ prefix) {
|
|
762
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
763
|
+
let mdAttrKey = key;
|
|
764
|
+
// Strip $ prefix for Jx keywords
|
|
765
|
+
if (JX_DOLLAR_KEYS.has(key)) {
|
|
766
|
+
mdAttrKey = key.slice(1);
|
|
767
|
+
}
|
|
768
|
+
// Strip : prefix for CSS pseudo-classes (inside style.* paths)
|
|
769
|
+
if (key.startsWith(":") && CSS_PSEUDO_NAMES.has(key.slice(1))) {
|
|
770
|
+
mdAttrKey = key.slice(1);
|
|
771
|
+
}
|
|
772
|
+
// Strip @ prefix for media queries (inside style.* paths)
|
|
773
|
+
if (key.startsWith("@--")) {
|
|
774
|
+
mdAttrKey = key.slice(1);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const fullKey = prefix ? `${prefix}.${mdAttrKey}` : mdAttrKey;
|
|
778
|
+
|
|
779
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
780
|
+
walk(value, fullKey);
|
|
781
|
+
} else {
|
|
782
|
+
result[fullKey] = String(value);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
walk(propsObj, "");
|
|
788
|
+
return result;
|
|
789
|
+
}
|
|
@@ -26,6 +26,8 @@ export function tabIcon(tag, size) {
|
|
|
26
26
|
html`<sp-icon-event slot="icon" size=${s}></sp-icon-event>`,
|
|
27
27
|
"sp-icon-brush": (/** @type {any} */ s) =>
|
|
28
28
|
html`<sp-icon-brush slot="icon" size=${s}></sp-icon-brush>`,
|
|
29
|
+
"sp-icon-file-single-web-page": (/** @type {any} */ s) =>
|
|
30
|
+
html`<sp-icon-file-single-web-page slot="icon" size=${s}></sp-icon-file-single-web-page>`,
|
|
29
31
|
"sp-icon-artboard": (/** @type {any} */ s) =>
|
|
30
32
|
html`<sp-icon-artboard slot="icon" size=${s}></sp-icon-artboard>`,
|
|
31
33
|
"sp-icon-box": (/** @type {any} */ s) =>
|
|
@@ -44,6 +46,7 @@ export function renderActivityBar(S) {
|
|
|
44
46
|
{ value: "blocks", icon: "sp-icon-view-grid", label: "Elements" },
|
|
45
47
|
{ value: "state", icon: "sp-icon-brackets", label: "State" },
|
|
46
48
|
{ value: "data", icon: "sp-icon-data", label: "Data" },
|
|
49
|
+
{ value: "head", icon: "sp-icon-file-single-web-page", label: "Head" },
|
|
47
50
|
];
|
|
48
51
|
const tpl = html`
|
|
49
52
|
<sp-tabs
|