@jxsuite/studio 3.0.0 → 4.0.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/README.md +5 -5
- package/dist/chunks/Overlay.dev-723bv0df.js +23 -0
- package/dist/chunks/{cf-settings-zj2cj1ar.js → cf-settings-45fgyx8y.js} +6 -5
- package/dist/chunks/{components-z6cnkyy1.js → components-zmx10t62.js} +6 -5
- package/dist/chunks/content-source-t88vb2dr.js +49 -0
- package/dist/chunks/{convert-to-repeater-6vvx96n2.js → convert-to-repeater-dhw1nrz9.js} +27 -19
- package/dist/chunks/entry-commands-6z6bhxg0.js +46 -0
- package/dist/chunks/{extension-sections-kwzvpemr.js → extension-sections-3ydgjbdy.js} +32 -24
- package/dist/chunks/file-ops-rskmhxyq.js +48 -0
- package/dist/chunks/files-0cratsr0.js +92 -0
- package/dist/chunks/{format-host-8fr3545e.js → format-host-tgcmfs4p.js} +3 -3
- package/dist/chunks/fs-events-hrrg328g.js +21 -0
- package/dist/chunks/{git-panel-g01d93vc.js → git-panel-0yywyk76.js} +21 -16
- package/dist/chunks/{jx-validate-jj9rnt4n.js → jx-validate-8q2g05x1.js} +6 -4
- package/dist/chunks/layers-panel-z3tqe5cd.js +67 -0
- package/dist/chunks/layers-q75tj2mf.js +45 -0
- package/dist/chunks/lit-html-j055mkh8.js +22 -0
- package/dist/chunks/monaco-buffer-dqw0xxpx.js +25 -0
- package/dist/chunks/{monaco-setup-a5wkmbgh.js → monaco-setup-rws7k4br.js} +4 -4
- package/dist/chunks/notify-yjm79n4s.js +30 -0
- package/dist/chunks/parse-24d0gay3.js +19 -0
- package/dist/chunks/platform-ba1j1gbh.js +16 -0
- package/dist/chunks/publish-panel-54jvhskq.js +28 -0
- package/dist/chunks/quick-search-cgfmjwq5.js +65 -0
- package/dist/chunks/references-3qx8fkyp.js +29 -0
- package/dist/chunks/right-panel-cqp8bc4v.js +46 -0
- package/dist/chunks/sp-overlay.dev-yztmtrs1.js +18 -0
- package/dist/chunks/sp-popover.dev-n2etmqdp.js +9 -0
- package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +15 -0
- package/dist/chunks/sp-tooltip.dev-xcvpnqr0.js +15 -0
- package/dist/chunks/{sp-tray.dev-64v4pxw8.js → sp-tray.dev-tkbscm06.js} +5 -5
- package/dist/chunks/{studio-8ezdegcx.js → studio-1hbqnt19.js} +36 -32
- package/dist/chunks/{studio-r5xbtctf.js → studio-2zkz8evn.js} +446 -2451
- package/dist/chunks/{studio-pkp61mh8.js → studio-4pqqxbrt.js} +23 -15
- package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
- package/dist/chunks/{studio-3dkcd4c5.js → studio-5q69gfag.js} +60 -56
- package/dist/chunks/{studio-rh1cnamv.js → studio-5rywrkxz.js} +563 -133
- package/dist/chunks/{studio-6kv3apa9.js → studio-68n67094.js} +25 -9
- package/dist/chunks/{studio-0rqqhrdy.js → studio-7nte2hj4.js} +2 -2
- package/dist/chunks/{studio-xntfdy1x.js → studio-8swrjbny.js} +31 -18
- package/dist/chunks/studio-92ab2jqe.js +282 -0
- package/dist/chunks/{studio-sas7mt3k.js → studio-9ab8gqsm.js} +2 -2
- package/dist/chunks/{studio-ry1vz5sv.js → studio-9c4k4vhv.js} +15 -13
- package/dist/chunks/studio-9n4mrmnb.js +184 -0
- package/dist/chunks/{studio-tr20m8g8.js → studio-9r6j5ev1.js} +37 -2
- package/dist/chunks/{studio-2x9zbxmx.js → studio-ahjfdy6e.js} +9 -7
- package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
- package/dist/chunks/{studio-dc8s8heb.js → studio-cb12yz9w.js} +8528 -8780
- package/dist/chunks/studio-cft9r4r7.js +119 -0
- package/dist/chunks/{studio-hcxzyk3x.js → studio-eenfkt4e.js} +6 -6
- package/dist/chunks/studio-eesa1myv.js +498 -0
- package/dist/chunks/{studio-ay8wcr2b.js → studio-eqq1w638.js} +7 -9
- package/dist/chunks/{studio-7p35254k.js → studio-ex8zyp98.js} +30 -26
- package/dist/chunks/studio-gv8vvshc.js +118 -0
- package/dist/chunks/studio-hbtp0eh1.js +1315 -0
- package/dist/chunks/{studio-a0yp7syn.js → studio-htsf0w6m.js} +29 -114
- package/dist/chunks/{studio-j595e748.js → studio-kjjsjak4.js} +139 -63
- package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
- package/dist/chunks/{studio-kfpek536.js → studio-kyh9avvv.js} +2 -2
- package/dist/chunks/{studio-g5gdqnyd.js → studio-kyk2v92a.js} +14 -14
- package/dist/chunks/{studio-twsafr1e.js → studio-m3hwn7y1.js} +3 -3
- package/dist/chunks/{studio-tzx3xmvx.js → studio-mmv72a27.js} +8 -8
- package/dist/chunks/{studio-vhk5fmfv.js → studio-n8b9wqwy.js} +5 -5
- package/dist/chunks/{studio-cjm7fdjf.js → studio-p4ngc046.js} +8 -104
- package/dist/chunks/{studio-p5dja96s.js → studio-p8j86wag.js} +2 -2
- package/dist/chunks/{studio-6z5eyjwn.js → studio-pdfjntbk.js} +66 -129
- package/dist/chunks/studio-q43a5y9h.js +394 -0
- package/dist/chunks/studio-q7v10cg2.js +106 -0
- package/dist/chunks/{studio-2w11grav.js → studio-qq6jym04.js} +3 -3
- package/dist/chunks/studio-r1r7afxx.js +1066 -0
- package/dist/chunks/{studio-3fk1ac9z.js → studio-rdx8jcxy.js} +4 -4
- package/dist/chunks/{studio-4mqb9a0j.js → studio-rn9ta2mz.js} +4 -4
- package/dist/chunks/studio-sf7zjgv1.js +91 -0
- package/dist/chunks/studio-t7qb9db8.js +665 -0
- package/dist/chunks/studio-tmc5emrw.js +8 -0
- package/dist/chunks/{studio-wdt9y92a.js → studio-txwkm6p9.js} +2 -2
- package/dist/chunks/{studio-k4jvjcfx.js → studio-v38hmvx1.js} +7 -7
- package/dist/chunks/{studio-7ejemhej.js → studio-vadzgkrz.js} +6 -6
- package/dist/chunks/studio-wcy8trfx.js +263 -0
- package/dist/chunks/{studio-5efrtn1m.js → studio-wx5609xd.js} +21 -17
- package/dist/chunks/{studio-qpfq7xhm.js → studio-xrapxwwc.js} +22 -285
- package/dist/chunks/{studio-hj2se583.js → studio-yxrkyv8a.js} +41 -31
- package/dist/chunks/{studio-xm3dbxvn.js → studio-zczps70q.js} +2 -2
- package/dist/chunks/workspace-xpres40v.js +89 -0
- package/dist/iframe-entry.js +108 -24
- package/dist/studio.js +2362 -1695
- package/package.json +16 -15
- package/src/browse/library-pane.ts +57 -3
- package/src/canvas/canvas-render.ts +23 -1
- package/src/canvas/iframe-patch.ts +47 -21
- package/src/canvas/iframe-render.ts +22 -6
- package/src/commands/app-commands.ts +4 -0
- package/src/commands/command-args.ts +29 -0
- package/src/commands/context.ts +15 -1
- package/src/commands/defaults.ts +29 -1
- package/src/commands/levels.ts +16 -0
- package/src/commands/live-context.ts +3 -0
- package/src/content/collection-match.ts +267 -0
- package/src/content/entry-commands.ts +38 -29
- package/src/content/entry-fields.ts +6 -0
- package/src/editor/shortcuts.ts +5 -0
- package/src/files/file-ops.ts +10 -42
- package/src/files/files.ts +485 -59
- package/src/files/fs-events.ts +9 -0
- package/src/files/gitignore.ts +432 -0
- package/src/files/serialize-document.ts +60 -0
- package/src/format/convert-file.ts +511 -0
- package/src/format/format-choices.ts +199 -0
- package/src/grid/grid-view.ts +16 -2
- package/src/media/media-open.ts +58 -0
- package/src/media/media-pane.ts +310 -0
- package/src/new-project/import-tab.ts +332 -160
- package/src/new-project/new-project-modal.ts +27 -34
- package/src/packages/ensure-deps.ts +14 -7
- package/src/packages/jxsuite-update.ts +11 -11
- package/src/page-params.ts +10 -46
- package/src/panels/activity-bar.ts +45 -16
- package/src/panels/ai-chat/attached-context.ts +6 -1
- package/src/panels/ai-chat/chat-view.ts +255 -13
- package/src/panels/ai-chat/composer.ts +21 -97
- package/src/panels/ai-panel.ts +116 -2
- package/src/panels/navigator-panels.ts +7 -6
- package/src/panels/settings-menu.ts +799 -0
- package/src/panels/toolbar.ts +135 -15
- package/src/platforms/cloud.ts +20 -0
- package/src/platforms/devserver.ts +67 -33
- package/src/preview/preview-overlay.ts +226 -0
- package/src/services/ai-ask.ts +247 -0
- package/src/services/ai-import-tools.ts +494 -0
- package/src/services/ai-project-tools.ts +20 -26
- package/src/services/ai-system-prompt.ts +88 -2
- package/src/services/ai-turn-signal.ts +70 -0
- package/src/services/connection.ts +74 -0
- package/src/services/context-manager.ts +89 -0
- package/src/services/document-assistant.ts +45 -4
- package/src/services/import-client.ts +33 -6
- package/src/services/import-run.ts +158 -0
- package/src/services/import-seed.ts +120 -0
- package/src/services/jx-validate.ts +58 -6
- package/src/services/project-adoption.ts +71 -0
- package/src/services/references.ts +0 -0
- package/src/services/settings/definitions.ts +17 -0
- package/src/services/tool-executor.ts +34 -2
- package/src/settings/contexts-section.ts +45 -3
- package/src/settings/dependencies-editor.ts +43 -23
- package/src/settings/general-settings.ts +30 -12
- package/src/settings/preferences-dialog.ts +18 -41
- package/src/settings/preferences-sections.ts +57 -0
- package/src/settings/settings-document.ts +65 -9
- package/src/studio.ts +33 -6
- package/src/tabs/transact.ts +42 -19
- package/src/types.ts +103 -5
- package/src/ui/ai-model-picker.ts +162 -0
- package/src/ui/choice-field.ts +72 -0
- package/src/ui/layers.ts +83 -5
- package/src/utils/studio-utils.ts +22 -52
- package/styles/forced-colors.css +7 -0
- package/styles/overlays.css +256 -0
- package/styles/panels.css +13 -1
- package/styles/shell.css +111 -0
- package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
- package/dist/chunks/content-source-7fy9qrm0.js +0 -40
- package/dist/chunks/files-xcybanxe.js +0 -70
- package/dist/chunks/layers-panel-27essgkc.js +0 -60
- package/dist/chunks/publish-panel-09sy2bab.js +0 -22
- package/dist/chunks/quick-search-sefb61v8.js +0 -55
- package/dist/chunks/right-panel-9r32jd5j.js +0 -39
- package/dist/chunks/sp-overlay.dev-s18hymn9.js +0 -18
- package/dist/chunks/sp-popover.dev-dj9x7e7a.js +0 -9
- package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +0 -15
- package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +0 -15
- package/dist/chunks/studio-c31yb4t6.js +0 -55
- package/src/canvas/site-style-css.ts +0 -117
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -52,19 +52,20 @@
|
|
|
52
52
|
"lint:styles": "bun run scripts/check-styles.ts",
|
|
53
53
|
"test": "bun run lint:styles && bun test --isolate",
|
|
54
54
|
"test:coverage": "bun test --isolate --coverage",
|
|
55
|
-
"upgrade": "
|
|
55
|
+
"upgrade": "bun update --latest && bun install"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@atlaskit/pragmatic-drag-and-drop": "^3.0.0",
|
|
59
59
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.1.0",
|
|
60
|
-
"@jxsuite/ai": "^0.36.
|
|
61
|
-
"@jxsuite/collab": "^0.8.
|
|
62
|
-
"@jxsuite/create": "^1.3.
|
|
63
|
-
"@jxsuite/formulas": "^0.0.
|
|
64
|
-
"@jxsuite/markup": "^0.4.
|
|
65
|
-
"@jxsuite/protocol": "^2.
|
|
66
|
-
"@jxsuite/runtime": "^
|
|
67
|
-
"@jxsuite/schema": "^
|
|
60
|
+
"@jxsuite/ai": "^0.36.5",
|
|
61
|
+
"@jxsuite/collab": "^0.8.4",
|
|
62
|
+
"@jxsuite/create": "^1.3.8",
|
|
63
|
+
"@jxsuite/formulas": "^0.0.15",
|
|
64
|
+
"@jxsuite/markup": "^0.4.8",
|
|
65
|
+
"@jxsuite/protocol": "^2.1.0",
|
|
66
|
+
"@jxsuite/runtime": "^3.0.0",
|
|
67
|
+
"@jxsuite/schema": "^2.0.0",
|
|
68
|
+
"@jxsuite/site": "^1.0.0",
|
|
68
69
|
"@spectrum-web-components/accordion": "^1.12.2",
|
|
69
70
|
"@spectrum-web-components/action-bar": "1.12.2",
|
|
70
71
|
"@spectrum-web-components/action-button": "^1.12.2",
|
|
@@ -107,15 +108,15 @@
|
|
|
107
108
|
"lit": "^3.3.3",
|
|
108
109
|
"lit-html": "^3.3.3",
|
|
109
110
|
"monaco-editor": "^0.56.0",
|
|
110
|
-
"tabulator-tables": "^6.5",
|
|
111
|
+
"tabulator-tables": "^6.5.2",
|
|
111
112
|
"yaml": "^2.9.0"
|
|
112
113
|
},
|
|
113
114
|
"devDependencies": {
|
|
114
|
-
"@happy-dom/global-registrator": "^20.11.
|
|
115
|
+
"@happy-dom/global-registrator": "^20.11.8",
|
|
115
116
|
"@types/mdast": "^4.0.4",
|
|
116
|
-
"@webref/css": "^8.7.
|
|
117
|
-
"@webref/elements": "^2.
|
|
118
|
-
"@webref/idl": "^3.
|
|
117
|
+
"@webref/css": "^8.7.3",
|
|
118
|
+
"@webref/elements": "^2.8.0",
|
|
119
|
+
"@webref/idl": "^3.83.0"
|
|
119
120
|
},
|
|
120
121
|
"//vue-reactivity": "Exact pin required — must match @jxsuite/runtime to avoid cross-boundary proxy bugs"
|
|
121
122
|
}
|
|
@@ -42,6 +42,7 @@ import { invalidateUsages } from "../services/references";
|
|
|
42
42
|
import { confirmFileDelete, renamePromptMessage } from "../files/file-ops";
|
|
43
43
|
import { createFileIn, openFileInTab } from "../files/files";
|
|
44
44
|
import { createEntry } from "../content/entry-commands";
|
|
45
|
+
import { formatByExtension, formatSerialize } from "../format/format-host";
|
|
45
46
|
import { entryCollections } from "../content/entry-model";
|
|
46
47
|
import { uploadAccept, uploadAssets } from "../files/media-upload";
|
|
47
48
|
import { localeLabel } from "@jxsuite/schema/locale";
|
|
@@ -70,6 +71,7 @@ import {
|
|
|
70
71
|
tableRowsTpl,
|
|
71
72
|
} from "./library-layouts";
|
|
72
73
|
import type { ActivityHandle } from "../panels/activity-panel";
|
|
74
|
+
import type { FormatChoice } from "../files/files";
|
|
73
75
|
import type { EffectScope } from "@vue/reactivity";
|
|
74
76
|
import type { LibraryFile, LibraryLayout } from "./library-model";
|
|
75
77
|
import type { LibrarySource } from "./library-source";
|
|
@@ -602,10 +604,60 @@ export function libraryNewEntries(): LibraryNewEntry[] {
|
|
|
602
604
|
* `createEntry` is called rather than reimplemented; a second seeder here would drift from the one
|
|
603
605
|
* the palette's `content.newEntry` uses within a release.
|
|
604
606
|
*
|
|
605
|
-
* A **document kind**
|
|
606
|
-
* as easily as a `.json`, so the extension
|
|
607
|
-
*
|
|
607
|
+
* A **document kind** gets the format PICKER, which is the same argument the verbatim name field
|
|
608
|
+
* used to make: a page may be a `.md` as easily as a `.json`, so the extension is a real choice —
|
|
609
|
+
* now offered rather than left to be typed from memory. A collection's extension is not a choice;
|
|
610
|
+
* it is the collection's, and `createEntry` supplies it.
|
|
611
|
+
*
|
|
612
|
+
* **A layout is not a choice either, and that is not a policy.** Both readers of a layout parse it
|
|
613
|
+
* as JSON and neither dispatches through the format registry — `site/layout-resolver.ts` throws and
|
|
614
|
+
* `site-context.ts` returns null — so a `.md` layout is a file the build cannot load. There is no
|
|
615
|
+
* `"layout"` document kind for a format to declare, so this is stated here rather than derived.
|
|
616
|
+
*
|
|
617
|
+
* A **component** keeps the picker: a format whose `$studio.documentMode.componentWhen` promotes a
|
|
618
|
+
* frontmatter `tagName` (Markdown's does) can express one perfectly well. What it cannot do is
|
|
619
|
+
* express one from a `newFileTemplate` that carries no `tagName`, so the seed below supplies it —
|
|
620
|
+
* through the format's own serializer, because this module knows no format's syntax.
|
|
621
|
+
*/
|
|
622
|
+
/** How each document kind's extension is settled. Anything else falls back to the full picker. */
|
|
623
|
+
const FORMAT_CHOICE_BY_KIND: Record<string, FormatChoice> = {
|
|
624
|
+
component: { defaultExt: ".json", docKind: "component", kind: "choose" },
|
|
625
|
+
layout: { ext: ".json", kind: "fixed" },
|
|
626
|
+
page: { defaultExt: ".json", docKind: "page", kind: "choose" },
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* The body a new component starts as, when the reader picked a format rather than `.json`.
|
|
631
|
+
*
|
|
632
|
+
* `.json` answers `undefined` and takes the shared blank document. A format answers with its OWN
|
|
633
|
+
* serialization of `{ tagName }` — never a hand-written `---\ntagName: …\n---`, which would put one
|
|
634
|
+
* format's syntax in the Library. Without the `tagName` the file satisfies neither
|
|
635
|
+
* `$studio.documentMode.componentWhen` nor the build's tag registration, and is a component nothing
|
|
636
|
+
* loads. A stem that is not a valid custom-element name (no hyphen) cannot carry one, so it falls
|
|
637
|
+
* back to the format's own template and the author names the tag themselves.
|
|
608
638
|
*/
|
|
639
|
+
async function componentSeed(ext: string, fileName: string): Promise<string | undefined> {
|
|
640
|
+
const format = formatByExtension(ext);
|
|
641
|
+
if (!format?.capabilities.serialize) {
|
|
642
|
+
return undefined;
|
|
643
|
+
}
|
|
644
|
+
const tagName = fileName.slice(0, fileName.length - ext.length);
|
|
645
|
+
if (!/^[a-z][\d.a-z]*-[\d.a-z-]*$/.test(tagName)) {
|
|
646
|
+
return undefined;
|
|
647
|
+
}
|
|
648
|
+
try {
|
|
649
|
+
return await formatSerialize(
|
|
650
|
+
format.name,
|
|
651
|
+
{ children: [], tagName },
|
|
652
|
+
{ frontmatter: true, mode: "roundtrip" },
|
|
653
|
+
);
|
|
654
|
+
} catch {
|
|
655
|
+
// The serializer is the project's, so its failure is not a reason to refuse the creation —
|
|
656
|
+
// The format's own template still produces a file the author can finish by hand.
|
|
657
|
+
return undefined;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
609
661
|
export async function createLibraryEntry(key: string): Promise<string | null> {
|
|
610
662
|
const entry = libraryNewEntries().find((row) => row.key === key);
|
|
611
663
|
if (!entry) {
|
|
@@ -614,7 +666,9 @@ export async function createLibraryEntry(key: string): Promise<string | null> {
|
|
|
614
666
|
const created =
|
|
615
667
|
entry.collection === undefined
|
|
616
668
|
? await createFileIn({
|
|
669
|
+
...(entry.key === "component" ? { content: componentSeed } : {}),
|
|
617
670
|
dir: entry.dir,
|
|
671
|
+
format: FORMAT_CHOICE_BY_KIND[entry.key] ?? { defaultExt: ".json", kind: "choose" },
|
|
618
672
|
source: "Library",
|
|
619
673
|
suggestedName: "untitled",
|
|
620
674
|
title: `New ${entry.label}`,
|
|
@@ -37,10 +37,12 @@ import { attachCursorStyles } from "../collab/monaco-cursors";
|
|
|
37
37
|
import type { AwarenessLike } from "../collab/monaco-cursors";
|
|
38
38
|
import type { BindingAwareness } from "../collab/monaco-binding";
|
|
39
39
|
import { monacoTheme, shell } from "../shell";
|
|
40
|
-
import { parseSourceForPath
|
|
40
|
+
import { parseSourceForPath } from "../files/file-ops";
|
|
41
|
+
import { serializeDocument } from "../files/serialize-document";
|
|
41
42
|
import { detachGridPanel, gridPanelMounted, renderGridMode } from "../grid/grid-panel";
|
|
42
43
|
import { detachLibraryPane, libraryPaneMounted, renderLibraryMode } from "../browse/library-pane";
|
|
43
44
|
import { detachEntryPane, entryPaneMounted, renderEntryMode } from "../content/entry-editor";
|
|
45
|
+
import { detachMediaPane, mediaPaneMounted, renderMediaMode } from "../media/media-pane";
|
|
44
46
|
import {
|
|
45
47
|
detachSettingsPane,
|
|
46
48
|
renderSettingsPane,
|
|
@@ -370,6 +372,7 @@ function resetCanvasView(surface: CanvasSurface) {
|
|
|
370
372
|
detachGridPanel(paneId);
|
|
371
373
|
detachLibraryPane(paneId);
|
|
372
374
|
detachEntryPane(paneId);
|
|
375
|
+
detachMediaPane(paneId);
|
|
373
376
|
detachSettingsPane(paneId);
|
|
374
377
|
disposeSourceCollab(surface);
|
|
375
378
|
disposeSourceEditor(surface);
|
|
@@ -883,6 +886,12 @@ function renderCanvasImpl(surface: CanvasSurface) {
|
|
|
883
886
|
return;
|
|
884
887
|
}
|
|
885
888
|
|
|
889
|
+
// Media fast-path, same shape: the viewer owns its own effect, so learning an image's dimensions
|
|
890
|
+
// Repaints that panel and nothing reaches the canvas pipeline.
|
|
891
|
+
if (canvasMode === "media" && mediaPaneMounted(surface.paneId, tab) && !modeChanged) {
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
|
|
886
895
|
// Settings fast-path, same shape: the Project Settings editor subscribes to the section registry
|
|
887
896
|
// And to its own chosen section, so a mounted editor needs nothing from the canvas pipeline.
|
|
888
897
|
if (canvasMode === "settings" && settingsPaneMounted(surface) && !modeChanged) {
|
|
@@ -953,6 +962,9 @@ function renderCanvasImpl(surface: CanvasSurface) {
|
|
|
953
962
|
// Same for the entry form, whose effect scope subscribes to the document's frontmatter
|
|
954
963
|
detachEntryPane(surface.paneId);
|
|
955
964
|
|
|
965
|
+
// And for the media viewer, whose effect scope subscribes to the tab's path
|
|
966
|
+
detachMediaPane(surface.paneId);
|
|
967
|
+
|
|
956
968
|
// Same for the Project Settings editor, which holds a registry subscription
|
|
957
969
|
detachSettingsPane(surface.paneId);
|
|
958
970
|
|
|
@@ -1068,6 +1080,15 @@ function renderCanvasImpl(surface: CanvasSurface) {
|
|
|
1068
1080
|
return;
|
|
1069
1081
|
}
|
|
1070
1082
|
|
|
1083
|
+
/* Media mode: an image, a video, a font specimen — a file the studio can SHOW but has no document
|
|
1084
|
+
model for (`media/media-pane.ts`). Same non-iframe-editor pattern as the four above. */
|
|
1085
|
+
if (canvasMode === "media") {
|
|
1086
|
+
canvasWrap.style.padding = "0";
|
|
1087
|
+
canvasWrap.style.display = "block";
|
|
1088
|
+
renderMediaMode(surface, tab);
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1071
1092
|
// Grid mode: spreadsheet editor over the tab's grid source (mirrors the Monaco/stylebook
|
|
1072
1093
|
// Non-iframe-editor pattern; the panel owns its own reactivity from here).
|
|
1073
1094
|
if (canvasMode === "grid") {
|
|
@@ -1508,6 +1529,7 @@ setSurfaceTeardown((surface) => {
|
|
|
1508
1529
|
detachGridPanel(surface.paneId);
|
|
1509
1530
|
detachLibraryPane(surface.paneId);
|
|
1510
1531
|
detachEntryPane(surface.paneId);
|
|
1532
|
+
detachMediaPane(surface.paneId);
|
|
1511
1533
|
detachSettingsPane(surface.paneId);
|
|
1512
1534
|
attachDocumentHeaderHost(surface.paneId, null);
|
|
1513
1535
|
disposeSourceCollab(surface);
|
|
@@ -56,12 +56,38 @@ export function applyIframePatch(
|
|
|
56
56
|
const echoed = new Set(echoPaths.map((p) => serializeJxPath(p)));
|
|
57
57
|
for (const op of forwardOps) {
|
|
58
58
|
if (op.op === "set-key" && echoed.has(serializeJxPath(op.path))) {
|
|
59
|
+
// The content is already right; only the model-derived placeholder class can be stale, and
|
|
60
|
+
// Reconciling a class touches neither the caret nor the text under it.
|
|
61
|
+
syncEchoedPlaceholder(shadowDoc, op.path, container);
|
|
59
62
|
continue;
|
|
60
63
|
}
|
|
61
64
|
applyOpToDom(shadowDoc, op, container, ctx);
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Re-derive the empty-placeholder class of an ECHOED node without writing its content.
|
|
70
|
+
*
|
|
71
|
+
* An echo is the author's own typing coming back as a patch, so the DOM already holds the text —
|
|
72
|
+
* but `empty-text-placeholder` was stamped when the block was empty and nothing else would ever
|
|
73
|
+
* take it off, because the write that would have (patchText) is exactly what echo suppression
|
|
74
|
+
* skips. The CSS asks the DOM whether the block is empty, so nothing is misdrawn in the meantime;
|
|
75
|
+
* this keeps the class itself honest, so the DOM does not carry a claim the document contradicts.
|
|
76
|
+
*
|
|
77
|
+
* Best-effort by design: a missing element is not an error worth escalating a full render (and the
|
|
78
|
+
* caret with it) over a class.
|
|
79
|
+
*/
|
|
80
|
+
function syncEchoedPlaceholder(
|
|
81
|
+
doc: JxMutableNode,
|
|
82
|
+
path: (string | number)[],
|
|
83
|
+
container: HTMLElement,
|
|
84
|
+
): void {
|
|
85
|
+
const el = findElement(container, path);
|
|
86
|
+
if (el) {
|
|
87
|
+
syncPlaceholderClass(el, getNodeAtPath(doc, path));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
65
91
|
/** The render context, asserted present for ops that re-render a subtree (the caller always has it). */
|
|
66
92
|
function requireCtx(ctx: IframeRenderCtx | undefined): IframeRenderCtx {
|
|
67
93
|
if (!ctx) {
|
|
@@ -134,7 +160,7 @@ function insertChild(
|
|
|
134
160
|
// Remap existing siblings BEFORE attaching the new subtree so it isn't itself remapped.
|
|
135
161
|
remapChildPaths(container, parentPath, index, 1);
|
|
136
162
|
insertAt(parentEl, newEl, domChildReference(parentEl, parentNode, index));
|
|
137
|
-
|
|
163
|
+
syncPlaceholderClass(parentEl, parentNode);
|
|
138
164
|
}
|
|
139
165
|
|
|
140
166
|
/** Re-render the node at `docPath` and swap it into the DOM in place (disposing the old subtree). */
|
|
@@ -165,7 +191,7 @@ function removeChild(
|
|
|
165
191
|
el.remove();
|
|
166
192
|
remapChildPaths(container, parentPath, index + 1, -1);
|
|
167
193
|
if (parentEl) {
|
|
168
|
-
|
|
194
|
+
syncPlaceholderClass(parentEl, getNodeAtPath(doc, parentPath));
|
|
169
195
|
}
|
|
170
196
|
}
|
|
171
197
|
|
|
@@ -202,25 +228,25 @@ function moveChild(
|
|
|
202
228
|
// Emptiness may have changed at either end (e.g. moved the last/only child out, or into an empty
|
|
203
229
|
// Container) — keep both parents' placeholder classes in sync with the folded doc.
|
|
204
230
|
if (fromParentEl) {
|
|
205
|
-
|
|
231
|
+
syncPlaceholderClass(
|
|
206
232
|
fromParentEl,
|
|
207
233
|
getNodeAtPath(doc, elementPath(fromParentEl) ?? fromParentPath),
|
|
208
234
|
);
|
|
209
235
|
}
|
|
210
|
-
|
|
236
|
+
syncPlaceholderClass(toParentEl, toParentNode);
|
|
211
237
|
}
|
|
212
238
|
|
|
213
239
|
/**
|
|
214
|
-
*
|
|
215
|
-
* change.
|
|
240
|
+
* Put `el`'s empty-placeholder class back in step with its (folded) doc node — a container's after
|
|
241
|
+
* a child change, a text block's after its content changed.
|
|
216
242
|
*/
|
|
217
|
-
function
|
|
243
|
+
function syncPlaceholderClass(el: Element, node: JxMutableNode | undefined): void {
|
|
218
244
|
for (const cls of EMPTY_PLACEHOLDER_CLASSES) {
|
|
219
|
-
|
|
245
|
+
el.classList.remove(cls);
|
|
220
246
|
}
|
|
221
|
-
const placeholder =
|
|
247
|
+
const placeholder = node ? computeEmptyPlaceholderClass(node) : null;
|
|
222
248
|
if (placeholder) {
|
|
223
|
-
|
|
249
|
+
el.classList.add(placeholder);
|
|
224
250
|
}
|
|
225
251
|
}
|
|
226
252
|
|
|
@@ -321,24 +347,24 @@ function patchText(doc: JxMutableNode, path: (string | number)[], container: HTM
|
|
|
321
347
|
const el = requireElement(container, path);
|
|
322
348
|
const node = getNodeAtPath(doc, path);
|
|
323
349
|
el.textContent = textDisplayValue(node);
|
|
324
|
-
|
|
325
|
-
el.classList.remove(cls);
|
|
326
|
-
}
|
|
327
|
-
const placeholder = node ? computeEmptyPlaceholderClass(node) : null;
|
|
328
|
-
if (placeholder) {
|
|
329
|
-
el.classList.add(placeholder);
|
|
330
|
-
}
|
|
350
|
+
syncPlaceholderClass(el, node);
|
|
331
351
|
}
|
|
332
352
|
|
|
333
353
|
/** Locate the rendered element for a document path via its stamped `data-jx-path` attribute. */
|
|
334
354
|
function requireElement(container: HTMLElement, path: (string | number)[]): HTMLElement {
|
|
355
|
+
const el = findElement(container, path);
|
|
356
|
+
if (!el) {
|
|
357
|
+
throw new Error(`iframe-patch-element-not-found:${serializeJxPath(path)}`);
|
|
358
|
+
}
|
|
359
|
+
return el;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** The rendered element for a document path, or null when this render has none. */
|
|
363
|
+
function findElement(container: HTMLElement, path: (string | number)[]): HTMLElement | null {
|
|
335
364
|
const serialized = serializeJxPath(path);
|
|
336
365
|
const esc = serialized.replaceAll("\\", String.raw`\\`).replaceAll("'", String.raw`\'`);
|
|
337
366
|
const el = container.querySelector(`[data-jx-path='${esc}']`);
|
|
338
|
-
|
|
339
|
-
throw new Error(`iframe-patch-element-not-found:${serialized}`);
|
|
340
|
-
}
|
|
341
|
-
return el;
|
|
367
|
+
return el instanceof HTMLElement ? el : null;
|
|
342
368
|
}
|
|
343
369
|
|
|
344
370
|
/**
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
import { resolveAssetRef } from "./asset-resolve";
|
|
29
29
|
import { classifyRenderNode, serializeJxPath } from "./path-mapping";
|
|
30
30
|
import type { AssetContext } from "./asset-resolve";
|
|
31
|
-
import { SITE_STYLE_ID, buildSiteStyleCSS } from "
|
|
31
|
+
import { SITE_STYLE_ID, buildSiteStyleCSS } from "@jxsuite/site/site-style";
|
|
32
32
|
import type { CanvasMode } from "./iframe-protocol";
|
|
33
33
|
import type { JxDocument } from "@jxsuite/schema/types";
|
|
34
34
|
import type { PathMapCtx } from "./path-mapping";
|
|
@@ -105,9 +105,25 @@ export const EDIT_PLACEHOLDER_STYLE_ID = "jx-canvas-edit-css";
|
|
|
105
105
|
* iframe-safe fallbacks — the parent theme variables (--fg-dim/--radius/--accent) don't exist in
|
|
106
106
|
* the iframe document. The placeholder CLASSES are stamped by prepareForEditMode (parent-side
|
|
107
107
|
* resolution + surgical subtree renders), so preview mode never matches these rules — but the sheet
|
|
108
|
-
* is still removed there (belt and braces). The `[
|
|
109
|
-
*
|
|
110
|
-
*
|
|
108
|
+
* is still removed there (belt and braces). The `[data-jx-active-block]` hint marks the caret's
|
|
109
|
+
* empty paragraph (e.g. right after an Enter split) and advertises the slash menu.
|
|
110
|
+
*
|
|
111
|
+
* `.empty-text-placeholder` is gated on emptiness IN THE DOM because a CLASS cannot tell the truth
|
|
112
|
+
* about a block the author is typing in. The class is stamped from the document, and the first
|
|
113
|
+
* thing typed into an empty block reaches the DOM natively — the model learns of it a commit tick
|
|
114
|
+
* later, and that patch comes back as an ECHO, which `applyIframePatch` deliberately does not
|
|
115
|
+
* re-render (it would destroy the caret). So the class outlived its own meaning, and "Click here to
|
|
116
|
+
* add text..." sat beside the text you had just typed until the next full render. The class now
|
|
117
|
+
* selects WHICH affordance; the DOM answers whether the block is empty.
|
|
118
|
+
*
|
|
119
|
+
* "Empty" is `:empty` OR a lone `<br>`, because emptying a block by editing does not leave it
|
|
120
|
+
* `:empty`: the engine drops in a filler `<br>` to keep the line visible (Chrome 151, verified —
|
|
121
|
+
* `execCommand("delete")` over a `<p>`'s whole content leaves `<p><br></p>`). Without that arm the
|
|
122
|
+
* affordance would go dark exactly when the block became empty again.
|
|
123
|
+
*
|
|
124
|
+
* `:not([data-jx-active-block])` keeps the slash hint winning on an empty block that HAS the caret:
|
|
125
|
+
* both rules match the same element, and the emptiness test broke the specificity tie that used to
|
|
126
|
+
* decide it by source order.
|
|
111
127
|
*/
|
|
112
128
|
export const EDIT_PLACEHOLDER_CSS = `
|
|
113
129
|
.empty-media-placeholder {
|
|
@@ -123,7 +139,7 @@ export const EDIT_PLACEHOLDER_CSS = `
|
|
|
123
139
|
font-size: 0;
|
|
124
140
|
overflow: hidden;
|
|
125
141
|
}
|
|
126
|
-
.empty-text-placeholder:not([contenteditable])::after {
|
|
142
|
+
.empty-text-placeholder:is(:empty, :has(> br:only-child)):not([data-jx-active-block]):not([contenteditable])::after {
|
|
127
143
|
content: "Click here to add text...";
|
|
128
144
|
color: color-mix(in srgb, #808080 40%, transparent);
|
|
129
145
|
font-style: italic;
|
|
@@ -149,7 +165,7 @@ export const EDIT_PLACEHOLDER_CSS = `
|
|
|
149
165
|
pointer-events: none;
|
|
150
166
|
white-space: nowrap;
|
|
151
167
|
}
|
|
152
|
-
[data-jx-active-block]:empty::after {
|
|
168
|
+
[data-jx-active-block]:is(:empty, :has(> br:only-child))::after {
|
|
153
169
|
content: "Type / for commands";
|
|
154
170
|
color: color-mix(in srgb, #808080 40%, transparent);
|
|
155
171
|
font-style: italic;
|
|
@@ -61,6 +61,7 @@ import { i18nCommands } from "../i18n/i18n-commands";
|
|
|
61
61
|
import { newProjectCommands } from "../new-project/new-project-modal";
|
|
62
62
|
import { canvasCommands } from "../editor/shortcuts";
|
|
63
63
|
import { formatCommands, registerSelectionCommands } from "../panels/block-action-bar";
|
|
64
|
+
import { fileFormatCommands } from "../format/convert-file";
|
|
64
65
|
import { registerTabCommands } from "../workspace/workspace";
|
|
65
66
|
import { derivationCommands, noopDerivationDeps } from "../workspace/pane-derive";
|
|
66
67
|
import type { AnyCommand } from "./registry";
|
|
@@ -154,6 +155,9 @@ export function appCommandSet(): AnyCommand[] {
|
|
|
154
155
|
// That renders them, and projected here so the palette, the level check and the generated
|
|
155
156
|
// Keyboard sheet all see the first `caret`-scoped chords the app has ever had.
|
|
156
157
|
...formatCommands(),
|
|
158
|
+
// The file-format verb: `file.convertFormat`, declared for `context/file` so the Files tree
|
|
159
|
+
// Renders it off the record rather than hand-building a row beside the declared ones.
|
|
160
|
+
...fileFormatCommands(),
|
|
157
161
|
/* The canvas keyboard's own nine, plus the clipboard pair they spread.
|
|
158
162
|
These were the last records the app registered and this projection did not contain, so ⌘C,
|
|
159
163
|
⌘X, ⌘V, Enter, the three structural arrows and all three zoom chords were absent from
|
|
@@ -216,6 +216,35 @@ export function enumProperty(declared: readonly string[], description: string):
|
|
|
216
216
|
return { description, enum: [...declared], type: "string" };
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* An enum property whose values are derived WHEN THE SCHEMA IS READ, not when the record is
|
|
221
|
+
* defined.
|
|
222
|
+
*
|
|
223
|
+
* `enumProperty(collections(), …)` looks identical and is wrong for anything a PROJECT supplies,
|
|
224
|
+
* and the difference is invisible in a unit test: a command record is built at module scope —
|
|
225
|
+
* `commands/app-commands.ts` and `studio.ts` both build theirs before a project is open — so the
|
|
226
|
+
* array freezes at `[]`, and `panels/quick-search.ts`'s `paletteArgs` offers an empty choice list
|
|
227
|
+
* forever after. `run` re-derives the list, which is why the programmatic call works and the
|
|
228
|
+
* palette does not: two answers to "what is there", one of them a snapshot of the empty boot
|
|
229
|
+
* state.
|
|
230
|
+
*
|
|
231
|
+
* A getter is the whole fix — the palette reads `property.enum` when it opens the prompt, the AI
|
|
232
|
+
* tool's parameters serialise it when the tool list is built, and `scripts/check-shot-contract.ts`
|
|
233
|
+
* reads it in a bare Bun process where the honest answer really is "none declared".
|
|
234
|
+
*/
|
|
235
|
+
export function derivedEnumProperty(
|
|
236
|
+
declared: () => readonly string[],
|
|
237
|
+
description: string,
|
|
238
|
+
): object {
|
|
239
|
+
return {
|
|
240
|
+
description,
|
|
241
|
+
get enum() {
|
|
242
|
+
return [...declared()];
|
|
243
|
+
},
|
|
244
|
+
type: "string",
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
219
248
|
/** A number property, optionally bounded — the bounds a control already enforces, written down. */
|
|
220
249
|
export function numberProperty(
|
|
221
250
|
description: string,
|
package/src/commands/context.ts
CHANGED
|
@@ -55,6 +55,7 @@ export type EditorKind =
|
|
|
55
55
|
| "library"
|
|
56
56
|
| "config"
|
|
57
57
|
| "entry"
|
|
58
|
+
| "media"
|
|
58
59
|
| "none";
|
|
59
60
|
|
|
60
61
|
/** The Canvas view axis — one control, three values (§4.2). */
|
|
@@ -87,6 +88,10 @@ const EDITOR_KIND_BY_MODE: Readonly<Record<string, EditorKind>> = {
|
|
|
87
88
|
"git-diff": "diff",
|
|
88
89
|
grid: "grid",
|
|
89
90
|
manage: "library",
|
|
91
|
+
/* The Media viewer — `media/media-pane.ts`. An image, a video, a font specimen: files the studio
|
|
92
|
+
can SHOW but has no document model for, which is why clicking one used to end in "No format
|
|
93
|
+
class imported for …", advice that means nothing about a PNG. */
|
|
94
|
+
media: "media",
|
|
90
95
|
preview: "canvas",
|
|
91
96
|
/* Project Settings — `settings/settings-document.ts`'s SETTINGS_MODE. A form over `project.json`,
|
|
92
97
|
which is a configuration object and not a document tree; `stylebook` (Project Styles) is the
|
|
@@ -117,6 +122,7 @@ export const EDITOR_KIND_LABELS: Readonly<Record<EditorKind, string>> = {
|
|
|
117
122
|
diff: "Diff",
|
|
118
123
|
entry: "Entry",
|
|
119
124
|
grid: "Grid",
|
|
125
|
+
media: "Media",
|
|
120
126
|
library: "Library",
|
|
121
127
|
none: "None",
|
|
122
128
|
};
|
|
@@ -232,6 +238,14 @@ export interface CommandContext {
|
|
|
232
238
|
ai: {
|
|
233
239
|
configured: boolean;
|
|
234
240
|
streaming: boolean;
|
|
241
|
+
/**
|
|
242
|
+
* A turn is suspended on an `ask_user` question.
|
|
243
|
+
*
|
|
244
|
+
* Distinct from `streaming` rather than folded into it: no tokens are moving, so a surface that
|
|
245
|
+
* paints a spinner on `streaming` must not paint one here — but the turn is alive and holding a
|
|
246
|
+
* tool open, so Stop is enabled on the union of the two.
|
|
247
|
+
*/
|
|
248
|
+
waiting: boolean;
|
|
235
249
|
};
|
|
236
250
|
capability: Record<Capability, boolean>;
|
|
237
251
|
}
|
|
@@ -287,7 +301,7 @@ export function emptyContext(): CommandContext {
|
|
|
287
301
|
focus: { region: "pane" },
|
|
288
302
|
modal: { open: false },
|
|
289
303
|
collab: { attached: false, readOnly: false, sourceCanonical: false },
|
|
290
|
-
ai: { configured: false, streaming: false },
|
|
304
|
+
ai: { configured: false, streaming: false, waiting: false },
|
|
291
305
|
capability: {
|
|
292
306
|
gitClone: false,
|
|
293
307
|
importSite: false,
|
package/src/commands/defaults.ts
CHANGED
|
@@ -102,6 +102,7 @@ export interface CommandDeps {
|
|
|
102
102
|
undo: () => void;
|
|
103
103
|
redo: () => void;
|
|
104
104
|
openInBrowser: () => void | Promise<void>;
|
|
105
|
+
buildSite: () => void | Promise<void>;
|
|
105
106
|
closeDocument: () => void;
|
|
106
107
|
duplicateSelection: () => void;
|
|
107
108
|
deleteSelection: () => void;
|
|
@@ -147,6 +148,7 @@ export function noopCommandDeps(): CommandDeps {
|
|
|
147
148
|
undo: () => {},
|
|
148
149
|
redo: () => {},
|
|
149
150
|
openInBrowser: () => {},
|
|
151
|
+
buildSite: () => {},
|
|
150
152
|
closeDocument: () => {},
|
|
151
153
|
duplicateSelection: () => {},
|
|
152
154
|
deleteSelection: () => {},
|
|
@@ -243,9 +245,35 @@ export function defaultCommands(deps: CommandDeps): AnyCommand[] {
|
|
|
243
245
|
group: "2_output",
|
|
244
246
|
when: (ctx) => ctx.project.isSite,
|
|
245
247
|
enablement: documentOpen,
|
|
246
|
-
|
|
248
|
+
/* "A page to preview", not "a built page to open". Nothing is built on the way here any
|
|
249
|
+
more: the working tree is rendered at its real route, so what this waits for is a document
|
|
250
|
+
with a route rather than an output file. */
|
|
251
|
+
requires: "a page to preview",
|
|
247
252
|
run: () => deps.openInBrowser(),
|
|
248
253
|
},
|
|
254
|
+
{
|
|
255
|
+
/*
|
|
256
|
+
* The compiler, kept reachable under its own verb.
|
|
257
|
+
*
|
|
258
|
+
* `View: Open in Browser` used to run a full build, which answered "does my site build?" as a
|
|
259
|
+
* side effect of asking "what does this page look like?". Those are different questions and
|
|
260
|
+
* only one of them is worth waiting for a bundler, an image pipeline and every emitter. So
|
|
261
|
+
* the build keeps a command of its own rather than disappearing with the coupling.
|
|
262
|
+
*
|
|
263
|
+
* `commandbar/overflow` rather than `commandbar/primary`: the primary row is budgeted at five
|
|
264
|
+
* and is document-level by frequency (studio-ui-guidelines §12), and a build is neither
|
|
265
|
+
* frequent nor about the document in front of you. No default chord for the same reason.
|
|
266
|
+
*/
|
|
267
|
+
id: "project.buildSite",
|
|
268
|
+
title: "Build Site",
|
|
269
|
+
category: "Project",
|
|
270
|
+
level: "project",
|
|
271
|
+
menus: ["commandbar/overflow", "palette"],
|
|
272
|
+
group: "2_output",
|
|
273
|
+
when: (ctx) => ctx.project.isSite,
|
|
274
|
+
requires: "a site project",
|
|
275
|
+
run: () => deps.buildSite(),
|
|
276
|
+
},
|
|
249
277
|
|
|
250
278
|
// ── Edit ──
|
|
251
279
|
{
|
package/src/commands/levels.ts
CHANGED
|
@@ -77,6 +77,14 @@ export const PLACEMENTS = [
|
|
|
77
77
|
*/
|
|
78
78
|
"blockbar/format",
|
|
79
79
|
"outline/row",
|
|
80
|
+
/**
|
|
81
|
+
* The rail foot's ⚙ Settings menu — the settings FAMILY, and only it.
|
|
82
|
+
*
|
|
83
|
+
* A separate placement rather than a second use of `commandbar/overflow`, for the reason the
|
|
84
|
+
* status bar is three placements: the ⬢ menu is the chrome's residue for retired controls and
|
|
85
|
+
* this is a named family with its own trigger. Same admits, different surface, different budget.
|
|
86
|
+
*/
|
|
87
|
+
"settings/menu",
|
|
80
88
|
"palette",
|
|
81
89
|
"never",
|
|
82
90
|
] as const;
|
|
@@ -119,6 +127,14 @@ export const PLACEMENT_MATRIX: Readonly<Record<Placement, PlacementRule>> = {
|
|
|
119
127
|
note: "the bar's inline-format cluster — a range inside the selection is still the selection",
|
|
120
128
|
},
|
|
121
129
|
"outline/row": { admits: ["selection"], note: "row actions act on the row's node" },
|
|
130
|
+
"settings/menu": {
|
|
131
|
+
admits: ["application", "project"],
|
|
132
|
+
note:
|
|
133
|
+
"the rail foot's gear menu — the settings family. It is a MENU, not a pinned slot, so it " +
|
|
134
|
+
"may host two levels for the reason commandbar/overflow does: a menu prints each row's own " +
|
|
135
|
+
"name, chord and gate, and draws a divider where the level changes. The rail's PINNED " +
|
|
136
|
+
"groups stay single-level, in PANEL_PLACEMENT_MATRIX",
|
|
137
|
+
},
|
|
122
138
|
palette: {
|
|
123
139
|
admits: ["application", "project", "document", "selection"],
|
|
124
140
|
note: "the level-agnostic surface; it groups rows by level",
|
|
@@ -72,6 +72,8 @@ export interface LiveContextSources {
|
|
|
72
72
|
* private, so there is nothing to read yet; the caller passes a probe when one exists.
|
|
73
73
|
*/
|
|
74
74
|
aiStreaming?: () => boolean;
|
|
75
|
+
/** Whether a turn is suspended on an `ask_user` question (`panels/ai-panel.ts`). */
|
|
76
|
+
aiWaiting?: () => boolean;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
/** `capability.*` is presence-of-method on the PAL — the check every call site does by hand today. */
|
|
@@ -187,6 +189,7 @@ export function createLiveContext(sources: LiveContextSources): () => CommandCon
|
|
|
187
189
|
|
|
188
190
|
ctx.ai.configured = sources.aiConfigured();
|
|
189
191
|
ctx.ai.streaming = sources.aiStreaming?.() ?? false;
|
|
192
|
+
ctx.ai.waiting = sources.aiWaiting?.() ?? false;
|
|
190
193
|
ctx.capability = capabilities(sources.platform());
|
|
191
194
|
return ctx;
|
|
192
195
|
};
|