@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.
Files changed (89) hide show
  1. package/dist/iframe-entry.js +760 -65
  2. package/dist/iframe-entry.js.map +16 -12
  3. package/dist/studio.css +1333 -0
  4. package/dist/studio.js +71573 -50074
  5. package/dist/studio.js.map +123 -88
  6. package/package.json +11 -9
  7. package/src/canvas/canvas-live-render.ts +21 -2
  8. package/src/canvas/canvas-render.ts +35 -16
  9. package/src/canvas/canvas-utils.ts +118 -72
  10. package/src/canvas/iframe-entry.ts +37 -1
  11. package/src/canvas/iframe-eval.ts +155 -0
  12. package/src/canvas/iframe-host.ts +193 -6
  13. package/src/canvas/iframe-inline-edit.ts +107 -1
  14. package/src/canvas/iframe-protocol.ts +56 -3
  15. package/src/canvas/iframe-render.ts +53 -14
  16. package/src/canvas/site-style-css.ts +121 -0
  17. package/src/collab/collab-session.ts +23 -8
  18. package/src/collab/collab-state.ts +6 -3
  19. package/src/component-props.ts +104 -0
  20. package/src/editor/inline-edit-apply.ts +36 -1
  21. package/src/editor/inline-edit.ts +58 -1
  22. package/src/editor/shortcuts.ts +41 -12
  23. package/src/files/file-ops.ts +14 -0
  24. package/src/files/files.ts +53 -1
  25. package/src/grid/cell-editors.ts +288 -0
  26. package/src/grid/cell-popovers.ts +108 -0
  27. package/src/grid/csv-codec.ts +122 -0
  28. package/src/grid/edit-buffer.ts +490 -0
  29. package/src/grid/grid-controller.ts +417 -0
  30. package/src/grid/grid-layout.ts +83 -0
  31. package/src/grid/grid-open.ts +167 -0
  32. package/src/grid/grid-panel.ts +302 -0
  33. package/src/grid/grid-source.ts +210 -0
  34. package/src/grid/grid-view.ts +367 -0
  35. package/src/grid/schema-columns.ts +261 -0
  36. package/src/grid/sources/connector-source.ts +217 -0
  37. package/src/grid/sources/content-source.ts +542 -0
  38. package/src/grid/sources/csv-file-source.ts +247 -0
  39. package/src/grid/tabulator-tables.d.ts +120 -0
  40. package/src/panels/block-action-bar.ts +8 -3
  41. package/src/panels/chat-panel.ts +98 -0
  42. package/src/panels/data-grid.ts +9 -387
  43. package/src/panels/editors.ts +43 -17
  44. package/src/panels/events-panel.ts +137 -44
  45. package/src/panels/formula-workspace.ts +379 -0
  46. package/src/panels/frontmatter-fields.ts +231 -0
  47. package/src/panels/frontmatter-panel.ts +132 -0
  48. package/src/panels/git-panel.ts +1 -1
  49. package/src/panels/head-panel.ts +11 -175
  50. package/src/panels/properties-panel.ts +154 -144
  51. package/src/panels/right-panel.ts +9 -47
  52. package/src/panels/signals-panel.ts +115 -23
  53. package/src/panels/statement-editor.ts +710 -0
  54. package/src/panels/style-panel.ts +76 -22
  55. package/src/panels/stylebook-panel.ts +0 -2
  56. package/src/panels/tab-bar.ts +214 -10
  57. package/src/panels/tab-strip.ts +62 -6
  58. package/src/panels/toolbar.ts +37 -3
  59. package/src/services/ai-project-tools.ts +541 -0
  60. package/src/services/ai-session-store.ts +28 -0
  61. package/src/services/ai-system-prompt.ts +194 -24
  62. package/src/services/ai-tools.ts +88 -21
  63. package/src/services/automation.ts +16 -0
  64. package/src/services/document-assistant.ts +81 -11
  65. package/src/services/gated-registry.ts +72 -0
  66. package/src/services/live-preview.ts +0 -0
  67. package/src/services/preview-eval.ts +68 -0
  68. package/src/services/project-adoption.ts +31 -0
  69. package/src/settings/css-vars-editor.ts +122 -4
  70. package/src/site-context.ts +2 -0
  71. package/src/store.ts +4 -0
  72. package/src/studio.ts +90 -52
  73. package/src/tabs/patch-ops.ts +25 -0
  74. package/src/tabs/tab.ts +46 -1
  75. package/src/tabs/transact.ts +60 -13
  76. package/src/types.ts +12 -0
  77. package/src/ui/dynamic-slot.ts +272 -0
  78. package/src/ui/expression-editor.ts +423 -125
  79. package/src/ui/field-row.ts +5 -0
  80. package/src/ui/formula-catalog.ts +557 -0
  81. package/src/ui/formula-chips.ts +216 -0
  82. package/src/ui/formula-palette.ts +211 -0
  83. package/src/ui/layers.ts +40 -0
  84. package/src/ui/media-picker.ts +1 -1
  85. package/src/ui/panel-resize.ts +15 -1
  86. package/src/utils/canvas-media.ts +23 -0
  87. package/src/utils/preview-format.ts +26 -0
  88. package/src/view.ts +4 -4
  89. 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
- const toolRegistry = createToolRegistry();
55
- registerAiTools(toolRegistry, {
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
- projectStyle: (workspace.projectConfig as ProjectConfig | null)?.style as
66
- | Record<string, string>
67
- | undefined,
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
- let controller: AbortController | null = null;
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
- /** The persisted session backing the live chat; null = fresh unsaved chat. */
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 and fonts are NOT
4
- * media-aware; sizes/spacing are media-aware.
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 ? Object.keys(media).filter((m) => m !== "--") : [];
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(groups.color, updateVar, deleteVar, addVar)}
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
  }
@@ -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) ───────────────────