@jxsuite/studio 1.0.0 → 1.1.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 (86) hide show
  1. package/dist/iframe-entry.js +622 -49
  2. package/dist/iframe-entry.js.map +15 -12
  3. package/dist/studio.css +1333 -0
  4. package/dist/studio.js +53069 -31746
  5. package/dist/studio.js.map +109 -74
  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 +20 -0
  11. package/src/canvas/iframe-eval.ts +155 -0
  12. package/src/canvas/iframe-host.ts +131 -5
  13. package/src/canvas/iframe-inline-edit.ts +107 -1
  14. package/src/canvas/iframe-protocol.ts +43 -3
  15. package/src/canvas/iframe-render.ts +18 -0
  16. package/src/collab/collab-session.ts +23 -8
  17. package/src/collab/collab-state.ts +6 -3
  18. package/src/component-props.ts +104 -0
  19. package/src/editor/inline-edit-apply.ts +36 -1
  20. package/src/editor/inline-edit.ts +58 -1
  21. package/src/editor/shortcuts.ts +41 -12
  22. package/src/files/file-ops.ts +14 -0
  23. package/src/files/files.ts +53 -1
  24. package/src/grid/cell-editors.ts +288 -0
  25. package/src/grid/cell-popovers.ts +108 -0
  26. package/src/grid/csv-codec.ts +122 -0
  27. package/src/grid/edit-buffer.ts +490 -0
  28. package/src/grid/grid-controller.ts +417 -0
  29. package/src/grid/grid-layout.ts +83 -0
  30. package/src/grid/grid-open.ts +167 -0
  31. package/src/grid/grid-panel.ts +302 -0
  32. package/src/grid/grid-source.ts +210 -0
  33. package/src/grid/grid-view.ts +367 -0
  34. package/src/grid/schema-columns.ts +261 -0
  35. package/src/grid/sources/connector-source.ts +217 -0
  36. package/src/grid/sources/content-source.ts +542 -0
  37. package/src/grid/sources/csv-file-source.ts +247 -0
  38. package/src/grid/tabulator-tables.d.ts +120 -0
  39. package/src/panels/block-action-bar.ts +8 -3
  40. package/src/panels/chat-panel.ts +98 -0
  41. package/src/panels/data-grid.ts +9 -387
  42. package/src/panels/editors.ts +43 -17
  43. package/src/panels/events-panel.ts +137 -44
  44. package/src/panels/formula-workspace.ts +379 -0
  45. package/src/panels/frontmatter-fields.ts +231 -0
  46. package/src/panels/frontmatter-panel.ts +132 -0
  47. package/src/panels/git-panel.ts +1 -1
  48. package/src/panels/head-panel.ts +11 -175
  49. package/src/panels/properties-panel.ts +154 -144
  50. package/src/panels/right-panel.ts +9 -47
  51. package/src/panels/signals-panel.ts +115 -23
  52. package/src/panels/statement-editor.ts +710 -0
  53. package/src/panels/style-panel.ts +25 -1
  54. package/src/panels/stylebook-panel.ts +0 -2
  55. package/src/panels/tab-bar.ts +175 -6
  56. package/src/panels/tab-strip.ts +62 -6
  57. package/src/panels/toolbar.ts +37 -3
  58. package/src/services/ai-project-tools.ts +541 -0
  59. package/src/services/ai-session-store.ts +28 -0
  60. package/src/services/ai-system-prompt.ts +194 -24
  61. package/src/services/ai-tools.ts +88 -21
  62. package/src/services/automation.ts +16 -0
  63. package/src/services/document-assistant.ts +81 -11
  64. package/src/services/gated-registry.ts +72 -0
  65. package/src/services/live-preview.ts +0 -0
  66. package/src/services/preview-eval.ts +68 -0
  67. package/src/services/project-adoption.ts +31 -0
  68. package/src/site-context.ts +2 -0
  69. package/src/store.ts +4 -0
  70. package/src/studio.ts +83 -52
  71. package/src/tabs/patch-ops.ts +25 -0
  72. package/src/tabs/tab.ts +39 -1
  73. package/src/tabs/transact.ts +60 -13
  74. package/src/types.ts +12 -0
  75. package/src/ui/dynamic-slot.ts +272 -0
  76. package/src/ui/expression-editor.ts +423 -125
  77. package/src/ui/field-row.ts +5 -0
  78. package/src/ui/formula-catalog.ts +557 -0
  79. package/src/ui/formula-chips.ts +216 -0
  80. package/src/ui/formula-palette.ts +211 -0
  81. package/src/ui/layers.ts +40 -0
  82. package/src/ui/media-picker.ts +1 -1
  83. package/src/ui/panel-resize.ts +15 -1
  84. package/src/utils/preview-format.ts +26 -0
  85. package/src/view.ts +4 -4
  86. package/src/workspace/workspace.ts +14 -0
@@ -1,84 +1,28 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * Data grid — the owner console over the platform's data surface (plan Part 4a).
3
+ * Data section actions — the owner console entry points over the platform's data surface.
4
4
  *
5
5
  * Integration: the settings modal's contributed sections stay fully generic — extension-sections
6
6
  * passes this module's {@link dataSectionActions} into the ContributedSectionOptions.actions slot
7
7
  * for the data-domain sections ("connections"/"data") whenever the platform implements the
8
8
  * protocol's data routes. Those actions surface Test Connection, Push Schema (dry-run plan
9
- * confirmation before apply), and Open Data Grid; the grid itself is a full-surface modal
10
- * (settings-modal pattern) gated on `platform.dataRows`.
11
- *
12
- * The grid is v1-minimal by design: connection + table picker (declared tables plus system tables
13
- * discovered from backend introspection), 50-row pagination, inline cell edit committed via
14
- * dataUpdateRow keyed on the introspected primary key, an add-row footer, and two-step per-row
15
- * delete. No filter/sort UI.
9
+ * confirmation before apply), and Open Data Grid — which opens the grid-tab source picker; table
10
+ * editing itself lives in the spreadsheet grid tabs (src/grid/), which replaced the old modal grid
11
+ * at feature parity (paging, cell edit, add/delete row) and added batch save with undo.
16
12
  */
17
13
 
18
14
  import { html, nothing } from "lit-html";
19
- import { live } from "lit-html/directives/live.js";
20
15
  import { openModal } from "../ui/layers";
21
- import {
22
- dataSurfaceAvailable,
23
- deleteRow,
24
- fetchConnections,
25
- fetchRows,
26
- insertRow,
27
- pushSchema,
28
- testConnection,
29
- updateRow,
30
- } from "../services/data-service";
16
+ import { dataSurfaceAvailable, pushSchema, testConnection } from "../services/data-service";
17
+ import { openGridSourcePicker } from "../grid/grid-open";
31
18
  import type { TemplateResult } from "lit-html";
32
- import type {
33
- DataColumnMeta,
34
- DataConnectionInfo,
35
- DataConnectionTestResult,
36
- DataPushResult,
37
- } from "../types";
19
+ import type { DataConnectionTestResult, DataPushResult } from "../types";
38
20
  import type { SectionActionsContext } from "../settings/contributed-section";
39
21
 
40
- export const DATA_GRID_PAGE_SIZE = 50;
41
-
42
- // ─── Grid state ───────────────────────────────────────────────────────────────
43
-
44
- interface GridState {
45
- connections: DataConnectionInfo[];
46
- connection: string | null;
47
- table: string | null;
48
- columns: DataColumnMeta[];
49
- rows: Record<string, unknown>[];
50
- total: number;
51
- offset: number;
52
- loading: boolean;
53
- error: string | null;
54
- /** Pk value armed for the two-step delete confirm. */
55
- pendingDelete: string | null;
56
- /** Add-row footer draft, keyed by column name. */
57
- draft: Record<string, string>;
58
- }
59
-
60
- const blankGrid = (): GridState => ({
61
- columns: [],
62
- connection: null,
63
- connections: [],
64
- draft: {},
65
- error: null,
66
- loading: false,
67
- offset: 0,
68
- pendingDelete: null,
69
- rows: [],
70
- table: null,
71
- total: 0,
72
- });
73
-
74
- let grid: GridState = blankGrid();
75
- let gridHandle: ReturnType<typeof openModal> | null = null;
22
+ // ─── Grid opening (delegates to the grid-tab source picker) ──────────────────
76
23
 
77
24
  /** Reset all module UI state and close any open surfaces (test hook / project switch). */
78
25
  export function resetDataGridState(): void {
79
- grid = blankGrid();
80
- gridHandle?.close();
81
- gridHandle = null;
82
26
  actionsState = { pushing: false, testResult: null, testing: null };
83
27
  closePushDialog();
84
28
  }
@@ -88,325 +32,6 @@ export function isDataGridAvailable(): boolean {
88
32
  return dataSurfaceAvailable();
89
33
  }
90
34
 
91
- /** Close the grid modal. */
92
- export function closeDataGrid(): void {
93
- gridHandle?.close();
94
- gridHandle = null;
95
- }
96
-
97
- /**
98
- * Open the data grid modal, loading connections and the first page.
99
- *
100
- * @param {{ connection?: string; table?: string }} [preselect]
101
- */
102
- export async function openDataGrid(preselect: { connection?: string; table?: string } = {}) {
103
- if (!isDataGridAvailable() || gridHandle) {
104
- return;
105
- }
106
- grid = blankGrid();
107
- grid.loading = true;
108
- renderGrid();
109
- const res = await fetchConnections().catch(() => null);
110
- grid.connections = res?.connections ?? [];
111
- grid.connection =
112
- (preselect.connection &&
113
- grid.connections.some((c) => c.name === preselect.connection) &&
114
- preselect.connection) ||
115
- grid.connections.find((c) => c.isDefault)?.name ||
116
- grid.connections[0]?.name ||
117
- null;
118
- const tables = currentTables();
119
- grid.table =
120
- preselect.table && tables.includes(preselect.table) ? preselect.table : (tables[0] ?? null);
121
- await loadRows();
122
- }
123
-
124
- /** Table names of the selected connection (declared + backend-introspected system tables). */
125
- function currentTables(): string[] {
126
- return grid.connections.find((c) => c.name === grid.connection)?.tables ?? [];
127
- }
128
-
129
- /** The primary-key column name (backend convention: "id" when nothing is flagged). */
130
- function pkColumn(): string {
131
- return grid.columns.find((c) => c.pk)?.name ?? "id";
132
- }
133
-
134
- /** Load the current page. */
135
- async function loadRows(): Promise<void> {
136
- if (!grid.table) {
137
- grid.rows = [];
138
- grid.columns = [];
139
- grid.total = 0;
140
- grid.loading = false;
141
- renderGrid();
142
- return;
143
- }
144
- grid.loading = true;
145
- renderGrid();
146
- try {
147
- const result = await fetchRows({
148
- limit: DATA_GRID_PAGE_SIZE,
149
- offset: grid.offset,
150
- table: grid.table,
151
- ...(grid.connection === null ? {} : { connection: grid.connection }),
152
- });
153
- grid.rows = result.rows;
154
- grid.columns = result.columns;
155
- grid.total = result.total;
156
- grid.error = null;
157
- } catch (error) {
158
- grid.rows = [];
159
- grid.columns = [];
160
- grid.total = 0;
161
- grid.error = error instanceof Error ? error.message : String(error);
162
- }
163
- grid.loading = false;
164
- grid.pendingDelete = null;
165
- renderGrid();
166
- }
167
-
168
- /** Commit one edited cell via dataUpdateRow, keyed on the row's pk. */
169
- async function commitCell(
170
- row: Record<string, unknown>,
171
- column: DataColumnMeta,
172
- raw: string,
173
- ): Promise<void> {
174
- const current = row[column.name];
175
- const currentText = current == null ? "" : String(current);
176
- if (raw === currentText || !grid.table) {
177
- return;
178
- }
179
- try {
180
- const updated = await updateRow({
181
- pk: row[pkColumn()] as string | number,
182
- set: { [column.name]: raw === "" ? null : raw },
183
- table: grid.table,
184
- ...(grid.connection === null ? {} : { connection: grid.connection }),
185
- });
186
- const index = grid.rows.indexOf(row);
187
- if (index !== -1) {
188
- grid.rows[index] = updated.row;
189
- }
190
- grid.error = null;
191
- } catch (error) {
192
- grid.error = error instanceof Error ? error.message : String(error);
193
- }
194
- renderGrid();
195
- }
196
-
197
- /** Two-step delete: first click arms the row, second click deletes. */
198
- async function requestDelete(pk: string): Promise<void> {
199
- if (grid.pendingDelete !== pk) {
200
- grid.pendingDelete = pk;
201
- renderGrid();
202
- return;
203
- }
204
- if (!grid.table) {
205
- return;
206
- }
207
- try {
208
- await deleteRow({
209
- pk,
210
- table: grid.table,
211
- ...(grid.connection === null ? {} : { connection: grid.connection }),
212
- });
213
- await loadRows();
214
- } catch (error) {
215
- grid.error = error instanceof Error ? error.message : String(error);
216
- grid.pendingDelete = null;
217
- renderGrid();
218
- }
219
- }
220
-
221
- /** Insert the add-row footer draft. */
222
- async function commitDraft(): Promise<void> {
223
- if (!grid.table) {
224
- return;
225
- }
226
- const values: Record<string, unknown> = {};
227
- for (const [name, raw] of Object.entries(grid.draft)) {
228
- if (raw !== "") {
229
- values[name] = raw;
230
- }
231
- }
232
- try {
233
- await insertRow({
234
- table: grid.table,
235
- values,
236
- ...(grid.connection === null ? {} : { connection: grid.connection }),
237
- });
238
- grid.draft = {};
239
- await loadRows();
240
- } catch (error) {
241
- grid.error = error instanceof Error ? error.message : String(error);
242
- renderGrid();
243
- }
244
- }
245
-
246
- /** Columns editable in the grid (system-managed columns are display-only). */
247
- function editableColumn(column: DataColumnMeta): boolean {
248
- return !column.pk && column.name !== "created_at" && column.name !== "updated_at";
249
- }
250
-
251
- function gridBody(): TemplateResult {
252
- if (grid.loading) {
253
- return html`<div class="data-grid-empty">Loading…</div>`;
254
- }
255
- if (grid.error) {
256
- return html`<div class="data-grid-error">${grid.error}</div>`;
257
- }
258
- if (!grid.table) {
259
- return html`<div class="data-grid-empty">
260
- No tables on this connection — push a schema first.
261
- </div>`;
262
- }
263
- const pk = pkColumn();
264
- return html`
265
- <table class="data-grid-table">
266
- <thead>
267
- <tr>
268
- ${grid.columns.map(
269
- (c) =>
270
- html`<th title=${c.type}>
271
- ${c.name}${c.pk ? html` <span class="data-grid-pk">pk</span>` : nothing}
272
- </th>`,
273
- )}
274
- <th></th>
275
- </tr>
276
- </thead>
277
- <tbody>
278
- ${grid.rows.map((row) => {
279
- const pkValue = String(row[pk] ?? "");
280
- return html`<tr>
281
- ${grid.columns.map((c) =>
282
- editableColumn(c)
283
- ? html`<td>
284
- <input
285
- class="data-grid-cell"
286
- .value=${live(row[c.name] == null ? "" : String(row[c.name]))}
287
- @change=${(e: Event) =>
288
- void commitCell(row, c, (e.target as HTMLInputElement).value)}
289
- />
290
- </td>`
291
- : html`<td class="data-grid-readonly">
292
- ${row[c.name] == null ? "" : String(row[c.name])}
293
- </td>`,
294
- )}
295
- <td>
296
- <sp-action-button
297
- quiet
298
- size="s"
299
- class="data-grid-delete"
300
- @click=${() => void requestDelete(pkValue)}
301
- >
302
- ${grid.pendingDelete === pkValue ? "Confirm?" : "Delete"}
303
- </sp-action-button>
304
- </td>
305
- </tr>`;
306
- })}
307
- </tbody>
308
- <tfoot>
309
- <tr class="data-grid-add-row">
310
- ${grid.columns.map((c) =>
311
- editableColumn(c)
312
- ? html`<td>
313
- <input
314
- class="data-grid-draft"
315
- placeholder=${c.name}
316
- .value=${live(grid.draft[c.name] ?? "")}
317
- @input=${(e: Event) => {
318
- grid.draft[c.name] = (e.target as HTMLInputElement).value;
319
- }}
320
- />
321
- </td>`
322
- : html`<td></td>`,
323
- )}
324
- <td>
325
- <sp-action-button size="s" class="data-grid-add" @click=${() => void commitDraft()}>
326
- Add
327
- </sp-action-button>
328
- </td>
329
- </tr>
330
- </tfoot>
331
- </table>
332
- `;
333
- }
334
-
335
- function renderGrid(): void {
336
- const first = grid.total === 0 ? 0 : grid.offset + 1;
337
- const last = grid.offset + grid.rows.length;
338
- const tpl = html`
339
- <sp-underlay open @close=${closeDataGrid}></sp-underlay>
340
- <div class="settings-modal data-grid-modal">
341
- <div class="settings-modal-header">
342
- <h2 class="settings-modal-title">Data</h2>
343
- <sp-picker
344
- size="s"
345
- class="data-grid-connection"
346
- value=${grid.connection ?? ""}
347
- @change=${(e: Event) => {
348
- grid.connection = (e.target as HTMLInputElement).value || null;
349
- grid.offset = 0;
350
- grid.table = currentTables()[0] ?? null;
351
- void loadRows();
352
- }}
353
- >
354
- ${grid.connections.map(
355
- (c) => html`<sp-menu-item value=${c.name}>${c.name}</sp-menu-item>`,
356
- )}
357
- </sp-picker>
358
- <sp-picker
359
- size="s"
360
- class="data-grid-tables"
361
- value=${grid.table ?? ""}
362
- @change=${(e: Event) => {
363
- grid.table = (e.target as HTMLInputElement).value || null;
364
- grid.offset = 0;
365
- grid.draft = {};
366
- void loadRows();
367
- }}
368
- >
369
- ${currentTables().map(
370
- (table) => html`<sp-menu-item value=${table}>${table}</sp-menu-item>`,
371
- )}
372
- </sp-picker>
373
- <sp-action-button quiet size="s" title="Close" @click=${closeDataGrid}>
374
- <sp-icon-close slot="icon"></sp-icon-close>
375
- </sp-action-button>
376
- </div>
377
- <div class="settings-modal-body data-grid-body">${gridBody()}</div>
378
- <div class="data-grid-footer">
379
- <sp-action-button
380
- size="s"
381
- class="data-grid-prev"
382
- ?disabled=${grid.offset === 0}
383
- @click=${() => {
384
- grid.offset = Math.max(0, grid.offset - DATA_GRID_PAGE_SIZE);
385
- void loadRows();
386
- }}
387
- >Prev</sp-action-button
388
- >
389
- <span class="data-grid-range">${first}–${last} of ${grid.total}</span>
390
- <sp-action-button
391
- size="s"
392
- class="data-grid-next"
393
- ?disabled=${grid.offset + DATA_GRID_PAGE_SIZE >= grid.total}
394
- @click=${() => {
395
- grid.offset += DATA_GRID_PAGE_SIZE;
396
- void loadRows();
397
- }}
398
- >Next</sp-action-button
399
- >
400
- </div>
401
- </div>
402
- `;
403
- if (gridHandle) {
404
- gridHandle.update(tpl);
405
- } else {
406
- gridHandle = openModal(tpl);
407
- }
408
- }
409
-
410
35
  // ─── Push dialog (dry-run plan confirmation before apply) ─────────────────────
411
36
 
412
37
  interface PushDialogState {
@@ -598,10 +223,7 @@ function renderSectionActions(sectionKey: string, ctx: SectionActionsContext): T
598
223
  <sp-action-button
599
224
  size="s"
600
225
  class="data-action-grid"
601
- @click=${() =>
602
- void openDataGrid(
603
- sectionKey === "connections" && selected ? { connection: selected } : {},
604
- )}
226
+ @click=${() => void openGridSourcePicker()}
605
227
  >
606
228
  Open Data Grid
607
229
  </sp-action-button>
@@ -14,6 +14,7 @@ import { activeTab } from "../workspace/workspace";
14
14
  import { mutateUpdateDef, mutateUpdateProperty, transactDoc } from "../tabs/transact";
15
15
  import { view } from "../view";
16
16
  import { codeService, getFunctionArgs, setLintMarkers } from "../services/code-services";
17
+ import { globalEntries, namedFormulaEntries } from "../ui/formula-catalog";
17
18
 
18
19
  import type { OxLintDiagnostic } from "../services/code-services";
19
20
  import type { JxMutableNode, JxPrototypeDef } from "@jxsuite/schema/types";
@@ -185,7 +186,10 @@ export function registerFunctionCompletions() {
185
186
  view._completionRegistered = true;
186
187
  monaco.languages.registerCompletionItemProvider("javascript", {
187
188
  provideCompletionItems(model, position) {
188
- const defs = activeTab.value?.doc.document?.state || {};
189
+ if (!activeTab.value) {
190
+ return { suggestions: [] };
191
+ }
192
+ const defs = activeTab.value.doc.document?.state || {};
189
193
  const word = model.getWordUntilPosition(position);
190
194
  const range = {
191
195
  endColumn: word.endColumn,
@@ -194,23 +198,45 @@ export function registerFunctionCompletions() {
194
198
  startLineNumber: position.lineNumber,
195
199
  };
196
200
 
197
- const suggestions = Object.entries(defs).map(([key, def]) => {
198
- let kind = monaco.languages.CompletionItemKind.Variable;
199
- if (
200
- (def as JxPrototypeDef)?.$prototype === "Function" ||
201
- (def as Record<string, unknown>)?.$handler
202
- ) {
203
- kind = monaco.languages.CompletionItemKind.Function;
204
- } else if ((def as JxPrototypeDef)?.$prototype) {
205
- kind = monaco.languages.CompletionItemKind.Property;
206
- }
207
- return {
208
- insertText: `state.${key}`,
209
- kind,
210
- label: `state.${key}`,
201
+ // Named-formula catalog metadata enriches their completions with documentation.
202
+ const formulaDocs = new Map(namedFormulaEntries(defs).map((e) => [e.name, e.description]));
203
+
204
+ const suggestions: monaco.languages.CompletionItem[] = Object.entries(defs).map(
205
+ ([key, def]) => {
206
+ let kind = monaco.languages.CompletionItemKind.Variable;
207
+ if (
208
+ (def as JxPrototypeDef)?.$prototype === "Function" ||
209
+ (def as Record<string, unknown>)?.$handler ||
210
+ formulaDocs.has(key)
211
+ ) {
212
+ kind = monaco.languages.CompletionItemKind.Function;
213
+ } else if ((def as JxPrototypeDef)?.$prototype) {
214
+ kind = monaco.languages.CompletionItemKind.Property;
215
+ }
216
+ const item: monaco.languages.CompletionItem = {
217
+ insertText: `state.${key}`,
218
+ kind,
219
+ label: `state.${key}`,
220
+ range,
221
+ };
222
+ const documentation = formulaDocs.get(key);
223
+ if (documentation) {
224
+ item.documentation = documentation;
225
+ }
226
+ return item;
227
+ },
228
+ );
229
+
230
+ // Blessed pure globals from the formula catalog (Math.*, JSON.*, Object.*, …).
231
+ for (const entry of globalEntries()) {
232
+ suggestions.push({
233
+ documentation: entry.description,
234
+ insertText: `window.${entry.label}`,
235
+ kind: monaco.languages.CompletionItemKind.Function,
236
+ label: entry.label,
211
237
  range,
212
- };
213
- });
238
+ });
239
+ }
214
240
  return { suggestions };
215
241
  },
216
242
  triggerCharacters: ["."],