@jxsuite/studio 0.37.0 → 1.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.
Files changed (38) hide show
  1. package/dist/studio.js +77654 -76269
  2. package/dist/studio.js.map +128 -119
  3. package/package.json +44 -44
  4. package/src/account-status.ts +39 -0
  5. package/src/browse/browse.ts +10 -14
  6. package/src/canvas/iframe-host.ts +1 -1
  7. package/src/editor/context-menu.ts +1 -1
  8. package/src/editor/repeater-scope.ts +8 -13
  9. package/src/files/files.ts +3 -0
  10. package/src/format/format-host.ts +63 -1
  11. package/src/new-project/add-repo-modal.ts +183 -0
  12. package/src/new-project/new-project-modal.ts +22 -3
  13. package/src/page-params.ts +34 -8
  14. package/src/panels/ai-chat/chat-markdown.ts +2 -2
  15. package/src/panels/ai-panel.ts +61 -4
  16. package/src/panels/data-grid.ts +619 -0
  17. package/src/panels/signals-panel.ts +102 -437
  18. package/src/panels/statusbar.ts +1 -1
  19. package/src/panels/welcome-screen.ts +50 -0
  20. package/src/platform-errors.ts +30 -0
  21. package/src/platforms/cloud.ts +172 -89
  22. package/src/platforms/devserver.ts +172 -0
  23. package/src/services/context-resolver.ts +73 -0
  24. package/src/services/data-service.ts +155 -0
  25. package/src/services/monaco-setup.ts +75 -26
  26. package/src/settings/contributed-section.ts +406 -0
  27. package/src/settings/extension-sections.ts +145 -0
  28. package/src/settings/schema-field-ui.ts +4 -2
  29. package/src/settings/settings-modal.ts +101 -42
  30. package/src/site-context.ts +10 -1
  31. package/src/studio.ts +24 -0
  32. package/src/tabs/transact.ts +1 -1
  33. package/src/types.ts +120 -1
  34. package/src/ui/form-controls.ts +322 -0
  35. package/src/ui/progress-modal.ts +2 -2
  36. package/src/ui/schema-form.ts +524 -0
  37. package/src/utils/studio-utils.ts +4 -3
  38. package/src/settings/content-types-editor.ts +0 -599
@@ -2,41 +2,129 @@
2
2
  /**
3
3
  * Settings modal — site-wide project settings (CSS variables, definitions, content types, head,
4
4
  * general). Modeled after VS Code / Obsidian settings panels: left sidebar nav + right content
5
- * area.
5
+ * area. Sections come from a registry: built-ins register at module init, and extensions add
6
+ * descriptor-contributed sections through `registerSettingsSection`.
6
7
  */
7
8
 
8
9
  import { html } from "lit-html";
9
10
  import { classMap } from "lit-html/directives/class-map.js";
10
11
  import { ref } from "lit-html/directives/ref.js";
11
12
  import { renderDefsEditor } from "./defs-editor";
12
- import { renderContentTypesEditor } from "./content-types-editor";
13
13
  import { renderCssVarsEditor } from "./css-vars-editor";
14
14
  import { renderHeadEditor } from "./head-editor";
15
15
  import { renderGeneralSettings } from "./general-settings";
16
16
  import { renderDependenciesEditor } from "./dependencies-editor";
17
17
  import { openModal } from "../ui/layers";
18
18
 
19
+ // ─── Section registry ─────────────────────────────────────────────────────────
20
+
21
+ /** A settings-modal section: nav entry plus a renderer for the content area. */
22
+ export interface SettingsSection {
23
+ key: string;
24
+ label: string;
25
+ /** Nav icon name (reserved for future nav treatments). */
26
+ icon?: string | undefined;
27
+ /** Sort position — lower orders render higher in the nav. */
28
+ order: number;
29
+ render: (container: HTMLElement) => void;
30
+ }
31
+
32
+ const sectionRegistry = new Map<string, SettingsSection>();
33
+
34
+ /**
35
+ * Register (or replace) a settings section. Extensions use this hook to contribute
36
+ * descriptor-driven sections; built-ins register below at module init.
37
+ *
38
+ * @param {SettingsSection} section
39
+ */
40
+ export function registerSettingsSection(section: SettingsSection): void {
41
+ sectionRegistry.set(section.key, section);
42
+ }
43
+
44
+ /**
45
+ * Remove a registered section — used when a descriptor-contributed section's extension is disabled
46
+ * (see ./extension-sections). Built-ins are never unregistered.
47
+ *
48
+ * @param {string} key
49
+ */
50
+ export function unregisterSettingsSection(key: string): void {
51
+ sectionRegistry.delete(key);
52
+ if (_activeSection === key) {
53
+ _activeSection = "general";
54
+ }
55
+ }
56
+
57
+ /** Registered sections sorted by order (registration order breaks ties). */
58
+ function sortedSections(): SettingsSection[] {
59
+ return [...sectionRegistry.values()].toSorted((a, b) => a.order - b.order);
60
+ }
61
+
62
+ // Built-in sections — orders preserve the historical display order
63
+ registerSettingsSection({
64
+ icon: "sp-icon-properties",
65
+ key: "general",
66
+ label: "General",
67
+ order: 10,
68
+ render: renderGeneralSettings,
69
+ });
70
+ registerSettingsSection({
71
+ icon: "sp-icon-file-single-web-page",
72
+ key: "head",
73
+ label: "Head",
74
+ order: 20,
75
+ render: renderHeadEditor,
76
+ });
77
+ registerSettingsSection({
78
+ icon: "sp-icon-brush",
79
+ key: "cssVars",
80
+ label: "CSS Variables",
81
+ order: 30,
82
+ render: renderCssVarsEditor,
83
+ });
84
+ registerSettingsSection({
85
+ icon: "sp-icon-data",
86
+ key: "definitions",
87
+ label: "Definitions",
88
+ order: 40,
89
+ render: renderDefsEditor,
90
+ });
91
+ // Content Types is no longer a built-in: @jxsuite/parser contributes it (order 50) through its
92
+ // Content class descriptor's $studio.settings block, registered via ./extension-sections.
93
+ registerSettingsSection({
94
+ icon: "sp-icon-box",
95
+ key: "dependencies",
96
+ label: "Dependencies",
97
+ order: 60,
98
+ render: renderDependenciesEditor,
99
+ });
100
+
101
+ // ─── Modal state ──────────────────────────────────────────────────────────────
102
+
19
103
  let _handle: ReturnType<typeof openModal> | null = null;
20
104
 
21
105
  let _activeSection = "general";
22
106
 
23
107
  let _contentEl: HTMLElement | null = null;
24
108
 
25
- const sections = [
26
- { icon: "sp-icon-properties", key: "general", label: "General" },
27
- { icon: "sp-icon-file-single-web-page", key: "head", label: "Head" },
28
- { icon: "sp-icon-brush", key: "cssVars", label: "CSS Variables" },
29
- { icon: "sp-icon-data", key: "definitions", label: "Definitions" },
30
- { icon: "sp-icon-view-grid", key: "contentTypes", label: "Content Types" },
31
- { icon: "sp-icon-box", key: "dependencies", label: "Dependencies" },
32
- ];
33
-
34
109
  export function openSettingsModal() {
35
110
  if (_handle) {
36
111
  return;
37
112
  }
38
113
  _activeSection = "general";
39
114
  renderModal();
115
+ // Refresh descriptor-contributed sections (cached payloads make this cheap) and rerender the
116
+ // Nav once they land. Lazy import breaks the settings-modal ↔ extension-sections module cycle.
117
+ void import("./extension-sections")
118
+ .then(async ({ syncExtensionSettingsSections }) => {
119
+ await syncExtensionSettingsSections();
120
+ if (_handle) {
121
+ renderModal();
122
+ renderActiveSection();
123
+ }
124
+ })
125
+ .catch(() => {
126
+ // Contributed sections are optional — the built-ins render regardless.
127
+ });
40
128
  }
41
129
 
42
130
  export function closeSettingsModal() {
@@ -73,7 +161,7 @@ function renderModal() {
73
161
  </div>
74
162
  <div class="settings-modal-body">
75
163
  <nav class="settings-modal-nav">
76
- ${sections.map(
164
+ ${sortedSections().map(
77
165
  (s) => html`
78
166
  <button
79
167
  class=${classMap({
@@ -111,34 +199,5 @@ function renderActiveSection() {
111
199
  if (!_handle || !_contentEl) {
112
200
  return;
113
201
  }
114
-
115
- switch (_activeSection) {
116
- case "general": {
117
- renderGeneralSettings(_contentEl);
118
- break;
119
- }
120
- case "head": {
121
- renderHeadEditor(_contentEl);
122
- break;
123
- }
124
- case "cssVars": {
125
- renderCssVarsEditor(_contentEl);
126
- break;
127
- }
128
- case "definitions": {
129
- renderDefsEditor(_contentEl);
130
- break;
131
- }
132
- case "contentTypes": {
133
- renderContentTypesEditor(_contentEl);
134
- break;
135
- }
136
- case "dependencies": {
137
- renderDependenciesEditor(_contentEl);
138
- break;
139
- }
140
- default: {
141
- break;
142
- }
143
- }
202
+ sectionRegistry.get(_activeSection)?.render(_contentEl);
144
203
  }
@@ -258,7 +258,9 @@ function fillSlots(
258
258
  }
259
259
 
260
260
  /**
261
- * Update the project's project.json with a partial patch and persist to disk.
261
+ * Update the project's project.json with a partial patch and persist to disk. A patch touching
262
+ * `extensions` invalidates the format/extensions caches and refreshes the editor's per-project
263
+ * schemas plus the contributed settings sections — the enabled-extension surface just changed.
262
264
  *
263
265
  * @param {Partial<ProjectConfig>} patch - Fields to merge into the current projectConfig
264
266
  */
@@ -270,4 +272,11 @@ export async function updateSiteConfig(patch: Partial<ProjectConfig>) {
270
272
  } as ProjectConfig;
271
273
  await platform.writeFile("project.json", JSON.stringify(config, null, 2));
272
274
  setProjectState({ ...requireProjectState(), projectConfig: config });
275
+ if ("extensions" in patch) {
276
+ const { loadFormats, refreshExtensionUi, refreshFormats } =
277
+ await import("./format/format-host");
278
+ refreshFormats();
279
+ void loadFormats();
280
+ refreshExtensionUi(platform);
281
+ }
273
282
  }
package/src/studio.ts CHANGED
@@ -63,6 +63,7 @@ import {
63
63
  documentExtensions,
64
64
  formatForPath,
65
65
  loadFormats,
66
+ refreshExtensionUi,
66
67
  refreshFormats,
67
68
  } from "./format/format-host";
68
69
  import {
@@ -110,6 +111,8 @@ import { cloneRepository, renderGitPanel } from "./panels/git-panel";
110
111
  // By Bun's bundler despite sideEffects declarations in Spectrum's package.json.
111
112
  import { components as _swc } from "./ui/spectrum";
112
113
  import "./ui/panel-resize.js";
114
+ // Built-in schema-form controls (schema-builder, secret) register on import
115
+ import "./ui/form-controls.js";
113
116
  import { initLayers } from "./ui/layers";
114
117
  import { initShortcuts } from "./editor/shortcuts";
115
118
  import { renderActivityBar, mount as mountActivityBar } from "./panels/activity-bar";
@@ -131,10 +134,12 @@ import {
131
134
  } from "./panels/block-action-bar";
132
135
  import { initCssData } from "./panels/style-utils";
133
136
  import { initQuickSearch } from "./panels/quick-search";
137
+ import { hydrateAccountStatus } from "./account-status";
134
138
  import { hydrateProjectList } from "./project-list";
135
139
  import { addRecentProject, hydrateRecentProjects, removeRecentProject } from "./recent-projects";
136
140
  import { hydrateSettings } from "./services/settings-store";
137
141
  import { initWelcome } from "./panels/welcome-screen";
142
+ import { openAddRepoModal } from "./new-project/add-repo-modal";
138
143
  import { openNewProjectModal } from "./new-project/new-project-modal";
139
144
  import type { DocumentStackEntry, GitDiffState } from "./types";
140
145
  import type { JxPath } from "./state";
@@ -471,6 +476,16 @@ initCanvasRender({
471
476
  });
472
477
 
473
478
  initWelcome({
479
+ addExistingRepo: async () => {
480
+ const result = await openAddRepoModal();
481
+ if (result) {
482
+ // The catalogue gained an entry; refresh it before navigating into the project.
483
+ void hydrateProjectList().then(() => {
484
+ render();
485
+ });
486
+ void openRecentProject(result.root);
487
+ }
488
+ },
474
489
  cloneRepository: () => cloneRepository({ openRecentProject }),
475
490
  openNewProject: async () => {
476
491
  const result = await openNewProjectModal();
@@ -653,6 +668,7 @@ if (_projectParam) {
653
668
  selectedPath: siteCtx.fileRelPath || null,
654
669
  });
655
670
 
671
+ refreshExtensionUi(platform);
656
672
  await autoSyncProjectOnOpen();
657
673
  await ensureDependenciesInstalled();
658
674
  await loadComponentRegistry();
@@ -769,6 +785,13 @@ void hydrateProjectList().then(() => {
769
785
  render();
770
786
  });
771
787
 
788
+ // Hydrate the account onboarding status (GitHub-App installation coverage on cloud), then refresh
789
+ // The welcome screen's install prompt. No-op on platforms without getAccountStatus.
790
+ // oxlint-disable-next-line unicorn/prefer-top-level-await -- deliberate fire-and-forget: hydration must not block initial render
791
+ void hydrateAccountStatus().then(() => {
792
+ render();
793
+ });
794
+
772
795
  // Hydrate user settings (AI connection parameters) from the backend store, then re-render so
773
796
  // Key-gated surfaces (assistant gate, New Project Import/Agent tabs) see the stored key.
774
797
  // oxlint-disable-next-line unicorn/prefer-top-level-await -- deliberate fire-and-forget: hydration must not block initial render
@@ -819,6 +842,7 @@ async function openRecentProject(root: string) {
819
842
  // "No format class imported" until a reload. Mirrors openProject in files.ts.
820
843
  refreshFormats();
821
844
  void loadFormats();
845
+ refreshExtensionUi(platform);
822
846
  const content = await platform.readFile("project.json");
823
847
  const config = JSON.parse(content) as ProjectConfig;
824
848
 
@@ -343,7 +343,7 @@ function applyDocOp(tab: Tab, op: JxDocOp) {
343
343
  toIndex: op.toIndex,
344
344
  toParentPath: op.toParentPath,
345
345
  });
346
- return;
346
+ break;
347
347
  }
348
348
  default: {
349
349
  break;
package/src/types.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  /// <reference lib="dom" />
2
2
  import type { CollabHandle } from "@jxsuite/collab/provider";
3
- import type { JxMutableNode, JxPath, ProjectConfig } from "@jxsuite/schema/types";
3
+ import type {
4
+ ContentTypeSchema,
5
+ JxMutableNode,
6
+ JxPath,
7
+ ProjectConfig,
8
+ } from "@jxsuite/schema/types";
4
9
 
5
10
  // ─── Wire types (the Studio Backend Protocol) ───────────────────────────────
6
11
  /* The request/response shapes every backend serves live in @jxsuite/protocol;
@@ -11,7 +16,16 @@ import type {
11
16
  CfConnection,
12
17
  CodeServiceResult,
13
18
  ComponentMeta,
19
+ DataConnectionsResponse,
20
+ DataConnectionTestResult,
21
+ DataPushResult,
22
+ DataRowDelete,
23
+ DataRowInsert,
24
+ DataRowsQuery,
25
+ DataRowsResult,
26
+ DataRowUpdate,
14
27
  DirEntry,
28
+ ExtensionsInfo,
15
29
  FsEvent,
16
30
  GitBranchesResult,
17
31
  GitLogEntry,
@@ -22,8 +36,11 @@ import type {
22
36
  PackageInfo,
23
37
  PackageOpResult,
24
38
  ProjectListEntry,
39
+ ProjectSchemasResponse,
25
40
  RecentProjectEntry,
26
41
  RenameResult,
42
+ SecretsSetRequest,
43
+ SecretsSetResponse,
27
44
  StarterInfo,
28
45
  } from "@jxsuite/protocol";
29
46
 
@@ -35,8 +52,24 @@ export type {
35
52
  CodeServiceResult,
36
53
  ComponentMeta,
37
54
  ComponentSlotMeta,
55
+ DataColumnMeta,
56
+ DataConnectionInfo,
57
+ DataConnectionsResponse,
58
+ DataConnectionTestResult,
59
+ DataConnectorInfo,
60
+ DataPushRequest,
61
+ DataPushResult,
62
+ DataPushStep,
63
+ DataRowDelete,
64
+ DataRowInsert,
65
+ DataRowsQuery,
66
+ DataRowsResult,
67
+ DataRowUpdate,
38
68
  DirEntry,
39
69
  ErrorBody,
70
+ ExtensionContributionInfo,
71
+ ExtensionProjectBlock,
72
+ ExtensionsInfo,
40
73
  FsEvent,
41
74
  GitBranchesResult,
42
75
  GitFileStatus,
@@ -49,12 +82,36 @@ export type {
49
82
  PackageInfo,
50
83
  PackageOpResult,
51
84
  ProjectListEntry,
85
+ ProjectSchemasResponse,
52
86
  PullRequestInfo,
53
87
  RecentProjectEntry,
54
88
  RenameResult,
89
+ SecretsListResponse,
90
+ SecretsSetRequest,
91
+ SecretsSetResponse,
55
92
  StarterInfo,
56
93
  } from "@jxsuite/protocol";
57
94
 
95
+ /** Repository-access onboarding state returned by `StudioPlatform.getAccountStatus`. */
96
+ export interface AccountStatus {
97
+ /** GitHub App installations (personal + organization) visible to the signed-in user. */
98
+ installations: { id: number; account: string | null }[];
99
+ /** Where to install the App (github.com/apps/<slug>/installations/new), when known. */
100
+ appInstallUrl?: string;
101
+ }
102
+
103
+ /** A repository visible to `StudioPlatform.listRepos` (the add-existing-repository picker). */
104
+ export interface RepoInfo {
105
+ owner: string;
106
+ name: string;
107
+ fullName: string;
108
+ private: boolean;
109
+ defaultBranch: string;
110
+ permission: "admin" | "write" | "read" | "none";
111
+ /** Already recognized as a Jx project (topic-tagged / cataloged). */
112
+ isJxProject: boolean;
113
+ }
114
+
58
115
  export interface StudioPlatform {
59
116
  id: string;
60
117
  projectRoot: string;
@@ -126,8 +183,39 @@ export interface StudioPlatform {
126
183
  searchFiles: (query: string, extensions?: string[]) => Promise<DirEntry[]>;
127
184
  /** List the project's registered format classes (auto-discovered from imports). */
128
185
  listFormats?: () => Promise<unknown[]>;
186
+ /**
187
+ * List the project's enabled extension packages with their project-section contributions
188
+ * (specs/extensions.md §9/§9.1) — the formats route's sibling `extensions` payload. Optional:
189
+ * platforms without it lose descriptor-contributed settings sections.
190
+ */
191
+ listExtensions?: () => Promise<ExtensionsInfo[]>;
192
+ /**
193
+ * Fetch the project's generated entry schemas (project.schema.json / document.schema.json),
194
+ * PRE-BUNDLED into self-contained documents for editor registration. Optional: without it the
195
+ * JSON editor keeps the bundled core schemas.
196
+ */
197
+ fetchProjectSchemas?: () => Promise<ProjectSchemasResponse>;
129
198
  /** Invoke a format capability (parse/serialize) — { format, action, source?, doc?, options? }. */
130
199
  formatAction?: (payload: Record<string, unknown>) => Promise<unknown>;
200
+ // ─── Data surface + secrets (owner console; specs/extensions.md §13) ────────
201
+ // Optional as a family: backends without the connector data routes omit them all, and Studio
202
+ // Hides the data grid and connection/push/test actions. Row CRUD is the ADMIN path — it
203
+ // Intentionally bypasses table permission rules; the backend boundary is the gate.
204
+ /** List connector connections with configured state, table names, and provider metadata. */
205
+ dataConnections?: () => Promise<DataConnectionsResponse>;
206
+ /** Probe one connection through the backend's connector registry. */
207
+ dataConnectionTest?: (connection: string) => Promise<DataConnectionTestResult>;
208
+ /** Additive schema push; `dryRun` compiles the plan without applying it. */
209
+ dataPush?: (opts?: { connection?: string; dryRun?: boolean }) => Promise<DataPushResult>;
210
+ /** Page a table's rows with introspected column metadata. */
211
+ dataRows?: (query: DataRowsQuery) => Promise<DataRowsResult>;
212
+ dataInsertRow?: (req: DataRowInsert) => Promise<{ row: Record<string, unknown> }>;
213
+ dataUpdateRow?: (req: DataRowUpdate) => Promise<{ row: Record<string, unknown> }>;
214
+ dataDeleteRow?: (req: DataRowDelete) => Promise<{ ok: boolean }>;
215
+ /** Configured secret env-var NAMES — never values. */
216
+ listSecrets?: () => Promise<string[]>;
217
+ /** Write/remove secrets in the backend store (.dev.vars locally); names-only response. */
218
+ setSecrets?: (req: SecretsSetRequest) => Promise<SecretsSetResponse>;
131
219
  fetchPluginSchema: (src: string, prototype?: string, base?: string) => Promise<unknown>;
132
220
  /**
133
221
  * Resolve a class-prototype config through the backend's `/__jx_resolve__` pipeline (with the
@@ -235,6 +323,25 @@ export interface StudioPlatform {
235
323
  // ─── Identity & hosting connections (publish surface) ───────────────────────
236
324
  /** The signed-in user's identity, when the platform has one (cloud). */
237
325
  getUser?: () => Promise<{ login: string; name?: string; avatarUrl?: string } | null>;
326
+ /**
327
+ * Repository-access onboarding state: the platform's GitHub App installations visible to this
328
+ * user and where to install the App. Cloud-only; when the list is empty Studio's welcome screen
329
+ * prompts the user to install the App (without it, no repos are reachable). Null = unknown (don't
330
+ * prompt).
331
+ */
332
+ getAccountStatus?: () => Promise<AccountStatus | null>;
333
+ /**
334
+ * Browse every repository the platform's account link can reach — personal and organization repos
335
+ * covered by a GitHub App installation on cloud. Backs the "Add Existing Repository" picker;
336
+ * local platforms omit it (the OS picker / clone flow covers them).
337
+ */
338
+ listRepos?: () => Promise<RepoInfo[]>;
339
+ /**
340
+ * Adopt an existing repository as a Jx project and return its catalogue root key (openable via
341
+ * the recent-projects path). Rejects with a structured message when the repository carries no
342
+ * project.json.
343
+ */
344
+ importProject?: (opts: { owner: string; name: string }) => Promise<{ root: string }>;
238
345
  /**
239
346
  * Open a pull request for the current branch. Cloud platforms implement it against their session;
240
347
  * local platforms omit it and Studio falls back to a direct GitHub API call with the user's
@@ -273,6 +380,18 @@ export interface StudioPlatform {
273
380
 
274
381
  // ─── Studio Types ───────────────────────────────────────────────────────────
275
382
 
383
+ /**
384
+ * A project.json `content` section entry, as the studio consumes it. The parser extension owns the
385
+ * full shape (its project fragment schema is the validation source of truth); the studio only reads
386
+ * these fields.
387
+ */
388
+ export interface ContentSectionEntry {
389
+ source?: string;
390
+ format?: string;
391
+ schema?: ContentTypeSchema;
392
+ $elements?: unknown[];
393
+ }
394
+
276
395
  export interface CanvasPanel {
277
396
  mediaName: string;
278
397
  element: HTMLElement;