@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
@@ -0,0 +1,542 @@
1
+ /**
2
+ * Frontmatter-file grid sources — content collections and the pages tree.
3
+ *
4
+ * One row per entry file: rows load by enumerating a directory and parsing each file through the
5
+ * format registry (same path the editor tabs use); cells are the entry frontmatter. Commit is
6
+ * per-row with the same guards a tab save gets: a stale-on-disk check against the load-time text,
7
+ * and a skip when the file is open in a dirty tab (a clean open tab reloads after the write).
8
+ * Frontmatter re-serializes through the format's roundtrip capability — the same lossy-YAML
9
+ * semantics as saving a single tab. Inserts write a new file from the required Path cell; deletes
10
+ * remove files (never while a tab is open on them).
11
+ *
12
+ * Collections type their columns from the content-type schema (plus inferred extras found in the
13
+ * data); the pages grid is fully inference-driven.
14
+ */
15
+ import { getPlatform } from "../../platform";
16
+ import { projectState } from "../../store";
17
+ import { workspace } from "../../workspace/workspace";
18
+ import { isRecentLocal, markLocalMutation } from "../../files/fs-events";
19
+ import {
20
+ defaultContentFormat,
21
+ documentExtensions,
22
+ formatByName,
23
+ formatForPath,
24
+ formatSerialize,
25
+ loadFormats,
26
+ } from "../../format/format-host";
27
+ import { parseSourceForPath } from "../../files/file-ops";
28
+ import { columnsFromSchema, inferColumnsFromRows } from "../schema-columns";
29
+ import { makeGridTabId } from "../grid-source";
30
+ import type {
31
+ CommitResult,
32
+ GridCellValue,
33
+ GridColumn,
34
+ GridEditBatch,
35
+ GridRow,
36
+ GridRowsResult,
37
+ GridSource,
38
+ } from "../grid-source";
39
+ import type { ContentSectionEntry } from "../../types";
40
+
41
+ /** The synthetic identity column: the entry's project path. Editable only on pending inserts. */
42
+ export const PATH_FIELD = "__path";
43
+
44
+ interface EntryRecord {
45
+ frontmatter: Record<string, unknown>;
46
+ document: Record<string, unknown>;
47
+ rawText: string;
48
+ formatName: string;
49
+ }
50
+
51
+ interface CollectionInfo {
52
+ name: string;
53
+ def: ContentSectionEntry;
54
+ dir: string;
55
+ ext: string;
56
+ }
57
+
58
+ /** Resolve a content type's definition, source dir, and entry extension from project config. */
59
+ export function collectionInfo(typeName: string): CollectionInfo | null {
60
+ const content = (projectState?.projectConfig?.content ?? {}) as Record<
61
+ string,
62
+ ContentSectionEntry
63
+ >;
64
+ const def = content[typeName];
65
+ if (!def?.source) {
66
+ return null;
67
+ }
68
+ const dir = def.source.replace(/^\.\//, "").replace(/\/$/, "");
69
+ const ext =
70
+ def.format === "json"
71
+ ? ".json"
72
+ : (formatByName(def.format)?.extensions[0] ??
73
+ defaultContentFormat()?.extensions[0] ??
74
+ ".json");
75
+ return { def, dir, ext, name: typeName };
76
+ }
77
+
78
+ /** Directory-backed collections in the project (for "Edit collection in Grid" affordances). */
79
+ export function collectionDirs(): { name: string; dir: string }[] {
80
+ const content = (projectState?.projectConfig?.content ?? {}) as Record<
81
+ string,
82
+ ContentSectionEntry
83
+ >;
84
+ return Object.entries(content)
85
+ .filter(([, def]) => {
86
+ const source = def.source?.replace(/^\.\//, "") ?? "";
87
+ return source !== "" && !/\.[a-z0-9]+$/i.test(source);
88
+ })
89
+ .map(([name, def]) => ({ dir: def.source!.replace(/^\.\//, "").replace(/\/$/, ""), name }));
90
+ }
91
+
92
+ /** Recursively list entry files under a dir matching any of the extensions. */
93
+ async function listEntryFiles(dir: string, exts: string[]): Promise<string[]> {
94
+ const platform = getPlatform();
95
+ const results: string[] = [];
96
+ const walk = async (current: string) => {
97
+ let children;
98
+ try {
99
+ children = await platform.listDirectory(current);
100
+ } catch {
101
+ return; // Missing/unreadable directory — an empty source, not an error.
102
+ }
103
+ for (const entry of children) {
104
+ if (entry.type === "directory") {
105
+ await walk(entry.path);
106
+ } else if (exts.some((ext) => entry.path.endsWith(ext))) {
107
+ results.push(entry.path);
108
+ }
109
+ }
110
+ };
111
+ await walk(dir);
112
+ return results.toSorted();
113
+ }
114
+
115
+ /** Entry ids (file stems) of a collection — used by relationship cell editors. */
116
+ export async function listCollectionEntryIds(typeName: string): Promise<string[]> {
117
+ await loadFormats().catch(() => {}); // Extension resolution needs the format registry.
118
+ const info = collectionInfo(typeName);
119
+ if (!info) {
120
+ return [];
121
+ }
122
+ const files = await listEntryFiles(info.dir, [info.ext]);
123
+ return files.map((path) => {
124
+ const base = path.split("/").pop() ?? path;
125
+ return base.slice(0, base.length - info.ext.length);
126
+ });
127
+ }
128
+
129
+ /** Bounded-concurrency map that preserves item order. */
130
+ async function mapLimit<T, R>(
131
+ items: T[],
132
+ limit: number,
133
+ fn: (item: T) => Promise<R>,
134
+ ): Promise<R[]> {
135
+ const results: R[] = Array.from({ length: items.length });
136
+ let next = 0;
137
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
138
+ while (next < items.length) {
139
+ const index = next;
140
+ next += 1;
141
+ results[index] = await fn(items[index]!);
142
+ }
143
+ });
144
+ await Promise.all(workers);
145
+ return results;
146
+ }
147
+
148
+ /** Frontmatter value → grid cell value. Nested objects surface as read-only JSON text. */
149
+ function toCellValue(value: unknown): GridCellValue {
150
+ if (value === null || value === undefined) {
151
+ return null;
152
+ }
153
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
154
+ return value;
155
+ }
156
+ if (Array.isArray(value) && value.every((v) => typeof v !== "object" || v === null)) {
157
+ return value.map(String);
158
+ }
159
+ return JSON.stringify(value);
160
+ }
161
+
162
+ /** A tab open on this path, if any (workspace tabs key by id = path). */
163
+ function openTabFor(path: string) {
164
+ for (const tab of workspace.tabs.values()) {
165
+ if (tab.documentPath === path) {
166
+ return tab;
167
+ }
168
+ }
169
+ return null;
170
+ }
171
+
172
+ /** Whether every sampled value for a field was a genuine string (vs. stringified objects). */
173
+ function extraRowsAreStrings(rows: Record<string, GridCellValue>[], field: string): boolean {
174
+ return rows.every((row) => {
175
+ const value = row[field];
176
+ return value === null || value === undefined || typeof value !== "string"
177
+ ? true
178
+ : !value.startsWith("{") && !value.startsWith("[");
179
+ });
180
+ }
181
+
182
+ interface EntryFileSourceOptions {
183
+ id: string;
184
+ label: string;
185
+ /** List the entry file paths (called on every (re)load). */
186
+ list: () => Promise<string[]>;
187
+ /** Schema driving typed columns; null/undefined → columns are fully inferred. */
188
+ schema: () => ContentSectionEntry["schema"] | null | undefined;
189
+ /** Normalize a Path-cell value into a full project path for inserts. */
190
+ resolveInsertPath: (raw: string) => string;
191
+ /** Format used to serialize a NEW entry at this path; null blocks the insert. */
192
+ insertFormatName: (path: string) => string | null;
193
+ /** Inferred columns to front-load, in order (e.g. title/description for pages). */
194
+ priorityFields?: string[];
195
+ }
196
+
197
+ /** Shared engine for frontmatter-file sources (collections, pages). */
198
+ function createEntryFileSource(opts: EntryFileSourceOptions): GridSource {
199
+ const entries = new Map<string, EntryRecord>();
200
+ let loadPromise: Promise<void> | null = null;
201
+ let columnsCache: GridColumn[] | null = null;
202
+
203
+ const load = (force = false): Promise<void> => {
204
+ if (!force && loadPromise) {
205
+ return loadPromise;
206
+ }
207
+ loadPromise = (async () => {
208
+ await loadFormats().catch(() => {}); // Extension resolution needs the format registry.
209
+ const platform = getPlatform();
210
+ const files = await opts.list();
211
+ entries.clear();
212
+ columnsCache = null;
213
+ await mapLimit(files, 8, async (path) => {
214
+ try {
215
+ const text = await platform.readFile(path);
216
+ const parsed = await parseSourceForPath(path, text);
217
+ entries.set(path, {
218
+ document: parsed.document,
219
+ formatName: parsed.format.name,
220
+ frontmatter: parsed.frontmatter ?? {},
221
+ rawText: text,
222
+ });
223
+ } catch {
224
+ // Unparseable entry — leave it out rather than failing the whole grid.
225
+ }
226
+ });
227
+ })();
228
+ return loadPromise;
229
+ };
230
+
231
+ const buildColumns = (): GridColumn[] => {
232
+ if (columnsCache) {
233
+ return columnsCache;
234
+ }
235
+ const pathColumn: GridColumn = {
236
+ editable: false,
237
+ field: PATH_FIELD,
238
+ insertOnly: true,
239
+ kind: "string",
240
+ pk: true,
241
+ required: true,
242
+ title: "Path",
243
+ widthHint: 220,
244
+ };
245
+ const schemaColumns = columnsFromSchema(opts.schema());
246
+ const known = new Set([PATH_FIELD, ...schemaColumns.map((c) => c.field)]);
247
+
248
+ // Keys present in entry data but absent from the schema still get (inferred) columns.
249
+ const extraRows = [...entries.values()].map((entry) => {
250
+ const cells: Record<string, GridCellValue> = {};
251
+ for (const [key, value] of Object.entries(entry.frontmatter)) {
252
+ if (!known.has(key) && !key.startsWith("$")) {
253
+ cells[key] = toCellValue(value);
254
+ }
255
+ }
256
+ return cells;
257
+ });
258
+ const extraColumns = inferColumnsFromRows(extraRows);
259
+ for (const column of extraColumns) {
260
+ // Values we had to stringify (nested objects) must not round-trip as strings.
261
+ column.editable = column.kind !== "text" || extraRowsAreStrings(extraRows, column.field);
262
+ }
263
+ if (opts.priorityFields?.length) {
264
+ const rank = (c: GridColumn) => {
265
+ const index = opts.priorityFields!.indexOf(c.field);
266
+ return index === -1 ? opts.priorityFields!.length : index;
267
+ };
268
+ extraColumns.sort((a, b) => rank(a) - rank(b));
269
+ }
270
+
271
+ columnsCache = [pathColumn, ...schemaColumns, ...extraColumns];
272
+ return columnsCache;
273
+ };
274
+
275
+ const rowFor = (path: string, entry: EntryRecord): GridRow => {
276
+ const cells: Record<string, GridCellValue> = { [PATH_FIELD]: path };
277
+ for (const column of buildColumns()) {
278
+ if (column.field === PATH_FIELD) {
279
+ continue;
280
+ }
281
+ cells[column.field] = toCellValue(entry.frontmatter[column.field]);
282
+ }
283
+ return { cells, fingerprint: entry.rawText, key: path };
284
+ };
285
+
286
+ /** Re-serialize one entry with patched frontmatter and write it (same path as tab saves). */
287
+ const writeEntry = async (
288
+ path: string,
289
+ entry: EntryRecord,
290
+ patches: { field: string; value: GridCellValue }[],
291
+ ): Promise<void> => {
292
+ const platform = getPlatform();
293
+ const frontmatter = { ...entry.frontmatter };
294
+ for (const patch of patches) {
295
+ if (patch.value === null) {
296
+ delete frontmatter[patch.field];
297
+ } else {
298
+ frontmatter[patch.field] = patch.value;
299
+ }
300
+ }
301
+ const doc = entry.document;
302
+ const fullDoc = { ...frontmatter, ...doc, children: doc.children ?? [] };
303
+ const text = await formatSerialize(entry.formatName, fullDoc, { mode: "roundtrip" });
304
+ markLocalMutation(path);
305
+ await platform.writeFile(path, text);
306
+ entries.set(path, { ...entry, frontmatter, rawText: text });
307
+ };
308
+
309
+ return {
310
+ backingPaths: () => new Map([...entries.keys()].map((path) => [path, path])),
311
+ capabilities: { delete: true, insert: true, remotePaging: false, remoteSort: false },
312
+
313
+ async columns(): Promise<GridColumn[]> {
314
+ await load();
315
+ return buildColumns();
316
+ },
317
+
318
+ async commit(batch: GridEditBatch): Promise<CommitResult> {
319
+ await load();
320
+ const platform = getPlatform();
321
+ const result: CommitResult = { cells: [], deletes: [], inserts: [] };
322
+
323
+ // Cell edits, grouped per file so each entry is read/checked/written once.
324
+ const byRow = new Map<string, { field: string; value: GridCellValue }[]>();
325
+ for (const cell of batch.cells) {
326
+ const list = byRow.get(cell.rowKey) ?? [];
327
+ list.push({ field: cell.field, value: cell.value });
328
+ byRow.set(cell.rowKey, list);
329
+ }
330
+ for (const [path, patches] of byRow) {
331
+ const outcome = (ok: boolean, error?: string, stale?: boolean) => {
332
+ for (const patch of patches) {
333
+ result.cells.push({ error, field: patch.field, ok, rowKey: path, stale });
334
+ }
335
+ };
336
+ const entry = entries.get(path);
337
+ if (!entry) {
338
+ outcome(false, "Entry no longer loaded — refresh the grid");
339
+ continue;
340
+ }
341
+ const openTab = openTabFor(path);
342
+ if (openTab?.doc.dirty) {
343
+ outcome(false, "Open in a tab with unsaved changes — save or close it first");
344
+ continue;
345
+ }
346
+ let onDisk: string | null = null;
347
+ try {
348
+ onDisk = await platform.readFile(path);
349
+ } catch {
350
+ onDisk = null;
351
+ }
352
+ if (onDisk !== entry.rawText && !isRecentLocal(path)) {
353
+ outcome(false, "File changed on disk — refresh to reload", true);
354
+ continue;
355
+ }
356
+ try {
357
+ await writeEntry(path, entry, patches);
358
+ outcome(true);
359
+ if (openTab) {
360
+ // Clean open tab: reload it so the editor reflects the grid's write. Dynamic import —
361
+ // Files.ts imports the grid modules, so a static import here would be a cycle.
362
+ const { reloadFileInTab } = await import("../../files/files");
363
+ void reloadFileInTab(path);
364
+ }
365
+ } catch (error) {
366
+ outcome(false, error instanceof Error ? error.message : String(error));
367
+ }
368
+ }
369
+
370
+ // Inserts: new entry files from the Path cell.
371
+ for (const insert of batch.inserts) {
372
+ const rawPath = insert.cells[PATH_FIELD];
373
+ if (typeof rawPath !== "string" || rawPath.trim() === "") {
374
+ result.inserts.push({ error: "Path is required", ok: false, tempKey: insert.tempKey });
375
+ continue;
376
+ }
377
+ const path = opts.resolveInsertPath(rawPath);
378
+ let exists = entries.has(path);
379
+ if (!exists) {
380
+ try {
381
+ await platform.readFile(path);
382
+ exists = true;
383
+ } catch {
384
+ exists = false;
385
+ }
386
+ }
387
+ if (exists) {
388
+ result.inserts.push({
389
+ error: `${path} already exists`,
390
+ ok: false,
391
+ tempKey: insert.tempKey,
392
+ });
393
+ continue;
394
+ }
395
+ const formatName = opts.insertFormatName(path);
396
+ if (!formatName) {
397
+ result.inserts.push({
398
+ error: `No format can serialize ${path}`,
399
+ ok: false,
400
+ tempKey: insert.tempKey,
401
+ });
402
+ continue;
403
+ }
404
+ try {
405
+ const frontmatter: Record<string, unknown> = {};
406
+ for (const [field, value] of Object.entries(insert.cells)) {
407
+ if (field !== PATH_FIELD && value !== null) {
408
+ frontmatter[field] = value;
409
+ }
410
+ }
411
+ const text = await formatSerialize(
412
+ formatName,
413
+ { ...frontmatter, children: [] },
414
+ { frontmatter: Object.keys(frontmatter).length > 0, mode: "roundtrip" },
415
+ );
416
+ markLocalMutation(path);
417
+ await platform.writeFile(path, text);
418
+ const parsed = await parseSourceForPath(path, text);
419
+ entries.set(path, {
420
+ document: parsed.document,
421
+ formatName: parsed.format.name,
422
+ frontmatter: parsed.frontmatter ?? {},
423
+ rawText: text,
424
+ });
425
+ result.inserts.push({ newKey: path, ok: true, tempKey: insert.tempKey });
426
+ } catch (error) {
427
+ result.inserts.push({
428
+ error: error instanceof Error ? error.message : String(error),
429
+ ok: false,
430
+ tempKey: insert.tempKey,
431
+ });
432
+ }
433
+ }
434
+
435
+ // Deletes: remove entry files (never while a tab is open on them).
436
+ for (const del of batch.deletes) {
437
+ if (openTabFor(del.rowKey)) {
438
+ result.deletes.push({
439
+ error: "Open in a tab — close it first",
440
+ ok: false,
441
+ rowKey: del.rowKey,
442
+ });
443
+ continue;
444
+ }
445
+ try {
446
+ markLocalMutation(del.rowKey);
447
+ await platform.deleteFile(del.rowKey);
448
+ entries.delete(del.rowKey);
449
+ result.deletes.push({ ok: true, rowKey: del.rowKey });
450
+ } catch (error) {
451
+ result.deletes.push({
452
+ error: error instanceof Error ? error.message : String(error),
453
+ ok: false,
454
+ rowKey: del.rowKey,
455
+ });
456
+ }
457
+ }
458
+
459
+ return result;
460
+ },
461
+
462
+ id: opts.id,
463
+ label: opts.label,
464
+
465
+ async refresh(): Promise<void> {
466
+ await load(true);
467
+ },
468
+
469
+ async rows(): Promise<GridRowsResult> {
470
+ await load();
471
+ const rows = [...entries.entries()].map(([path, entry]) => rowFor(path, entry));
472
+ return { rows, total: rows.length };
473
+ },
474
+ };
475
+ }
476
+
477
+ /** Create the grid source for one content collection. */
478
+ export function createCollectionSource(typeName: string): GridSource {
479
+ const requireInfo = (): CollectionInfo => {
480
+ const info = collectionInfo(typeName);
481
+ if (!info) {
482
+ throw new Error(`No content collection named "${typeName}" in project.json`);
483
+ }
484
+ return info;
485
+ };
486
+
487
+ return createEntryFileSource({
488
+ id: makeGridTabId({ kind: "collection", name: typeName }),
489
+ insertFormatName: (path) => {
490
+ const info = requireInfo();
491
+ return (
492
+ (info.def.format ? formatByName(info.def.format)?.name : undefined) ??
493
+ formatForPath(path)?.name ??
494
+ defaultContentFormat()?.name ??
495
+ null
496
+ );
497
+ },
498
+ label: typeName,
499
+ list: () => {
500
+ const info = requireInfo();
501
+ return listEntryFiles(info.dir, [info.ext]);
502
+ },
503
+ resolveInsertPath: (raw) => {
504
+ const info = requireInfo();
505
+ let path = raw.trim().replace(/^\.\//, "").replace(/^\//, "");
506
+ if (!path.includes("/")) {
507
+ path = `${info.dir}/${path}`;
508
+ }
509
+ if (!path.endsWith(info.ext)) {
510
+ path = `${path}${info.ext}`;
511
+ }
512
+ return path;
513
+ },
514
+ schema: () => requireInfo().def.schema,
515
+ });
516
+ }
517
+
518
+ /** Create the grid source for the pages tree (format-class pages; .json pages are excluded). */
519
+ export function createPagesSource(): GridSource {
520
+ const pageExts = () => {
521
+ const exts = documentExtensions("page").filter((ext) => ext !== ".json");
522
+ return exts.length > 0 ? exts : [".md"];
523
+ };
524
+ return createEntryFileSource({
525
+ id: makeGridTabId({ kind: "pages" }),
526
+ insertFormatName: (path) => formatForPath(path)?.name ?? null,
527
+ label: "Pages",
528
+ list: () => listEntryFiles("pages", pageExts()),
529
+ priorityFields: ["title", "description"],
530
+ resolveInsertPath: (raw) => {
531
+ let path = raw.trim().replace(/^\.\//, "").replace(/^\//, "");
532
+ if (!path.startsWith("pages/")) {
533
+ path = `pages/${path}`;
534
+ }
535
+ if (!pageExts().some((ext) => path.endsWith(ext))) {
536
+ path = `${path}${pageExts()[0]}`;
537
+ }
538
+ return path;
539
+ },
540
+ schema: () => null,
541
+ });
542
+ }