@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.
- package/dist/iframe-entry.js +622 -49
- package/dist/iframe-entry.js.map +15 -12
- package/dist/studio.css +1333 -0
- package/dist/studio.js +53069 -31746
- package/dist/studio.js.map +109 -74
- 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 +20 -0
- package/src/canvas/iframe-eval.ts +155 -0
- package/src/canvas/iframe-host.ts +131 -5
- package/src/canvas/iframe-inline-edit.ts +107 -1
- package/src/canvas/iframe-protocol.ts +43 -3
- package/src/canvas/iframe-render.ts +18 -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 +25 -1
- package/src/panels/stylebook-panel.ts +0 -2
- package/src/panels/tab-bar.ts +175 -6
- 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/site-context.ts +2 -0
- package/src/store.ts +4 -0
- package/src/studio.ts +83 -52
- package/src/tabs/patch-ops.ts +25 -0
- package/src/tabs/tab.ts +39 -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/preview-format.ts +26 -0
- package/src/view.ts +4 -4
- package/src/workspace/workspace.ts +14 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSV file grid source — one `.csv` project file edited as a grid.
|
|
3
|
+
*
|
|
4
|
+
* The file loads once into a positional string matrix (csv-codec). Columns are typed by the
|
|
5
|
+
* matching content-type schema when the CSV backs a collection (same resolver as the head panel),
|
|
6
|
+
* otherwise inferred by sampling values. Commit is atomic whole-file: pending edits are applied to
|
|
7
|
+
* the matrix, the document is re-serialized, and a save-time staleness check (file text vs. the
|
|
8
|
+
* load-time text) aborts the entire commit rather than clobber an external change. Only edited
|
|
9
|
+
* cells are canonicalized — untouched cells keep their raw file text.
|
|
10
|
+
*/
|
|
11
|
+
import { getPlatform } from "../../platform";
|
|
12
|
+
import { projectState } from "../../store";
|
|
13
|
+
import { findContentTypeSchema } from "../../utils/studio-utils";
|
|
14
|
+
import { isRecentLocal, markLocalMutation } from "../../files/fs-events";
|
|
15
|
+
import { parseCsv, serializeCsv } from "../csv-codec";
|
|
16
|
+
import {
|
|
17
|
+
cellToText,
|
|
18
|
+
coerceCellInput,
|
|
19
|
+
columnsFromSchema,
|
|
20
|
+
inferColumnsFromRows,
|
|
21
|
+
} from "../schema-columns";
|
|
22
|
+
import type { CsvDocument } from "../csv-codec";
|
|
23
|
+
import type {
|
|
24
|
+
CommitResult,
|
|
25
|
+
GridCellValue,
|
|
26
|
+
GridColumn,
|
|
27
|
+
GridEditBatch,
|
|
28
|
+
GridRow,
|
|
29
|
+
GridRowsResult,
|
|
30
|
+
GridSource,
|
|
31
|
+
} from "../grid-source";
|
|
32
|
+
|
|
33
|
+
/** Candidate identity columns, in the parser extension's fallback order. */
|
|
34
|
+
const ID_FIELDS = ["id", "sku", "slug", "Slug"];
|
|
35
|
+
|
|
36
|
+
interface CsvModel {
|
|
37
|
+
doc: CsvDocument;
|
|
38
|
+
/** Unique field name per column position (duplicate/empty headers get positional names). */
|
|
39
|
+
fields: string[];
|
|
40
|
+
columns: GridColumn[];
|
|
41
|
+
/** Map of rowKey → matrix row index. */
|
|
42
|
+
keyToIndex: Map<string, number>;
|
|
43
|
+
/** Matrix row index → rowKey. */
|
|
44
|
+
keys: string[];
|
|
45
|
+
/** File text at load time — the whole-file staleness fingerprint. */
|
|
46
|
+
loadText: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Unique grid field names for CSV headers (duplicates/empties fall back to positional names). */
|
|
50
|
+
export function fieldNamesForHeaders(headers: string[]): string[] {
|
|
51
|
+
const used = new Set<string>();
|
|
52
|
+
return headers.map((header, i) => {
|
|
53
|
+
let name = header.trim() === "" ? `column${i + 1}` : header;
|
|
54
|
+
if (used.has(name)) {
|
|
55
|
+
name = `${name}__${i + 1}`;
|
|
56
|
+
}
|
|
57
|
+
used.add(name);
|
|
58
|
+
return name;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Pick the identity column: first id-chain field whose values are all unique and non-empty. */
|
|
63
|
+
function pickIdField(fields: string[], doc: CsvDocument): string | null {
|
|
64
|
+
for (const candidate of ID_FIELDS) {
|
|
65
|
+
const idx = fields.indexOf(candidate);
|
|
66
|
+
if (idx === -1) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const values = doc.rows.map((row) => row[idx] ?? "");
|
|
70
|
+
if (values.every((v) => v !== "") && new Set(values).size === values.length) {
|
|
71
|
+
return candidate;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildModel(text: string, path: string): CsvModel {
|
|
78
|
+
const doc = parseCsv(text);
|
|
79
|
+
const fields = fieldNamesForHeaders(doc.headers);
|
|
80
|
+
const idField = pickIdField(fields, doc);
|
|
81
|
+
const idIndex = idField ? fields.indexOf(idField) : -1;
|
|
82
|
+
const keys = doc.rows.map((row, i) => (idIndex === -1 ? String(i) : row[idIndex]!));
|
|
83
|
+
const keyToIndex = new Map(keys.map((key, i) => [key, i]));
|
|
84
|
+
|
|
85
|
+
// Typed columns when this CSV is a collection source; sniffed kinds otherwise.
|
|
86
|
+
const contentType = findContentTypeSchema(path, projectState?.projectConfig);
|
|
87
|
+
const schemaProps = contentType?.schema?.properties ?? {};
|
|
88
|
+
const sampleRows = doc.rows.slice(0, 50).map((row) => {
|
|
89
|
+
const cells: Record<string, GridCellValue> = {};
|
|
90
|
+
for (const [i, field] of fields.entries()) {
|
|
91
|
+
cells[field] = row[i] ?? "";
|
|
92
|
+
}
|
|
93
|
+
return cells;
|
|
94
|
+
});
|
|
95
|
+
const inferred = new Map(inferColumnsFromRows(sampleRows).map((c) => [c.field, c]));
|
|
96
|
+
const typed = new Map(
|
|
97
|
+
columnsFromSchema(contentType?.schema, { idField: idField ?? undefined }).map((c) => [
|
|
98
|
+
c.field,
|
|
99
|
+
c,
|
|
100
|
+
]),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const columns = fields.map((field, i): GridColumn => {
|
|
104
|
+
const fromSchema = field in schemaProps ? typed.get(field) : undefined;
|
|
105
|
+
const column = fromSchema ??
|
|
106
|
+
inferred.get(field) ?? {
|
|
107
|
+
editable: true,
|
|
108
|
+
field,
|
|
109
|
+
kind: "string" as const,
|
|
110
|
+
title: doc.headers[i] || field,
|
|
111
|
+
};
|
|
112
|
+
return { ...column, pk: field === idField, title: doc.headers[i]?.trim() || column.title };
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return { columns, doc, fields, keys, keyToIndex, loadText: text };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Typed cell values for one matrix row. */
|
|
119
|
+
function rowCells(model: CsvModel, rowIndex: number): Record<string, GridCellValue> {
|
|
120
|
+
const cells: Record<string, GridCellValue> = {};
|
|
121
|
+
const row = model.doc.rows[rowIndex] ?? [];
|
|
122
|
+
for (const [i, field] of model.fields.entries()) {
|
|
123
|
+
const column = model.columns[i]!;
|
|
124
|
+
const raw = row[i] ?? "";
|
|
125
|
+
cells[field] =
|
|
126
|
+
column.kind === "string" || column.kind === "text" || column.kind === "readonly"
|
|
127
|
+
? raw
|
|
128
|
+
: coerceCellInput(raw, column);
|
|
129
|
+
}
|
|
130
|
+
return cells;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Apply a batch to a copy of the matrix (cells, then inserts, then deletes). */
|
|
134
|
+
function applyBatch(model: CsvModel, batch: GridEditBatch): CsvDocument {
|
|
135
|
+
const rows = model.doc.rows.map((row) => [...row]);
|
|
136
|
+
const colFor = (field: string) => model.fields.indexOf(field);
|
|
137
|
+
|
|
138
|
+
for (const cell of batch.cells) {
|
|
139
|
+
const rowIndex = model.keyToIndex.get(cell.rowKey);
|
|
140
|
+
const colIndex = colFor(cell.field);
|
|
141
|
+
if (rowIndex === undefined || colIndex === -1) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
rows[rowIndex]![colIndex] = cellToText(cell.value);
|
|
145
|
+
}
|
|
146
|
+
for (const insert of batch.inserts) {
|
|
147
|
+
const row = model.fields.map((field) => cellToText(insert.cells[field] ?? null));
|
|
148
|
+
rows.push(row);
|
|
149
|
+
}
|
|
150
|
+
const dropIndexes = batch.deletes
|
|
151
|
+
.map((del) => model.keyToIndex.get(del.rowKey))
|
|
152
|
+
.filter((i): i is number => i !== undefined)
|
|
153
|
+
.toSorted((a, b) => b - a);
|
|
154
|
+
for (const index of dropIndexes) {
|
|
155
|
+
rows.splice(index, 1);
|
|
156
|
+
}
|
|
157
|
+
return { ...model.doc, rows };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** All batch items resolved with one shared outcome (whole-file commits are atomic). */
|
|
161
|
+
function uniformResult(
|
|
162
|
+
batch: GridEditBatch,
|
|
163
|
+
outcome: { ok: boolean; error?: string; stale?: boolean },
|
|
164
|
+
): CommitResult {
|
|
165
|
+
return {
|
|
166
|
+
cells: batch.cells.map((c) => ({ field: c.field, rowKey: c.rowKey, ...outcome })),
|
|
167
|
+
deletes: batch.deletes.map((d) => ({ rowKey: d.rowKey, ...outcome })),
|
|
168
|
+
inserts: batch.inserts.map((i) => ({
|
|
169
|
+
error: outcome.error,
|
|
170
|
+
ok: outcome.ok,
|
|
171
|
+
tempKey: i.tempKey,
|
|
172
|
+
})),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Create the grid source for one CSV file. The tab id is the file path. */
|
|
177
|
+
export function createCsvFileSource(path: string): GridSource {
|
|
178
|
+
let loadPromise: Promise<CsvModel> | null = null;
|
|
179
|
+
|
|
180
|
+
const load = (force = false): Promise<CsvModel> => {
|
|
181
|
+
if (!force && loadPromise) {
|
|
182
|
+
return loadPromise;
|
|
183
|
+
}
|
|
184
|
+
loadPromise = getPlatform()
|
|
185
|
+
.readFile(path)
|
|
186
|
+
.then((text) => buildModel(text, path));
|
|
187
|
+
return loadPromise;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
backingPaths: () => new Map([[path, "*"]]),
|
|
192
|
+
capabilities: { delete: true, insert: true, remotePaging: false, remoteSort: false },
|
|
193
|
+
|
|
194
|
+
async columns(): Promise<GridColumn[]> {
|
|
195
|
+
const current = await load();
|
|
196
|
+
return current.columns;
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
async commit(batch: GridEditBatch): Promise<CommitResult> {
|
|
200
|
+
const current = await load();
|
|
201
|
+
const platform = getPlatform();
|
|
202
|
+
|
|
203
|
+
// Whole-file staleness: abort if the file changed under us (and it wasn't our own write).
|
|
204
|
+
let onDisk: string | null = null;
|
|
205
|
+
try {
|
|
206
|
+
onDisk = await platform.readFile(path);
|
|
207
|
+
} catch {
|
|
208
|
+
onDisk = null; // Deleted underneath — treat as stale.
|
|
209
|
+
}
|
|
210
|
+
if (onDisk !== current.loadText && !isRecentLocal(path)) {
|
|
211
|
+
return uniformResult(batch, {
|
|
212
|
+
error: "File changed on disk — refresh to reload",
|
|
213
|
+
ok: false,
|
|
214
|
+
stale: true,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const nextDoc = applyBatch(current, batch);
|
|
219
|
+
const text = serializeCsv(nextDoc);
|
|
220
|
+
markLocalMutation(path);
|
|
221
|
+
await platform.writeFile(path, text);
|
|
222
|
+
loadPromise = Promise.resolve(buildModel(text, path));
|
|
223
|
+
return uniformResult(batch, { ok: true });
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
id: path,
|
|
227
|
+
label: path.split("/").pop() ?? path,
|
|
228
|
+
|
|
229
|
+
async refresh(): Promise<void> {
|
|
230
|
+
await load(true);
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
async rows(): Promise<GridRowsResult> {
|
|
234
|
+
const current = await load();
|
|
235
|
+
const rows: GridRow[] = current.doc.rows.map((_, i) => ({
|
|
236
|
+
cells: rowCells(current, i),
|
|
237
|
+
key: current.keys[i]!,
|
|
238
|
+
}));
|
|
239
|
+
return { rows, total: rows.length };
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
async serializeForSource(batch: GridEditBatch): Promise<string> {
|
|
243
|
+
const current = await load();
|
|
244
|
+
return serializeCsv(applyBatch(current, batch));
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal ambient types for tabulator-tables 6.x — the package ships no declarations. Only the
|
|
3
|
+
* surface grid-view.ts touches is typed; extend here as usage grows.
|
|
4
|
+
*/
|
|
5
|
+
declare module "tabulator-tables" {
|
|
6
|
+
export interface CellComponent {
|
|
7
|
+
getValue: () => unknown;
|
|
8
|
+
getField: () => string;
|
|
9
|
+
getElement: () => HTMLElement;
|
|
10
|
+
getRow: () => RowComponent;
|
|
11
|
+
setValue: (value: unknown, mutate?: boolean) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RowComponent {
|
|
15
|
+
getData: () => Record<string, unknown>;
|
|
16
|
+
getElement: () => HTMLElement;
|
|
17
|
+
getCells: () => CellComponent[];
|
|
18
|
+
reformat: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RangeComponent {
|
|
22
|
+
getRows: () => RowComponent[];
|
|
23
|
+
getCells: () => CellComponent[][];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ColumnComponent {
|
|
27
|
+
getField: () => string;
|
|
28
|
+
getWidth: () => number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type CellEditor = (
|
|
32
|
+
cell: CellComponent,
|
|
33
|
+
onRendered: (fn: () => void) => void,
|
|
34
|
+
success: (value: unknown) => void,
|
|
35
|
+
cancel: () => void,
|
|
36
|
+
) => HTMLElement | false;
|
|
37
|
+
|
|
38
|
+
export type CellFormatter = (
|
|
39
|
+
cell: CellComponent,
|
|
40
|
+
formatterParams: Record<string, unknown>,
|
|
41
|
+
onRendered: (fn: () => void) => void,
|
|
42
|
+
) => HTMLElement | string;
|
|
43
|
+
|
|
44
|
+
export interface ColumnDefinition {
|
|
45
|
+
title: string;
|
|
46
|
+
field?: string | undefined;
|
|
47
|
+
width?: number | undefined;
|
|
48
|
+
frozen?: boolean | undefined;
|
|
49
|
+
editor?: CellEditor | undefined;
|
|
50
|
+
editable?: boolean | ((cell: CellComponent) => boolean) | undefined;
|
|
51
|
+
formatter?: CellFormatter | undefined;
|
|
52
|
+
sorter?: string | ((a: unknown, b: unknown) => number) | undefined;
|
|
53
|
+
headerSort?: boolean | undefined;
|
|
54
|
+
headerFilter?: string | undefined;
|
|
55
|
+
cssClass?: string | undefined;
|
|
56
|
+
resizable?: boolean | string | undefined;
|
|
57
|
+
headerSortTristate?: boolean | undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface Options {
|
|
61
|
+
data?: Record<string, unknown>[];
|
|
62
|
+
columns?: ColumnDefinition[];
|
|
63
|
+
index?: string;
|
|
64
|
+
layout?: string;
|
|
65
|
+
height?: string | number;
|
|
66
|
+
renderVertical?: string;
|
|
67
|
+
renderHorizontal?: string;
|
|
68
|
+
history?: boolean;
|
|
69
|
+
reactiveData?: boolean;
|
|
70
|
+
editTriggerEvent?: string;
|
|
71
|
+
headerSortClickElement?: string;
|
|
72
|
+
selectableRange?: number | boolean;
|
|
73
|
+
selectableRangeColumns?: boolean;
|
|
74
|
+
selectableRangeRows?: boolean;
|
|
75
|
+
selectableRangeClearCells?: boolean;
|
|
76
|
+
selectableRangeClearCellsValue?: unknown;
|
|
77
|
+
clipboard?: boolean;
|
|
78
|
+
clipboardCopyStyled?: boolean;
|
|
79
|
+
clipboardCopyConfig?: Record<string, unknown>;
|
|
80
|
+
clipboardCopyRowRange?: string;
|
|
81
|
+
clipboardPasteParser?: string;
|
|
82
|
+
clipboardPasteAction?: string;
|
|
83
|
+
movableColumns?: boolean;
|
|
84
|
+
columnDefaults?: Partial<ColumnDefinition>;
|
|
85
|
+
rowFormatter?: (row: RowComponent) => void;
|
|
86
|
+
placeholder?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class Tabulator {
|
|
90
|
+
constructor(host: HTMLElement, options: Options);
|
|
91
|
+
static registerModule(modules: unknown[]): void;
|
|
92
|
+
on(event: string, callback: (...args: never[]) => void): void;
|
|
93
|
+
replaceData(data: Record<string, unknown>[]): Promise<void>;
|
|
94
|
+
getRows(activeOnly?: string): RowComponent[];
|
|
95
|
+
getRanges(): RangeComponent[];
|
|
96
|
+
setFilter(filter: (data: Record<string, unknown>) => boolean): void;
|
|
97
|
+
clearFilter(includeHeaderFilters?: boolean): void;
|
|
98
|
+
redraw(force?: boolean): void;
|
|
99
|
+
destroy(): void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const EditModule: unknown;
|
|
103
|
+
export const FormatModule: unknown;
|
|
104
|
+
export const SortModule: unknown;
|
|
105
|
+
export const FilterModule: unknown;
|
|
106
|
+
export const ResizeColumnsModule: unknown;
|
|
107
|
+
export const MoveColumnsModule: unknown;
|
|
108
|
+
export const FrozenColumnsModule: unknown;
|
|
109
|
+
export const SelectRangeModule: unknown;
|
|
110
|
+
export const ClipboardModule: unknown;
|
|
111
|
+
export const ExportModule: unknown;
|
|
112
|
+
export const KeybindingsModule: unknown;
|
|
113
|
+
export const InteractionModule: unknown;
|
|
114
|
+
export const TooltipModule: unknown;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
declare module "tabulator-tables/dist/css/tabulator.min.css" {
|
|
118
|
+
const css: unknown;
|
|
119
|
+
export default css;
|
|
120
|
+
}
|
|
@@ -606,10 +606,12 @@ export function renderBlockActionBar() {
|
|
|
606
606
|
const tag = (node.tagName ?? "div").toLowerCase();
|
|
607
607
|
|
|
608
608
|
// Inline format state, sourced from the iframe's selection snapshot.
|
|
609
|
-
const { editing, snapshot } = getEditSnapshot();
|
|
609
|
+
const { editing, editingProp, snapshot } = getEditSnapshot();
|
|
610
610
|
const inlineEditing = editing;
|
|
611
611
|
const actions = getInlineActions(tag) || [];
|
|
612
|
-
|
|
612
|
+
// A prop-bound plain session edits a single plain string — never show the format group (belt and
|
|
613
|
+
// Braces: component tags have no $inlineActions, so `actions` is empty there anyway).
|
|
614
|
+
const showFormat = inlineEditing && !editingProp && actions.length > 0;
|
|
613
615
|
const activeValues =
|
|
614
616
|
showFormat && snapshot
|
|
615
617
|
? actions.filter((a) => snapshot.activeTags.includes(a.tag)).map((a) => a.tag)
|
|
@@ -646,7 +648,10 @@ export function renderBlockActionBar() {
|
|
|
646
648
|
@click=${badgeInteractive
|
|
647
649
|
? (e: MouseEvent) => onTagBadgeClick(e, convertTargets, selection)
|
|
648
650
|
: nothing}
|
|
649
|
-
>${isRepeater ? nodeLabel(node) : node.$id || (node.tagName ?? "div")}
|
|
651
|
+
>${isRepeater ? nodeLabel(node) : node.$id || (node.tagName ?? "div")}${inlineEditing &&
|
|
652
|
+
editingProp
|
|
653
|
+
? ` · ${editingProp}`
|
|
654
|
+
: ""}</span
|
|
650
655
|
>
|
|
651
656
|
|
|
652
657
|
${selection.length >= 2
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Chat-panel.ts — the persistent AI chat sidebar (`#chat-panel` shell region).
|
|
4
|
+
*
|
|
5
|
+
* Hosts the assistant UI from ai-panel.ts unconditionally: with no project (welcome screen), with a
|
|
6
|
+
* project but no open document, and with a document open. The panel is mounted once at studio boot
|
|
7
|
+
* and never tears down on tab switches — the assistant's module state and DOM (composer draft,
|
|
8
|
+
* scroll position) persist.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately NOT built on createPanelScheduler: ai-panel owns a focus-guard-free rAF render loop
|
|
11
|
+
* (streaming must repaint while the composer is focused). This module only provides the host
|
|
12
|
+
* container, the initial paint, and the pending-agent-prompt handoff; ai-panel's watcher drives all
|
|
13
|
+
* chat-state repaints through the same lit part cache.
|
|
14
|
+
*
|
|
15
|
+
* @license MIT
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { render as litRender } from "lit-html";
|
|
19
|
+
import { effect, effectScope } from "../reactivity";
|
|
20
|
+
import { applyPanelCollapse, view } from "../view";
|
|
21
|
+
import { workspace } from "../workspace/workspace";
|
|
22
|
+
import { consumePendingAgentPrompt, hasPendingAgentPrompt } from "../services/agent-seed";
|
|
23
|
+
import {
|
|
24
|
+
bindAiPanelHost,
|
|
25
|
+
mountAiPanel,
|
|
26
|
+
renderAiPanelTemplate,
|
|
27
|
+
seedAssistantPrompt,
|
|
28
|
+
} from "./ai-panel";
|
|
29
|
+
|
|
30
|
+
import type { EffectScope } from "@vue/reactivity";
|
|
31
|
+
|
|
32
|
+
let _host: HTMLElement | null = null;
|
|
33
|
+
let _container: HTMLElement | null = null;
|
|
34
|
+
let _scope: EffectScope | null = null;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Mount the chat sidebar into its shell region. Idempotent per host: the persistent `.panel-body`
|
|
38
|
+
* container is created once and bound as the ai-panel render host (lit needs a single render target
|
|
39
|
+
* for its part cache). A missing host (shell without a #chat-panel region, e.g. reduced test
|
|
40
|
+
* fixtures) is a no-op.
|
|
41
|
+
*
|
|
42
|
+
* @param {HTMLElement | null} host
|
|
43
|
+
*/
|
|
44
|
+
export function mount(host: HTMLElement | null) {
|
|
45
|
+
if (!host || (_host === host && _container)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
_host = host;
|
|
49
|
+
_container = document.createElement("div");
|
|
50
|
+
_container.className = "panel-body";
|
|
51
|
+
host.textContent = "";
|
|
52
|
+
host.append(_container);
|
|
53
|
+
|
|
54
|
+
mountAiPanel();
|
|
55
|
+
// The AI panel owns a focus-guard-free rAF render loop into this container so
|
|
56
|
+
// Streaming repaints while the composer is focused (see ai-panel.ts).
|
|
57
|
+
bindAiPanelHost(_container);
|
|
58
|
+
render();
|
|
59
|
+
|
|
60
|
+
_scope?.stop();
|
|
61
|
+
_scope = effectScope();
|
|
62
|
+
_scope.run(() => {
|
|
63
|
+
effect(() => {
|
|
64
|
+
// A pending agent prompt (stored by the New Project flow, possibly from another window) is
|
|
65
|
+
// Keyed by the absolute project root — consume it as soon as this window adopts that root.
|
|
66
|
+
const root = workspace.projectRoot;
|
|
67
|
+
if (!root || !hasPendingAgentPrompt(root)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (view.chatPanelCollapsed) {
|
|
71
|
+
view.chatPanelCollapsed = false;
|
|
72
|
+
applyPanelCollapse();
|
|
73
|
+
}
|
|
74
|
+
const prompt = consumePendingAgentPrompt(root);
|
|
75
|
+
if (prompt) {
|
|
76
|
+
// Defer past the current render so the assistant machinery is in place before the send.
|
|
77
|
+
requestAnimationFrame(() => void seedAssistantPrompt(prompt));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function unmount() {
|
|
84
|
+
_scope?.stop();
|
|
85
|
+
_scope = null;
|
|
86
|
+
if (_host) {
|
|
87
|
+
_host.textContent = "";
|
|
88
|
+
}
|
|
89
|
+
_host = null;
|
|
90
|
+
_container = null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Repaint the assistant template into the persistent container (no-op before mount). */
|
|
94
|
+
export function render() {
|
|
95
|
+
if (_container) {
|
|
96
|
+
litRender(renderAiPanelTemplate(), _container);
|
|
97
|
+
}
|
|
98
|
+
}
|