editor-ts 0.0.10 → 0.0.12

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/src/types.ts CHANGED
@@ -2,29 +2,185 @@
2
2
  * Core type definitions for the HTML content editing library
3
3
  */
4
4
 
5
+ import type { OpencodeClient, ServerOptions, createOpencode } from '@opencode-ai/sdk';
6
+ import type { Page } from './core/Page';
7
+ import type { StorageAdapter, StorageManager, SqlocalClient } from './core/StorageManager';
8
+
9
+ export type JsonPrimitive = string | number | boolean | null;
10
+ export type JsonValue = JsonPrimitive | JsonObject | JsonValue[] | Component;
11
+ export type JsonObject = { [key: string]: JsonValue };
12
+
5
13
  export interface PageData {
6
14
  title: string;
7
15
  item_id: number;
8
16
  body: PageBody;
9
17
  }
10
18
 
19
+ export interface MultiPageData {
20
+ pages: PageData[];
21
+ activePageIndex?: number;
22
+ }
23
+
24
+ export type PagePayload = PageData | MultiPageData | string;
25
+
26
+ export type AiProviderType = 'disabled' | 'opencode';
27
+ export type AiProviderMode = 'client' | 'client+server';
28
+
29
+ export type OpencodeServer = Awaited<ReturnType<typeof createOpencode>>['server'];
30
+
31
+ export interface OpencodeAiProviderConfig {
32
+ provider: 'opencode';
33
+
34
+ /**
35
+ * Optional: stream assistant output via server-sent events.
36
+ *
37
+ * When enabled, `editor.ai.chat()` can stream partial output via `options.onStream`.
38
+ */
39
+ stream?: {
40
+ enabled?: boolean;
41
+ };
42
+
43
+ /**
44
+ * Optional HTTP Basic Auth for password-protected opencode servers.
45
+ * Username defaults to "opencode" on the server if not provided.
46
+ */
47
+ auth?: {
48
+ username?: string;
49
+ password: string;
50
+ };
51
+
52
+ /**
53
+ * Use your own SDK client instance.
54
+ *
55
+ * This is useful when your app already created a client via:
56
+ * `createOpencodeClient({ baseUrl })`
57
+ * or
58
+ * `const { client } = await createOpencode()`
59
+ */
60
+ client?: OpencodeClient;
61
+
62
+ /**
63
+ * Optional server instance for `getUrl()`.
64
+ * If you pass this, EditorTs will NOT manage its lifecycle.
65
+ */
66
+ server?: OpencodeServer;
67
+
68
+ /**
69
+ * - 'client': connect to an existing opencode server via baseUrl
70
+ * - 'client+server': start a server and create a client
71
+ */
72
+ mode?: AiProviderMode;
73
+
74
+ // Client-only mode
75
+ baseUrl?: string;
76
+
77
+ // Client+server mode
78
+ hostname?: ServerOptions['hostname'];
79
+ port?: ServerOptions['port'];
80
+ signal?: ServerOptions['signal'];
81
+ timeout?: ServerOptions['timeout'];
82
+
83
+ // opencode config overrides
84
+ config?: ServerOptions['config'];
85
+ }
86
+
87
+ export type AiProviderConfig =
88
+ | { provider?: 'disabled' }
89
+ | OpencodeAiProviderConfig;
90
+
91
+ export type EditorTsAiChatReplacement = {
92
+ path: string;
93
+ content: string;
94
+ };
95
+
96
+ export type EditorTsAiChatSession = {
97
+ id: string;
98
+ title?: string;
99
+ };
100
+
101
+ export type EditorTsAiChatResult = {
102
+ replacements: EditorTsAiChatReplacement[];
103
+ rawText: string;
104
+
105
+ /** Session that produced this response (for reuse/persistence). */
106
+ sessionId: string;
107
+ };
108
+
109
+ export interface EditorTsAiModule {
110
+ provider: 'opencode';
111
+ mode: AiProviderMode;
112
+
113
+ /** Lazily resolves to an opencode client */
114
+ getClient(): Promise<OpencodeClient>;
115
+
116
+ /** Returns current server URL/baseUrl if known */
117
+ getUrl(): string | null;
118
+
119
+ /**
120
+ * Request full-file replacements from AI.
121
+ * If a session is selected, prompts reuse that session.
122
+ *
123
+ * If streaming is enabled, `onStream` receives incremental text deltas.
124
+ */
125
+ chat(
126
+ prompt: string,
127
+ options?: {
128
+ sessionId?: string;
129
+ model?: {
130
+ providerID: string;
131
+ modelID: string;
132
+ };
133
+ stream?: boolean;
134
+ onStream?: (delta: string) => void;
135
+ }
136
+ ): Promise<EditorTsAiChatResult>;
137
+
138
+ /** Apply replacements to the current page */
139
+ apply(replacements: EditorTsAiChatReplacement[]): Promise<void>;
140
+
141
+ /** AI session management (persisted via StorageManager) */
142
+ sessions: {
143
+ current(): string | null;
144
+ setCurrent(sessionId: string | null): Promise<void>;
145
+ list(): Promise<EditorTsAiChatSession[]>;
146
+ create(title?: string): Promise<EditorTsAiChatSession>;
147
+ };
148
+
149
+ /** Optional model selector data. */
150
+ models: {
151
+ list(): Promise<Array<{ providerID: string; modelID: string }>>;
152
+ };
153
+
154
+ /** Closes embedded server if started */
155
+ close(): Promise<void>;
156
+ }
157
+
11
158
  export interface PageBody {
12
- html: string;
13
- components: string; // JSON string of Component[]
14
- assets: Asset[];
15
- css: string;
16
- styles: Style[];
159
+ //NOTE: only need one of these
160
+ html?: string;
161
+ components?: string | Component[]; // JSON string of Component[]
162
+ assets?: Asset[];
163
+ //NOTE: only need one of these
164
+ css?: string;
165
+ styles?: Style[];
17
166
  }
18
167
 
168
+ export type ComponentAttributes = JsonObject & {
169
+ id?: string;
170
+ class?: string;
171
+ src?: string;
172
+ };
173
+
19
174
  export interface Component {
20
175
  type: string;
21
- attributes?: Record<string, any>;
176
+ attributes?: ComponentAttributes;
22
177
  components?: Component[];
23
178
  tagName?: string;
24
179
  void?: boolean;
25
180
  style?: string;
26
181
  script?: string;
27
- [key: string]: any;
182
+ content?: string; // Text content for the component
183
+ [key: string]: JsonValue | undefined;
28
184
  }
29
185
 
30
186
  export interface ToolbarConfig {
@@ -74,7 +230,7 @@ export interface ParsedComponents {
74
230
  export interface ComponentQuery {
75
231
  id?: string;
76
232
  type?: string;
77
- attributes?: Record<string, any>;
233
+ attributes?: Record<string, JsonValue>;
78
234
  tagName?: string;
79
235
  }
80
236
 
@@ -94,23 +250,94 @@ export interface ToolbarRule {
94
250
  config: ToolbarConfig;
95
251
  }
96
252
 
97
- export type ComponentSelector =
253
+ export type ComponentSelector =
98
254
  | { id: string }
99
255
  | { type: string }
100
256
  | { tagName: string }
101
- | { attributes: Record<string, any> }
257
+ | { attributes: Record<string, JsonValue> }
102
258
  | { custom: (component: Component) => boolean };
103
259
 
260
+ export interface EditorTsSyncMessage {
261
+ type: 'page';
262
+ key?: string;
263
+ payload: PagePayload;
264
+ sentAt: string;
265
+ }
266
+
267
+ export interface EditorTsSyncAck {
268
+ type: 'ack';
269
+ messageId: string;
270
+ receivedAt: string;
271
+ }
272
+
273
+ export type EditorTsSyncEnvelope = EditorTsSyncMessage | EditorTsSyncAck;
274
+
275
+ export type CustomComponentDefinition = {
276
+ /** Unique type identifier for this component (e.g. 'hero', 'button'). */
277
+ type: string;
278
+
279
+ /** Display name for UI (optional). */
280
+ label?: string;
281
+
282
+ /** Optional SVG icon (raw <svg>...</svg> markup). */
283
+ iconSvg?: string;
284
+
285
+ /**
286
+ * Create a default component JSON object.
287
+ *
288
+ * This should return clean JSON-only component data.
289
+ */
290
+ factory: () => Component;
291
+ };
292
+
293
+ export type CustomComponentRegistry = Record<string, CustomComponentDefinition>;
294
+
295
+ export type UiRender<Props> = (props: Props) => string | void;
296
+
297
+ export type PagesRenderProps = {
298
+ container: HTMLElement;
299
+ pages: PageData[];
300
+ activePageIndex: number;
301
+ onSelect: (index: number) => void;
302
+ };
303
+
104
304
  export interface InitConfig {
105
305
  // Required: iframe element ID (user creates this in their HTML)
106
306
  iframeId: string;
107
-
307
+
308
+ // Optional: vim mode defaults to false
309
+ vimMode?: boolean;
310
+
311
+ /** Optional: Version control / undo-redo history (runtime config, persisted separately). */
312
+ versionControl?: {
313
+ enabled?: boolean;
314
+ maxSnapshots?: number;
315
+ };
316
+
108
317
  // Required: page data
109
- data: PageData | string;
110
-
318
+ data: PagePayload;
319
+
320
+ /** Optional: load initial data from storage. */
321
+ initialStorageKey?: string;
322
+
323
+ /** Optional: auto-save configuration (runtime only). */
324
+ autoSave?: {
325
+ /** Enable auto-save (default: false). */
326
+ enabled?: boolean;
327
+
328
+ /** Save after this many edits (default: 1). */
329
+ everyEdits?: number;
330
+
331
+ /** Optional storage key; otherwise uses last saveTo/loadFrom key. */
332
+ key?: string;
333
+ };
334
+
111
335
  // Optional: toolbar configuration (runtime only)
112
336
  toolbars?: ToolbarInitConfig;
113
-
337
+
338
+ // Optional: custom component registry
339
+ customComponents?: CustomComponentRegistry;
340
+
114
341
  // Optional: UI container IDs (user controls placement)
115
342
  ui?: {
116
343
  sidebar?: {
@@ -125,13 +352,309 @@ export interface InitConfig {
125
352
  containerId?: string; // Where to render selected component info (optional)
126
353
  enabled?: boolean;
127
354
  };
355
+ layers?: {
356
+ containerId?: string; // Where to render layer panel (optional)
357
+ enabled?: boolean;
358
+ };
359
+
360
+ /** Optional: multipage switcher UI */
361
+ pages?: {
362
+ containerId?: string; // Where to render page dropdown
363
+ enabled?: boolean;
364
+ /** Optional: custom render for page dropdown. */
365
+ render?: UiRender<PagesRenderProps>;
366
+ };
367
+
368
+ // Optional: component palette (click-to-place)
369
+ componentPalette?: {
370
+ containerId?: string;
371
+ enabled?: boolean;
372
+ };
373
+
374
+ /** Optional: AI chat UI bindings (expand/collapse + controls). */
375
+ aiChat?: {
376
+ /** Root element to receive dataset/class toggles. Defaults to containerId if omitted. */
377
+ rootId?: string;
378
+
379
+ /** Expand/collapse button id. */
380
+ expandButtonId?: string;
381
+
382
+ /** Optional: set initial expanded state. */
383
+ defaultExpanded?: boolean;
384
+
385
+ /** Optional: set a class on the root when expanded. */
386
+ expandedClassName?: string;
387
+
388
+ /** Optional: set a class on the root when collapsed. */
389
+ collapsedClassName?: string;
390
+
391
+ /** AI chat input textarea id. */
392
+ inputId?: string;
393
+
394
+ /** Send button id. */
395
+ sendButtonId?: string;
396
+
397
+ /** Optional manual apply button id (fallback when auto-apply fails/disabled). */
398
+ applyButtonId?: string;
399
+
400
+ /** Chat log container id. */
401
+ logId?: string;
402
+
403
+ /** Optional sessions dropdown id. */
404
+ sessionSelectId?: string;
405
+
406
+ /** Optional model selector dropdown id. */
407
+ modelSelectId?: string;
408
+
409
+ /** Optional create-session button id. */
410
+ sessionNewButtonId?: string;
411
+
412
+ /** Optional health-check button id. */
413
+ healthButtonId?: string;
414
+
415
+ /** Optional health-check status container id. */
416
+ healthStatusId?: string;
417
+
418
+ /** Optional: baseUrl input id (used only when aiProvider is opencode client mode). */
419
+ baseUrlInputId?: string;
420
+
421
+ /** Optional: enable auto-apply for chat results. Default: true. */
422
+ autoApply?: boolean;
423
+
424
+ /** Optional: override streaming for this chat UI. */
425
+ stream?: {
426
+ enabled?: boolean;
427
+ };
428
+
429
+ /** Optional: external link to OpenCode web chat UI. */
430
+ link?: {
431
+ /** Anchor element id for the external-link button. */
432
+ anchorId?: string;
433
+
434
+ /** Optional: override the URL path appended to the base URL. */
435
+ path?: string;
436
+
437
+ enabled?: boolean;
438
+ };
439
+
440
+ enabled?: boolean;
441
+ };
442
+ /** Optional: auto-save progress UI. */
443
+ autoSave?: {
444
+ /** Progress bar element id. */
445
+ progressBarId?: string;
446
+ enabled?: boolean;
447
+ };
448
+
449
+ /** Optional: command palette UI (Ctrl/Cmd+K). */
450
+ commandPalette?: {
451
+ /** Root modal container id. */
452
+ containerId?: string;
453
+ /** Search input id. */
454
+ inputId?: string;
455
+ /** Results list container id. */
456
+ resultsId?: string;
457
+ /** Optional: close button id. */
458
+ closeButtonId?: string;
459
+ /** Optional: hint element id (for status text). */
460
+ hintId?: string;
461
+ /** Optional: custom items to show in the palette. */
462
+ items?: Array<{
463
+ title: string;
464
+ action: () => void | Promise<void>;
465
+ type?: 'component' | 'command';
466
+ }>;
467
+ /** Optional: command palette shortcuts (runtime only). */
468
+ shortcuts?: ShortcutDefinition[];
469
+ /** Enable palette UI (default true when IDs provided). */
470
+ enabled?: boolean;
471
+ };
472
+
473
+ editors?: {
474
+ files?: {
475
+ containerId?: string; // Where to render workspace file list
476
+ enabled?: boolean;
477
+ };
478
+ viewer?: {
479
+ containerId?: string; // Where to render read-only file preview
480
+ enabled?: boolean;
481
+ };
482
+ js?: {
483
+ containerId?: string; // Where to render component JS editor
484
+ enabled?: boolean;
485
+ };
486
+ css?: {
487
+ containerId?: string; // Where to render page CSS editor
488
+ enabled?: boolean;
489
+ };
490
+ json?: {
491
+ containerId?: string; // Where to render page JSON editor
492
+ enabled?: boolean;
493
+ };
494
+ jsx?: {
495
+ containerId?: string; // Where to render JSX/TSX view
496
+ enabled?: boolean;
497
+ };
498
+ };
499
+
500
+ /**
501
+ * Optional: wire UI tabs to toggle between canvas (iframe) and code panels.
502
+ *
503
+ * This does not create any UI; it only attaches click handlers to your
504
+ * existing buttons and toggles visibility/dataset state.
505
+ */
506
+ viewTabs?: {
507
+ editorButtonId?: string;
508
+ codeButtonId?: string;
509
+ defaultView?: 'editor' | 'code';
510
+ };
511
+
512
+ /**
513
+ * Optional: tabs within the code view (JS/CSS/JSON/JSX).
514
+ *
515
+ * This does not create any UI; it only wires existing buttons and toggles
516
+ * visibility of the editor containers.
517
+ */
518
+ codeTabs?: {
519
+ defaultTab?: 'files' | 'viewer' | 'js' | 'css' | 'json' | 'jsx';
520
+ filesButtonId?: string;
521
+ viewerButtonId?: string;
522
+ jsButtonId?: string;
523
+ cssButtonId?: string;
524
+ jsonButtonId?: string;
525
+ jsxButtonId?: string;
526
+ };
527
+ };
528
+
529
+ // Optional: built-in code editor provider
530
+ // - 'textarea' (default): lightweight, zero deps
531
+ // - 'modern-monaco': advanced editor (requires optional peer dependency)
532
+ codeEditor?: {
533
+ provider?: 'textarea' | 'modern-monaco';
534
+
535
+ /**
536
+ * When using modern-monaco, enable a workspace-backed virtual filesystem.
537
+ *
538
+ * This makes editor models behave like real files and is the basis for
539
+ * later handing a file tree to AI/codegen.
540
+ */
541
+ workspace?: {
542
+ enabled?: boolean;
543
+ name?: string;
544
+ };
545
+ };
546
+
547
+ // Optional: AI provider integration
548
+ // - 'disabled' (default): no AI integration
549
+ // - 'opencode': integrates with @opencode-ai/sdk
550
+ aiProvider?: AiProviderConfig;
551
+
552
+ /** Optional: keyboard shortcut definitions. */
553
+ shortcuts?: ShortcutDefinition[];
554
+
555
+ /** Optional: shortcut behavior configuration. */
556
+ shortcutConfig?: {
557
+ /** Which modifier key "mod" should map to (default: 'ctrl'). */
558
+ modKey?: 'ctrl' | 'meta' | 'alt';
128
559
  };
129
-
560
+
130
561
  // Optional: event callbacks
131
562
  onComponentSelect?: (component: Component) => void;
132
563
  onComponentEdit?: (component: Component) => void;
133
564
  onComponentDelete?: (component: Component) => void;
134
565
  onComponentDuplicate?: (component: Component, duplicate: Component) => void;
566
+
567
+ // Text editing callbacks
568
+ onTextEditStart?: (component: Component) => void;
569
+ onTextUpdate?: (component: Component, newContent: string, originalContent: string) => void;
570
+ onTextEditEnd?: (component: Component, saved: boolean) => void;
571
+
572
+ // Image editing callbacks
573
+ onImageEditStart?: (component: Component, currentSrc: string) => void;
574
+ onImageUpdate?: (component: Component, newSrc: string, originalSrc: string, fileInfo: ImageFileInfo) => void;
575
+ onImageEditEnd?: (component: Component, saved: boolean) => void;
576
+
577
+ // Optional: storage configuration
578
+ storage?: StorageConfig;
579
+ }
580
+
581
+ export type ShortcutDefinition = {
582
+ key: string;
583
+ action: () => void | Promise<void>;
584
+ };
585
+
586
+ export interface ImageFileInfo {
587
+ fileName: string;
588
+ fileType: string;
589
+ fileSize: number;
590
+ }
591
+
592
+ // Storage types (imported from StorageManager)
593
+ export interface LocalStorageConfig {
594
+ /**
595
+ * Local storage is the default when `storage` is omitted.
596
+ *
597
+ * This field is optional to allow concise configs like:
598
+ * { prefix: 'myapp_' }
599
+ */
600
+ type?: 'local';
601
+ prefix?: string;
602
+ }
603
+
604
+ export interface RemoteStorageConfig {
605
+ type: 'remote';
606
+ baseUrl: string;
607
+ imageUploadMethod?: 'form' | 'json';
608
+ headers?: Record<string, string>;
609
+ endpoints?: {
610
+ savePage?: string;
611
+ loadPage?: string;
612
+ deletePage?: string;
613
+ uploadImage?: string;
614
+ deleteImage?: string;
615
+ listPages?: string;
616
+ };
617
+ }
618
+
619
+ export interface SqlocalStorageConfig {
620
+ type: 'sqlocal';
621
+ /** SQLite database file name stored in OPFS (used when `client` is not provided). */
622
+ databaseName?: string;
623
+ /** Pre-initialized SQLocal client (avoids dynamic import). */
624
+ client?: SqlocalClient;
625
+ }
626
+
627
+ export type StorageConfig = LocalStorageConfig | RemoteStorageConfig | SqlocalStorageConfig;
628
+
629
+ export interface ServerPageMeta {
630
+ key: string;
631
+ updatedAt: number;
632
+ checksum?: string;
633
+ }
634
+
635
+ export interface ServerFile {
636
+ path: string;
637
+ content: string;
638
+ }
639
+
640
+ export interface ServerSyncAdapter {
641
+ listPages(): Promise<ServerPageMeta[]>;
642
+ listFiles(pageKey: string): Promise<ServerFile[]>;
643
+ saveFiles(pageKey: string, files: ServerFile[]): Promise<void>;
644
+ }
645
+
646
+ export type FrontendSyncStatus =
647
+ | { state: 'loading' }
648
+ | { state: 'saving' }
649
+ | { state: 'idle' }
650
+ | { state: 'error'; message: string };
651
+
652
+ export interface FrontendSyncOptions {
653
+ pageKey: string;
654
+ storage: StorageAdapter;
655
+ adapter: ServerSyncAdapter;
656
+ includeFiles?: (path: string) => boolean;
657
+ onStatus?: (status: FrontendSyncStatus) => void;
135
658
  }
136
659
 
137
660
  export interface ToolbarInitConfig {
@@ -141,12 +664,62 @@ export interface ToolbarInitConfig {
141
664
  default?: ToolbarConfig;
142
665
  }
143
666
 
667
+ export interface EditorTsEventMap {
668
+ componentSelect: [component: Component];
669
+ componentInsert: [component: Component, parentId: string | null];
670
+ componentReorder: [component: Component, newParentId: string | null, newIndex: number];
671
+
672
+ componentEdit: [component: Component];
673
+ componentEditJS: [component: Component];
674
+ componentDuplicate: [original: Component, duplicate: Component];
675
+ componentDelete: [component: Component];
676
+
677
+ pageEditCSS: [body: PageBody];
678
+ pageEditJSON: [body: PageBody];
679
+
680
+ pageSaved: [key: string];
681
+ pageLoaded: [key: string];
682
+
683
+ textEditStart: [component: Component];
684
+ textUpdate: [component: Component, newContent: string, originalContent: string];
685
+ textEditEnd: [component: Component, saved: boolean];
686
+
687
+ imageEditStart: [component: Component, currentSrc: string];
688
+ imageUpdate: [component: Component, newSrc: string, originalSrc: string, fileInfo: ImageFileInfo];
689
+ imageEditEnd: [component: Component, saved: boolean];
690
+ }
691
+
692
+ export type EditorTsEventName = keyof EditorTsEventMap;
693
+
144
694
  export interface EditorTsEditor {
145
- page: any; // Page class (avoid circular dependency)
146
- on(event: string, callback: Function): void;
147
- off(event: string, callback: Function): void;
695
+ page: Page;
696
+ storage: StorageManager;
697
+ ai?: EditorTsAiModule;
698
+ vimMode: boolean;
699
+ versionControl?: {
700
+ enabled: boolean;
701
+ canUndo(): boolean;
702
+ canRedo(): boolean;
703
+ undo(): Promise<boolean>;
704
+ redo(): Promise<boolean>;
705
+ commit(meta?: { source?: 'user' | 'ai' | 'system'; message?: string }): Promise<void>;
706
+ };
707
+ components: CustomComponentRegistry;
708
+ workspace?: {
709
+ name: string;
710
+ listFiles(): string[];
711
+ readFile(path: string): Promise<string | null>;
712
+ writeFile(path: string, content: string): Promise<void>;
713
+ openFile(path: string): Promise<void>;
714
+ };
715
+ on<K extends EditorTsEventName>(event: K, callback: (...args: EditorTsEventMap[K]) => void): void;
716
+ off<K extends EditorTsEventName>(event: K, callback: (...args: EditorTsEventMap[K]) => void): void;
148
717
  refresh(): void;
149
718
  save(): string;
719
+ /** Save page to storage */
720
+ saveTo(key: string): Promise<void>;
721
+ /** Load page from storage */
722
+ loadFrom(key: string): Promise<boolean>;
150
723
  destroy(): void;
151
724
  elements: {
152
725
  iframe: HTMLIFrameElement;
@@ -20,7 +20,21 @@ export const defaultToolbarActions: ToolbarAction[] = [
20
20
  label: 'Edit JS',
21
21
  icon: '📜',
22
22
  enabled: true,
23
- description: 'Edit component JavaScript with Monaco editor',
23
+ description: 'Edit component JavaScript',
24
+ },
25
+ {
26
+ id: 'editCSS',
27
+ label: 'Edit CSS',
28
+ icon: '🎨',
29
+ enabled: true,
30
+ description: 'Edit page CSS',
31
+ },
32
+ {
33
+ id: 'editJSON',
34
+ label: 'Edit JSON',
35
+ icon: '🧱',
36
+ enabled: true,
37
+ description: 'View/edit full page JSON structure',
24
38
  },
25
39
  {
26
40
  id: 'duplicate',