@jxsuite/studio 1.0.0 → 1.2.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/iframe-entry.js +760 -65
- package/dist/iframe-entry.js.map +16 -12
- package/dist/studio.css +1333 -0
- package/dist/studio.js +71573 -50074
- package/dist/studio.js.map +123 -88
- package/package.json +11 -9
- package/src/canvas/canvas-live-render.ts +21 -2
- package/src/canvas/canvas-render.ts +35 -16
- package/src/canvas/canvas-utils.ts +118 -72
- package/src/canvas/iframe-entry.ts +37 -1
- package/src/canvas/iframe-eval.ts +155 -0
- package/src/canvas/iframe-host.ts +193 -6
- package/src/canvas/iframe-inline-edit.ts +107 -1
- package/src/canvas/iframe-protocol.ts +56 -3
- package/src/canvas/iframe-render.ts +53 -14
- package/src/canvas/site-style-css.ts +121 -0
- package/src/collab/collab-session.ts +23 -8
- package/src/collab/collab-state.ts +6 -3
- package/src/component-props.ts +104 -0
- package/src/editor/inline-edit-apply.ts +36 -1
- package/src/editor/inline-edit.ts +58 -1
- package/src/editor/shortcuts.ts +41 -12
- package/src/files/file-ops.ts +14 -0
- package/src/files/files.ts +53 -1
- package/src/grid/cell-editors.ts +288 -0
- package/src/grid/cell-popovers.ts +108 -0
- package/src/grid/csv-codec.ts +122 -0
- package/src/grid/edit-buffer.ts +490 -0
- package/src/grid/grid-controller.ts +417 -0
- package/src/grid/grid-layout.ts +83 -0
- package/src/grid/grid-open.ts +167 -0
- package/src/grid/grid-panel.ts +302 -0
- package/src/grid/grid-source.ts +210 -0
- package/src/grid/grid-view.ts +367 -0
- package/src/grid/schema-columns.ts +261 -0
- package/src/grid/sources/connector-source.ts +217 -0
- package/src/grid/sources/content-source.ts +542 -0
- package/src/grid/sources/csv-file-source.ts +247 -0
- package/src/grid/tabulator-tables.d.ts +120 -0
- package/src/panels/block-action-bar.ts +8 -3
- package/src/panels/chat-panel.ts +98 -0
- package/src/panels/data-grid.ts +9 -387
- package/src/panels/editors.ts +43 -17
- package/src/panels/events-panel.ts +137 -44
- package/src/panels/formula-workspace.ts +379 -0
- package/src/panels/frontmatter-fields.ts +231 -0
- package/src/panels/frontmatter-panel.ts +132 -0
- package/src/panels/git-panel.ts +1 -1
- package/src/panels/head-panel.ts +11 -175
- package/src/panels/properties-panel.ts +154 -144
- package/src/panels/right-panel.ts +9 -47
- package/src/panels/signals-panel.ts +115 -23
- package/src/panels/statement-editor.ts +710 -0
- package/src/panels/style-panel.ts +76 -22
- package/src/panels/stylebook-panel.ts +0 -2
- package/src/panels/tab-bar.ts +214 -10
- package/src/panels/tab-strip.ts +62 -6
- package/src/panels/toolbar.ts +37 -3
- package/src/services/ai-project-tools.ts +541 -0
- package/src/services/ai-session-store.ts +28 -0
- package/src/services/ai-system-prompt.ts +194 -24
- package/src/services/ai-tools.ts +88 -21
- package/src/services/automation.ts +16 -0
- package/src/services/document-assistant.ts +81 -11
- package/src/services/gated-registry.ts +72 -0
- package/src/services/live-preview.ts +0 -0
- package/src/services/preview-eval.ts +68 -0
- package/src/services/project-adoption.ts +31 -0
- package/src/settings/css-vars-editor.ts +122 -4
- package/src/site-context.ts +2 -0
- package/src/store.ts +4 -0
- package/src/studio.ts +90 -52
- package/src/tabs/patch-ops.ts +25 -0
- package/src/tabs/tab.ts +46 -1
- package/src/tabs/transact.ts +60 -13
- package/src/types.ts +12 -0
- package/src/ui/dynamic-slot.ts +272 -0
- package/src/ui/expression-editor.ts +423 -125
- package/src/ui/field-row.ts +5 -0
- package/src/ui/formula-catalog.ts +557 -0
- package/src/ui/formula-chips.ts +216 -0
- package/src/ui/formula-palette.ts +211 -0
- package/src/ui/layers.ts +40 -0
- package/src/ui/media-picker.ts +1 -1
- package/src/ui/panel-resize.ts +15 -1
- package/src/utils/canvas-media.ts +23 -0
- package/src/utils/preview-format.ts +26 -0
- package/src/view.ts +4 -4
- package/src/workspace/workspace.ts +14 -0
|
@@ -12,16 +12,22 @@
|
|
|
12
12
|
import { createChatState, createProxyStreamingClient, createToolRegistry } from "@jxsuite/ai";
|
|
13
13
|
import type { ProjectConfig } from "@jxsuite/schema/types";
|
|
14
14
|
import { getPlatform } from "../platform";
|
|
15
|
-
import { activeTab, workspace } from "../workspace/workspace";
|
|
15
|
+
import { activeTab, setWorkspaceProject, workspace } from "../workspace/workspace";
|
|
16
16
|
import { toRaw } from "../reactivity";
|
|
17
|
+
import { projectState, setProjectState } from "../store";
|
|
18
|
+
import type { Tab } from "../tabs/tab";
|
|
17
19
|
import { componentRegistry } from "../files/components";
|
|
18
20
|
import { registerAiTools } from "./ai-tools";
|
|
21
|
+
import { registerProjectTools } from "./ai-project-tools";
|
|
22
|
+
import { createGatedToolRegistry } from "./gated-registry";
|
|
23
|
+
import type { ToolAvailability } from "./gated-registry";
|
|
24
|
+
import { adoptProject } from "./project-adoption";
|
|
19
25
|
import { runAgentLoop } from "./tool-executor";
|
|
20
|
-
import { buildSystemPrompt } from "./ai-system-prompt";
|
|
26
|
+
import { AI_TOOL_TIERS, buildSystemPrompt, tierActive } from "./ai-system-prompt";
|
|
21
27
|
import { getBaseUrl, getModel, getOpenAiKey } from "./ai-settings";
|
|
22
28
|
import { trimContext } from "./context-manager";
|
|
23
29
|
import { renderCheck } from "./render-critic";
|
|
24
|
-
import { openFileInTab } from "../files/files";
|
|
30
|
+
import { openFileInTab, reloadFileInTab } from "../files/files";
|
|
25
31
|
import * as sessionStore from "./ai-session-store";
|
|
26
32
|
|
|
27
33
|
/**
|
|
@@ -51,8 +57,23 @@ function projectRoot() {
|
|
|
51
57
|
export function createDocumentAssistant() {
|
|
52
58
|
const chatState = createChatState({ model: getModel() });
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
/** The persisted session backing the live chat; null = fresh unsaved chat. */
|
|
61
|
+
let sessionId: string | null = null;
|
|
62
|
+
|
|
63
|
+
const getProjectStyle = () =>
|
|
64
|
+
(workspace.projectConfig as ProjectConfig | null)?.style as Record<string, string> | undefined;
|
|
65
|
+
|
|
66
|
+
const findOpenTab = (path: string): Tab | null => {
|
|
67
|
+
for (const tab of workspace.tabs.values()) {
|
|
68
|
+
if (tab.documentPath === path) {
|
|
69
|
+
return tab;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const innerRegistry = createToolRegistry();
|
|
76
|
+
registerAiTools(innerRegistry, {
|
|
56
77
|
getTab: () => activeTab.value,
|
|
57
78
|
saveFile: async (relPath: string, content: string) => {
|
|
58
79
|
const plat = getPlatform();
|
|
@@ -62,23 +83,72 @@ export function createDocumentAssistant() {
|
|
|
62
83
|
doc: unknown,
|
|
63
84
|
) => Promise<{ ok: true } | { ok: false; error: string }>,
|
|
64
85
|
openDocument: openFileInTab,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
getProjectStyle,
|
|
87
|
+
findOpenTab,
|
|
88
|
+
reloadTab: reloadFileInTab,
|
|
89
|
+
});
|
|
90
|
+
registerProjectTools(innerRegistry, {
|
|
91
|
+
getTab: () => activeTab.value,
|
|
92
|
+
renderCheck: renderCheck as (
|
|
93
|
+
doc: unknown,
|
|
94
|
+
) => Promise<{ ok: true } | { ok: false; error: string }>,
|
|
95
|
+
getProjectStyle,
|
|
96
|
+
findOpenTab,
|
|
97
|
+
reloadTab: reloadFileInTab,
|
|
98
|
+
adoptProject,
|
|
99
|
+
// Re-key the live chat from the unscoped store to the adopted project so the bootstrap
|
|
100
|
+
// Conversation keeps persisting (saveSession drops writes for ids missing from the index).
|
|
101
|
+
onProjectAdopted: (root: string) => {
|
|
102
|
+
if (sessionId) {
|
|
103
|
+
sessionStore.moveSession("", root, sessionId);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
onProjectConfigWritten: (config: object) => {
|
|
107
|
+
setWorkspaceProject(workspace.projectRoot, config);
|
|
108
|
+
if (projectState) {
|
|
109
|
+
setProjectState({ ...projectState, projectConfig: config as ProjectConfig });
|
|
110
|
+
}
|
|
111
|
+
},
|
|
68
112
|
});
|
|
69
113
|
|
|
70
|
-
|
|
114
|
+
/*
|
|
115
|
+
* Gate tool advertisement/execution on live studio state, derived from the same tier table the
|
|
116
|
+
* system prompt renders — the model is never shown a tool it cannot execute. listForLLM() runs
|
|
117
|
+
* every agent-loop round, so a mid-loop create_project unlocks the higher tiers immediately.
|
|
118
|
+
*/
|
|
119
|
+
const TIER_REQUIREMENTS = {
|
|
120
|
+
"no-project": "no project to be open (it bootstraps one)",
|
|
121
|
+
project: "an open project",
|
|
122
|
+
document: "an open document (use open_document first)",
|
|
123
|
+
} as const;
|
|
124
|
+
const availability = new Map<string, ToolAvailability>(
|
|
125
|
+
AI_TOOL_TIERS.map((t) => [
|
|
126
|
+
t.name,
|
|
127
|
+
{
|
|
128
|
+
when: () => tierActive(t.tier, Boolean(workspace.projectRoot), Boolean(activeTab.value)),
|
|
129
|
+
requires: TIER_REQUIREMENTS[t.tier],
|
|
130
|
+
},
|
|
131
|
+
]),
|
|
132
|
+
);
|
|
133
|
+
const toolRegistry = createGatedToolRegistry(innerRegistry, availability);
|
|
71
134
|
|
|
72
|
-
|
|
73
|
-
let sessionId: string | null = null;
|
|
135
|
+
let controller: AbortController | null = null;
|
|
74
136
|
|
|
75
137
|
function buildPrompt() {
|
|
76
138
|
const tab = activeTab.value;
|
|
139
|
+
const inventory = projectState
|
|
140
|
+
? [...projectState.dirs.values()]
|
|
141
|
+
.flat()
|
|
142
|
+
.filter((e) => e.type === "file")
|
|
143
|
+
.map((e) => e.path)
|
|
144
|
+
: undefined;
|
|
77
145
|
return buildSystemPrompt({
|
|
78
146
|
document: tab ? toRaw(tab.doc.document) : undefined,
|
|
79
147
|
projectConfig: (workspace.projectConfig as ProjectConfig | null) || undefined,
|
|
80
148
|
components: componentRegistry.length > 0 ? componentRegistry : undefined,
|
|
81
149
|
projectRoot: workspace.projectRoot || undefined,
|
|
150
|
+
hasProject: Boolean(workspace.projectRoot),
|
|
151
|
+
...(inventory && inventory.length > 0 ? { fileInventory: inventory } : {}),
|
|
82
152
|
});
|
|
83
153
|
}
|
|
84
154
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gated-registry.ts — state-aware tool advertisement for the AI assistant.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a `@jxsuite/ai` ToolRegistry so each tool is only advertised to the LLM while its
|
|
5
|
+
* availability predicate holds (no project open → bootstrap tools only; project open → file tools;
|
|
6
|
+
* document open → document tools). `runAgentLoop` calls `listForLLM()` every round, so availability
|
|
7
|
+
* flips mid-loop: after `create_project` succeeds, the very next round advertises the file and
|
|
8
|
+
* document tools without any extra plumbing.
|
|
9
|
+
*
|
|
10
|
+
* Executing a tool whose predicate currently fails returns a tool error naming the missing state
|
|
11
|
+
* instead of throwing, so the agent loop can self-correct.
|
|
12
|
+
*
|
|
13
|
+
* @license MIT
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ToolDefinition, ToolRegistry } from "@jxsuite/ai/tools";
|
|
17
|
+
|
|
18
|
+
export interface ToolAvailability {
|
|
19
|
+
/** Whether the tool is currently advertised/executable. */
|
|
20
|
+
when: () => boolean;
|
|
21
|
+
/** Human-readable requirement, surfaced when execution is refused (e.g. "a project is open"). */
|
|
22
|
+
requires: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Wrap `inner` so tools listed in `availability` are only advertised/executable while their
|
|
27
|
+
* predicate holds. Tools without an entry are always available.
|
|
28
|
+
*
|
|
29
|
+
* @param {ToolRegistry} inner
|
|
30
|
+
* @param {Map<string, ToolAvailability>} availability
|
|
31
|
+
* @returns {ToolRegistry}
|
|
32
|
+
*/
|
|
33
|
+
export function createGatedToolRegistry(
|
|
34
|
+
inner: ToolRegistry,
|
|
35
|
+
availability: Map<string, ToolAvailability>,
|
|
36
|
+
): ToolRegistry {
|
|
37
|
+
function isAvailable(name: string): boolean {
|
|
38
|
+
const gate = availability.get(name);
|
|
39
|
+
return gate ? gate.when() : true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
register(tool: ToolDefinition) {
|
|
44
|
+
inner.register(tool);
|
|
45
|
+
},
|
|
46
|
+
list() {
|
|
47
|
+
return inner.list().filter((t) => isAvailable(t.name));
|
|
48
|
+
},
|
|
49
|
+
listForLLM() {
|
|
50
|
+
const available = new Set(this.list().map((t) => t.name));
|
|
51
|
+
return inner
|
|
52
|
+
.listForLLM()
|
|
53
|
+
.filter((t) => available.has((t as { function?: { name?: string } }).function?.name ?? ""));
|
|
54
|
+
},
|
|
55
|
+
getDefinition(toolName: string) {
|
|
56
|
+
return inner.getDefinition(toolName);
|
|
57
|
+
},
|
|
58
|
+
validate(toolName: string, args: object) {
|
|
59
|
+
return inner.validate(toolName, args);
|
|
60
|
+
},
|
|
61
|
+
async execute(toolName: string, args: object) {
|
|
62
|
+
if (inner.getDefinition(toolName) && !isAvailable(toolName)) {
|
|
63
|
+
const gate = availability.get(toolName);
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
error: `Tool "${toolName}" is not available right now — it requires ${gate?.requires ?? "a different studio state"}.`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return inner.execute(toolName, args);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Parent-side live expression preview (spec §19.9).
|
|
4
|
+
*
|
|
5
|
+
* Evaluates an expression node against the canvas iframe's last dataScope snapshot
|
|
6
|
+
* (tab.session.canvas.scope) on a structuredClone, so mutating operators are safe — they mutate the
|
|
7
|
+
* clone, never the live canvas. The engine's trace hook reports every sub-node's value keyed by its
|
|
8
|
+
* path within the tree; values are formatted to display strings at report time so later mutations
|
|
9
|
+
* of the clone cannot retroactively change a badge.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { evaluateExpression, isMutating } from "@jxsuite/runtime/expression";
|
|
13
|
+
import { toRaw } from "../reactivity";
|
|
14
|
+
// Imported for local use AND re-exported below so existing consumers keep their import site.
|
|
15
|
+
// oxlint-disable-next-line unicorn/prefer-export-from
|
|
16
|
+
import { formatPreviewValue } from "../utils/preview-format";
|
|
17
|
+
|
|
18
|
+
import type { ExpressionNode } from "@jxsuite/runtime/expression";
|
|
19
|
+
|
|
20
|
+
export interface ExpressionPreview {
|
|
21
|
+
/** Path-keyed display values: "" is the root node, "value/target" a nested operand, etc. */
|
|
22
|
+
values: Map<string, string>;
|
|
23
|
+
/** Evaluation error message, if the expression threw. */
|
|
24
|
+
error: string | null;
|
|
25
|
+
/** Whether the root operator mutates its target (badge renders as an effect, not a value). */
|
|
26
|
+
mutating: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The formatter is shared with the in-iframe live evaluator: it lives in utils/preview-format.ts
|
|
30
|
+
// (dependency-light, safe for the iframe bundle) and is re-exported here so existing consumers
|
|
31
|
+
// Keep their import site.
|
|
32
|
+
export { formatPreviewValue };
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Evaluate `node` against a scope snapshot for display. Returns null when no snapshot is available
|
|
36
|
+
* (canvas not yet rendered) — the editor renders no badges rather than wrong ones.
|
|
37
|
+
*/
|
|
38
|
+
export function previewExpression(
|
|
39
|
+
node: unknown,
|
|
40
|
+
scope?: Record<string, unknown> | null,
|
|
41
|
+
): ExpressionPreview | null {
|
|
42
|
+
if (!scope || !node || typeof node !== "object" || !("operator" in node)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let state: Record<string, unknown>;
|
|
47
|
+
try {
|
|
48
|
+
// Callers hand over tab.session.canvas.scope, which the reactive session tree wraps in a
|
|
49
|
+
// Proxy — structuredClone rejects proxies, so unwrap to the raw snapshot first.
|
|
50
|
+
state = structuredClone(toRaw(scope));
|
|
51
|
+
} catch {
|
|
52
|
+
// Snapshot values are JSON-safe by construction (serialize-scope), but stay defensive.
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const values = new Map<string, string>();
|
|
57
|
+
const expr = node as ExpressionNode;
|
|
58
|
+
let errorMessage: string | null = null;
|
|
59
|
+
try {
|
|
60
|
+
evaluateExpression(expr, state, null, undefined, {
|
|
61
|
+
report: (path, value) => values.set(path.join("/"), formatPreviewValue(value)),
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { error: errorMessage, mutating: isMutating(expr.operator), values };
|
|
68
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-adoption.ts — a late-bound slot for opening a freshly created project in this window.
|
|
3
|
+
*
|
|
4
|
+
* The AI `create_project` tool (services/ai-project-tools.ts) needs to run the full project-open
|
|
5
|
+
* flow (`openRecentProject` in studio.ts), but services must not import studio.ts — the entry
|
|
6
|
+
* module imports the services. studio.ts registers the adopter at boot; the tool calls
|
|
7
|
+
* `adoptProject(root)`.
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
let _adopter: ((root: string) => Promise<void>) | null = null;
|
|
13
|
+
|
|
14
|
+
/** Register the project-open flow (studio.ts boot). */
|
|
15
|
+
export function setProjectAdopter(fn: (root: string) => Promise<void>): void {
|
|
16
|
+
_adopter = fn;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Open the project at `root` in this window via the registered flow.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} root - Absolute project root.
|
|
23
|
+
*/
|
|
24
|
+
export async function adoptProject(root: string): Promise<void> {
|
|
25
|
+
if (!_adopter) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
"Project adoption is not available in this environment — open the project manually from the welcome screen.",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
await _adopter(root);
|
|
31
|
+
}
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* CSS Variables editor — form-based editor for managing design tokens. Colors
|
|
4
|
-
*
|
|
3
|
+
* CSS Variables editor — form-based editor for managing design tokens. Colors are scheme-aware
|
|
4
|
+
* (per-scheme overrides in `@--dark`-style blocks, spec §9.5); sizes/spacing are media-aware; fonts
|
|
5
|
+
* are neither.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { html, render as litRender, nothing } from "lit-html";
|
|
8
9
|
import { ref } from "lit-html/directives/ref.js";
|
|
9
10
|
import { projectState } from "../store";
|
|
10
11
|
import { getEffectiveMedia, updateSiteConfig } from "../site-context";
|
|
12
|
+
import { postSiteStyleToLiveHosts } from "../canvas/iframe-host";
|
|
13
|
+
import { isSchemeQuery, schemeOfQuery } from "../utils/canvas-media";
|
|
11
14
|
import { friendlyNameToVar, varDisplayName } from "../utils/studio-utils";
|
|
12
15
|
|
|
13
16
|
import type { JxStyle } from "@jxsuite/schema/types";
|
|
14
17
|
|
|
18
|
+
/** A declared scheme query in $media: the block key name plus the scheme it targets. */
|
|
19
|
+
export interface SchemeEntry {
|
|
20
|
+
name: string;
|
|
21
|
+
scheme: "light" | "dark";
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
/** @param {HTMLElement} container */
|
|
16
25
|
export function renderCssVarsEditor(container: HTMLElement) {
|
|
17
26
|
const config = projectState?.projectConfig || {};
|
|
@@ -50,10 +59,20 @@ export function renderCssVarsEditor(container: HTMLElement) {
|
|
|
50
59
|
}
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
const mediaNames = media
|
|
62
|
+
const mediaNames = media
|
|
63
|
+
? Object.keys(media).filter((m) => m !== "--" && !isSchemeQuery(String(media[m])))
|
|
64
|
+
: [];
|
|
65
|
+
const schemeEntries: SchemeEntry[] = media
|
|
66
|
+
? Object.entries(media).flatMap(([name, query]) => {
|
|
67
|
+
const scheme = schemeOfQuery(String(query));
|
|
68
|
+
return scheme ? [{ name, scheme }] : [];
|
|
69
|
+
})
|
|
70
|
+
: [];
|
|
54
71
|
|
|
55
72
|
const save = () => {
|
|
56
73
|
void updateSiteConfig({ style: { ...rootStyle } });
|
|
74
|
+
// Live canvas feedback: replace the site-style sheet in place — no re-render needed.
|
|
75
|
+
postSiteStyleToLiveHosts();
|
|
57
76
|
};
|
|
58
77
|
|
|
59
78
|
const updateVar = (name: string, val: string) => {
|
|
@@ -61,6 +80,35 @@ export function renderCssVarsEditor(container: HTMLElement) {
|
|
|
61
80
|
save();
|
|
62
81
|
};
|
|
63
82
|
|
|
83
|
+
// Write/clear a per-scheme token override in the scheme's `@--name` block (spec §9.5). An empty
|
|
84
|
+
// Value clears the override (and drops the block when it empties) — the token inherits the base.
|
|
85
|
+
const updateSchemeVar = (schemeName: string, varName: string, val: string) => {
|
|
86
|
+
const key = `@${schemeName}`;
|
|
87
|
+
if (val) {
|
|
88
|
+
const block = (rootStyle[key] ??= {}) as Record<string, unknown>;
|
|
89
|
+
block[varName] = val;
|
|
90
|
+
} else {
|
|
91
|
+
const block = rootStyle[key];
|
|
92
|
+
if (block && typeof block === "object") {
|
|
93
|
+
delete (block as Record<string, unknown>)[varName];
|
|
94
|
+
if (Object.keys(block).length === 0) {
|
|
95
|
+
delete rootStyle[key];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
save();
|
|
100
|
+
renderCssVarsEditor(container);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Without a declared scheme query nothing ever shows the scheme UI (here or in the tab bar) —
|
|
104
|
+
// This is the opt-in affordance for existing projects. Re-render after the async config update
|
|
105
|
+
// Lands (unlike style edits, $media is not mutated in place).
|
|
106
|
+
const enableDarkScheme = () => {
|
|
107
|
+
void updateSiteConfig({
|
|
108
|
+
$media: { ...config.$media, "--dark": "(prefers-color-scheme: dark)" },
|
|
109
|
+
}).then(() => renderCssVarsEditor(container));
|
|
110
|
+
};
|
|
111
|
+
|
|
64
112
|
const deleteVar = (name: string) => {
|
|
65
113
|
delete rootStyle[name];
|
|
66
114
|
save();
|
|
@@ -81,7 +129,16 @@ export function renderCssVarsEditor(container: HTMLElement) {
|
|
|
81
129
|
<div class="settings-section">
|
|
82
130
|
<h3 class="settings-section-title">CSS Variables</h3>
|
|
83
131
|
|
|
84
|
-
${renderColorSection(
|
|
132
|
+
${renderColorSection(
|
|
133
|
+
groups.color,
|
|
134
|
+
updateVar,
|
|
135
|
+
deleteVar,
|
|
136
|
+
addVar,
|
|
137
|
+
rootStyle,
|
|
138
|
+
schemeEntries,
|
|
139
|
+
updateSchemeVar,
|
|
140
|
+
enableDarkScheme,
|
|
141
|
+
)}
|
|
85
142
|
${renderFontSection(groups.font, updateVar, deleteVar, addVar)}
|
|
86
143
|
${renderSizeSection(groups.size, updateVar, deleteVar, addVar, rootStyle, mediaNames)}
|
|
87
144
|
${groups.other.length > 0
|
|
@@ -104,6 +161,10 @@ function renderColorSection(
|
|
|
104
161
|
updateVar: (name: string, val: string) => void,
|
|
105
162
|
deleteVar: (name: string) => void,
|
|
106
163
|
addVar: (prefix: string, friendlyName: string, val: string) => void,
|
|
164
|
+
rootStyle: JxStyle,
|
|
165
|
+
schemeEntries: SchemeEntry[],
|
|
166
|
+
updateSchemeVar: (schemeName: string, varName: string, val: string) => void,
|
|
167
|
+
enableDarkScheme: () => void,
|
|
107
168
|
) {
|
|
108
169
|
return html`
|
|
109
170
|
<div class="css-vars-group">
|
|
@@ -129,9 +190,66 @@ function renderColorSection(
|
|
|
129
190
|
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
130
191
|
</sp-action-button>
|
|
131
192
|
</div>
|
|
193
|
+
${schemeEntries.map((entry) =>
|
|
194
|
+
renderSchemeOverride(name, rootStyle, entry, updateSchemeVar),
|
|
195
|
+
)}
|
|
132
196
|
`,
|
|
133
197
|
)}
|
|
134
198
|
${renderAddRow("--color-", "Primary Blue", "#3b82f6", addVar)}
|
|
199
|
+
${schemeEntries.length === 0
|
|
200
|
+
? html`
|
|
201
|
+
<sp-action-button
|
|
202
|
+
size="s"
|
|
203
|
+
class="css-vars-enable-dark"
|
|
204
|
+
title="Add a dark color scheme: declares --dark in $media so every color token can carry a dark value"
|
|
205
|
+
@click=${enableDarkScheme}
|
|
206
|
+
>
|
|
207
|
+
Enable dark scheme
|
|
208
|
+
</sp-action-button>
|
|
209
|
+
`
|
|
210
|
+
: nothing}
|
|
211
|
+
</div>
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* One per-scheme override row under a color token: swatch + field bound to the scheme block's
|
|
217
|
+
* value. Empty shows "inherits" (the base value applies); clearing deletes the override.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} varName
|
|
220
|
+
* @param {JxStyle} rootStyle
|
|
221
|
+
* @param {SchemeEntry} entry
|
|
222
|
+
* @param {(schemeName: string, varName: string, val: string) => void} updateSchemeVar
|
|
223
|
+
*/
|
|
224
|
+
function renderSchemeOverride(
|
|
225
|
+
varName: string,
|
|
226
|
+
rootStyle: JxStyle,
|
|
227
|
+
entry: SchemeEntry,
|
|
228
|
+
updateSchemeVar: (schemeName: string, varName: string, val: string) => void,
|
|
229
|
+
) {
|
|
230
|
+
const block = rootStyle[`@${entry.name}`];
|
|
231
|
+
const current =
|
|
232
|
+
block && typeof block === "object" ? (block as Record<string, unknown>)[varName] : undefined;
|
|
233
|
+
const label = entry.scheme === "dark" ? "Dark" : "Light";
|
|
234
|
+
return html`
|
|
235
|
+
<div class="css-var-media-row css-var-scheme-row">
|
|
236
|
+
<span class="css-var-media-label">${label}</span>
|
|
237
|
+
<div class="css-var-swatch" style="background:${current ?? "transparent"}">
|
|
238
|
+
<input
|
|
239
|
+
type="color"
|
|
240
|
+
.value=${current && String(current).startsWith("#") ? String(current) : "#111111"}
|
|
241
|
+
@input=${(e: Event) =>
|
|
242
|
+
updateSchemeVar(entry.name, varName, (e.target as HTMLInputElement).value)}
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
<sp-textfield
|
|
246
|
+
size="s"
|
|
247
|
+
placeholder="inherits"
|
|
248
|
+
.value=${current == null ? "" : String(current)}
|
|
249
|
+
@change=${(e: Event) =>
|
|
250
|
+
updateSchemeVar(entry.name, varName, (e.target as HTMLInputElement).value)}
|
|
251
|
+
style="flex:1;max-width:160px"
|
|
252
|
+
></sp-textfield>
|
|
135
253
|
</div>
|
|
136
254
|
`;
|
|
137
255
|
}
|
package/src/site-context.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { projectState, requireProjectState, setProjectState } from "./store";
|
|
10
10
|
import { getPlatform } from "./platform";
|
|
11
|
+
import { setWorkspaceProject, workspace } from "./workspace/workspace";
|
|
11
12
|
|
|
12
13
|
import type {
|
|
13
14
|
JxElement,
|
|
@@ -272,6 +273,7 @@ export async function updateSiteConfig(patch: Partial<ProjectConfig>) {
|
|
|
272
273
|
} as ProjectConfig;
|
|
273
274
|
await platform.writeFile("project.json", JSON.stringify(config, null, 2));
|
|
274
275
|
setProjectState({ ...requireProjectState(), projectConfig: config });
|
|
276
|
+
setWorkspaceProject(workspace.projectRoot, config);
|
|
275
277
|
if ("extensions" in patch) {
|
|
276
278
|
const { loadFormats, refreshExtensionUi, refreshFormats } =
|
|
277
279
|
await import("./format/format-host");
|
package/src/store.ts
CHANGED
|
@@ -37,16 +37,20 @@ export let canvasWrap = null as unknown as HTMLElement;
|
|
|
37
37
|
export let activityBar = null as unknown as HTMLElement;
|
|
38
38
|
export let leftPanel = null as unknown as HTMLElement;
|
|
39
39
|
export let rightPanel = null as unknown as HTMLElement;
|
|
40
|
+
export let chatPanelEl = null as unknown as HTMLElement;
|
|
40
41
|
export let toolbarEl = null as unknown as HTMLElement;
|
|
41
42
|
export let statusbarEl = null as unknown as HTMLElement;
|
|
43
|
+
export let frontmatterPanelEl = null as unknown as HTMLElement;
|
|
42
44
|
|
|
43
45
|
export function initShellRefs() {
|
|
44
46
|
canvasWrap = document.querySelector("#canvas-wrap") as HTMLElement;
|
|
45
47
|
activityBar = document.querySelector("#activity-bar") as HTMLElement;
|
|
46
48
|
leftPanel = document.querySelector("#left-panel") as HTMLElement;
|
|
47
49
|
rightPanel = document.querySelector("#right-panel") as HTMLElement;
|
|
50
|
+
chatPanelEl = document.querySelector("#chat-panel") as HTMLElement;
|
|
48
51
|
toolbarEl = document.querySelector("#toolbar") as HTMLElement;
|
|
49
52
|
statusbarEl = document.querySelector("#statusbar") as HTMLElement;
|
|
53
|
+
frontmatterPanelEl = document.querySelector("#frontmatter-panel") as HTMLElement;
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
// ─── Shared containers (mutated in place by owner modules) ───────────────────
|