@upstart.gg/vite-plugins 0.0.37

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 (58) hide show
  1. package/dist/vite-plugin-upstart-attrs.d.ts +29 -0
  2. package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -0
  3. package/dist/vite-plugin-upstart-attrs.js +323 -0
  4. package/dist/vite-plugin-upstart-attrs.js.map +1 -0
  5. package/dist/vite-plugin-upstart-editor/plugin.d.ts +15 -0
  6. package/dist/vite-plugin-upstart-editor/plugin.d.ts.map +1 -0
  7. package/dist/vite-plugin-upstart-editor/plugin.js +55 -0
  8. package/dist/vite-plugin-upstart-editor/plugin.js.map +1 -0
  9. package/dist/vite-plugin-upstart-editor/runtime/click-handler.d.ts +12 -0
  10. package/dist/vite-plugin-upstart-editor/runtime/click-handler.d.ts.map +1 -0
  11. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +57 -0
  12. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -0
  13. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.d.ts +12 -0
  14. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.d.ts.map +1 -0
  15. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js +91 -0
  16. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js.map +1 -0
  17. package/dist/vite-plugin-upstart-editor/runtime/index.d.ts +22 -0
  18. package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -0
  19. package/dist/vite-plugin-upstart-editor/runtime/index.js +62 -0
  20. package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -0
  21. package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts +15 -0
  22. package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts.map +1 -0
  23. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js +292 -0
  24. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js.map +1 -0
  25. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +126 -0
  26. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -0
  27. package/dist/vite-plugin-upstart-editor/runtime/types.js +1 -0
  28. package/dist/vite-plugin-upstart-editor/runtime/utils.d.ts +15 -0
  29. package/dist/vite-plugin-upstart-editor/runtime/utils.d.ts.map +1 -0
  30. package/dist/vite-plugin-upstart-editor/runtime/utils.js +26 -0
  31. package/dist/vite-plugin-upstart-editor/runtime/utils.js.map +1 -0
  32. package/dist/vite-plugin-upstart-theme.d.ts +22 -0
  33. package/dist/vite-plugin-upstart-theme.d.ts.map +1 -0
  34. package/dist/vite-plugin-upstart-theme.js +179 -0
  35. package/dist/vite-plugin-upstart-theme.js.map +1 -0
  36. package/package.json +63 -0
  37. package/src/tests/fixtures/routes/default-layout.tsx +10 -0
  38. package/src/tests/fixtures/routes/dynamic-route.tsx +10 -0
  39. package/src/tests/fixtures/routes/missing-attributes.tsx +8 -0
  40. package/src/tests/fixtures/routes/missing-path.tsx +9 -0
  41. package/src/tests/fixtures/routes/valid-full.tsx +15 -0
  42. package/src/tests/fixtures/routes/valid-minimal.tsx +10 -0
  43. package/src/tests/fixtures/routes/with-comments.tsx +12 -0
  44. package/src/tests/fixtures/routes/with-nested-objects.tsx +15 -0
  45. package/src/tests/upstart-editor-api.test.ts +367 -0
  46. package/src/tests/vite-plugin-upstart-attrs.test.ts +1189 -0
  47. package/src/tests/vite-plugin-upstart-editor.test.ts +81 -0
  48. package/src/upstart-editor-api.ts +204 -0
  49. package/src/vite-plugin-upstart-attrs.ts +708 -0
  50. package/src/vite-plugin-upstart-editor/PLAN.md +1391 -0
  51. package/src/vite-plugin-upstart-editor/plugin.ts +73 -0
  52. package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +80 -0
  53. package/src/vite-plugin-upstart-editor/runtime/hover-overlay.ts +135 -0
  54. package/src/vite-plugin-upstart-editor/runtime/index.ts +90 -0
  55. package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +401 -0
  56. package/src/vite-plugin-upstart-editor/runtime/types.ts +120 -0
  57. package/src/vite-plugin-upstart-editor/runtime/utils.ts +34 -0
  58. package/src/vite-plugin-upstart-theme.ts +314 -0
@@ -0,0 +1,1391 @@
1
+ # Prompt: Create Upstart Visual Editor Plugin
2
+
3
+ ## Context
4
+
5
+ I have a Vite plugin (`vite-plugin-upstart-attrs.ts`) that adds `data-*` attributes to JSX elements during build time. This plugin:
6
+ - Adds `data-upstart-hash` (content-based hash) to all JSX elements
7
+ - Adds `data-upstart-editable-text="true"` to text leaf elements (elements containing only static text)
8
+ - Adds `data-upstart-file`, `data-upstart-component`, and various prop tracking attributes to components
9
+ - Tracks the source location of text nodes in a metadata registry
10
+
11
+ Now I need to create a **companion plugin** that injects a visual editor into the app at runtime, allowing users to edit text content directly in the browser.
12
+
13
+ ## Important Architecture Note: Iframe Communication
14
+
15
+ **The user's app runs inside an iframe**, embedded in a parent editor application. This means:
16
+
17
+ - ❌ **NO API calls** to `/api/update-text` or any HTTP endpoints
18
+ - ✅ **Use `window.parent.postMessage()`** to communicate with the parent editor
19
+ - The parent editor (outside the iframe) is responsible for:
20
+ - Receiving edit messages
21
+ - Updating the source files
22
+ - Running `vite build` to rebuild the app
23
+ - Reloading the iframe with the new build
24
+
25
+ **Important**: The app is always served as a **production build** (result of `vite build` with the Upstart plugins enabled), NOT from a dev server. There is no HMR - the parent editor must rebuild and reload the iframe after each change.
26
+
27
+ ### Two Editing Modes
28
+
29
+ The iframe has **two modes** controlled by the parent window:
30
+
31
+ 1. **Preview Mode** (default):
32
+ - Users can interact with the page normally (click links, buttons, etc.)
33
+ - No editing handlers are active
34
+ - No visual indicators (hover overlays, edit affordances)
35
+ - Behaves like a normal website
36
+
37
+ 2. **Edit Mode**:
38
+ - Text editing enabled (double-click to edit with TipTap inside iframe)
39
+ - Click elements to trigger className editing (UI shown in parent window)
40
+ - Hover overlays show editable elements
41
+ - Visual affordances indicate editable content
42
+
43
+ The parent window switches between modes by sending a postMessage to the iframe:
44
+ ```typescript
45
+ // Parent sends mode change
46
+ iframe.contentWindow.postMessage({
47
+ source: 'upstart-editor-parent',
48
+ type: 'set-mode',
49
+ mode: 'edit' // or 'preview'
50
+ }, '*');
51
+ ```
52
+
53
+ ### Two Types of Editing
54
+
55
+ 1. **Text Editing** (inside iframe):
56
+ - Uses TipTap editor
57
+ - Double-click text to activate inline editor
58
+ - Editing happens directly in the iframe
59
+ - Sends `text-save` message to parent when done
60
+
61
+ 2. **ClassName Editing** (outside iframe):
62
+ - Single-click an element (in edit mode)
63
+ - Sends `element-clicked` message to parent with element info
64
+ - Parent window shows UI for editing Tailwind classes
65
+ - Parent updates source file, rebuilds, reloads iframe
66
+
67
+ **Communication Flow:**
68
+ ```
69
+ ┌─────────────────────────────────────────────────┐
70
+ │ Parent Editor Application │
71
+ │ - Receives postMessage from iframe │
72
+ │ - Shows className editor UI (Radix/Shadcn) │
73
+ │ - Updates source files │
74
+ │ - Runs vite build │
75
+ │ - Reloads iframe │
76
+ │ - Sends mode changes to iframe │
77
+ │ ┌───────────────────────────────────────────┐ │
78
+ │ │ <iframe> │ │
79
+ │ │ Compiled App (vite build) │ │
80
+ │ │ - Listens for mode changes │ │
81
+ │ │ - Text editing (TipTap inside) │ │
82
+ │ │ - Click detection (triggers parent UI) │ │
83
+ │ │ - Sends postMessage to parent │ │
84
+ │ └───────────────────────────────────────────┘ │
85
+ └─────────────────────────────────────────────────┘
86
+ ```
87
+
88
+ ## Project Structure
89
+
90
+ The new plugin will live in its own directory alongside existing plugins:
91
+
92
+ ```
93
+ vite-plugins/
94
+ src/
95
+ vite-plugin-upstart-attrs.ts # Existing (attached)
96
+ vite-plugin-upstart-routes.ts # Existing
97
+ vite-plugin-upstart-theme.ts # Existing
98
+ upstart-editor-api.ts # Existing API helpers
99
+ vite-plugin-upstart-editor/ # NEW - To be created
100
+ plugin.ts # Vite plugin (build-time)
101
+ runtime/
102
+ index.ts # Main entry point
103
+ text-editor.ts # TipTap text editing logic
104
+ hover-overlay.ts # Visual hover indicators
105
+ types.ts # Shared TypeScript types
106
+ tests/
107
+ # ... existing tests
108
+ ```
109
+
110
+ ## Architecture Overview
111
+
112
+ The new plugin is split into two parts:
113
+
114
+ 1. **Build-time plugin** (`plugin.ts`): Injects the editor initialization code during Vite build
115
+ 2. **Runtime code** (`runtime/` directory): The actual editor that runs in the browser
116
+
117
+ The key insight is that the plugin references **actual TypeScript files** (not template strings) so we get:
118
+ - Full IDE support and type checking
119
+ - Vite's HMR and module resolution
120
+ - Easy testing and maintenance
121
+ - Proper code organization
122
+
123
+ ## Part 1: Build-Time Plugin (`vite-plugin-upstart-editor/plugin.ts`)
124
+
125
+ ### Purpose
126
+ Automatically inject the editor initialization code into the user's app during development.
127
+
128
+ ### Requirements
129
+
130
+ **Plugin Setup**:
131
+ - Use `unplugin` (same as the attached `vite-plugin-upstart-attrs.ts`)
132
+ - Export using `createUnplugin` pattern
133
+ - Plugin name: `"upstart-editor"`
134
+ - Enforce: `"pre"` (run before other transforms)
135
+
136
+ **Options Interface**:
137
+ ```typescript
138
+ interface UpstartEditorOptions {
139
+ enabled?: boolean; // Enable/disable editor (default: false)
140
+ autoInject?: boolean; // Auto-inject initialization (default: true)
141
+ }
142
+ ```
143
+
144
+ **Note**: This plugin is designed to be used with `vite build` (production builds), not `vite dev`. The `enabled` option should be set to `true` when building the app for use in the editor iframe.
145
+
146
+ **Core Functionality**:
147
+
148
+ 1. **If not enabled**: Return a minimal plugin with name `"upstart-editor-disabled"`
149
+
150
+ 2. **Get runtime path**:
151
+ - Use `import.meta.url` and `fileURLToPath` to get current directory
152
+ - Resolve absolute path to `runtime/index.ts`
153
+ - Example: `path.resolve(__dirname, './runtime/index.ts')`
154
+
155
+ 3. **Inject runtime code using `transform` hook**:
156
+ ```typescript
157
+ transform(code, id) {
158
+ // Only inject in main entry file (main.tsx, main.ts, or index.tsx)
159
+ if (!id.includes('main.') && !id.includes('index.')) {
160
+ return null;
161
+ }
162
+
163
+ // Check if user has manually imported editor
164
+ if (code.includes('initUpstartEditor')) {
165
+ return null; // User is handling initialization manually
166
+ }
167
+
168
+ // Inject import and initialization at the top of the file
169
+ const injection = `
170
+ import { initUpstartEditor } from '${runtimePath}';
171
+
172
+ // Auto-initialize editor when DOM is ready
173
+ if (typeof window !== 'undefined') {
174
+ if (document.readyState === 'loading') {
175
+ document.addEventListener('DOMContentLoaded', () => initUpstartEditor());
176
+ } else {
177
+ initUpstartEditor();
178
+ }
179
+ }
180
+
181
+ ${code}
182
+ `;
183
+
184
+ return {
185
+ code: injection,
186
+ map: null,
187
+ };
188
+ }
189
+ ```
190
+
191
+ **Important Implementation Details**:
192
+ - Use absolute path resolution so Vite can find the runtime files
193
+ - The runtime files will be processed normally by Vite (bundling, HMR, etc.)
194
+ - Do NOT inject code as template strings - reference actual `.ts` files
195
+ - Only inject once (check for existing imports)
196
+
197
+ **Export Pattern**:
198
+ ```typescript
199
+ import { createUnplugin } from "unplugin";
200
+
201
+ export const upstartEditor = createUnplugin<UpstartEditorOptions>((options = {}) => {
202
+ // Plugin implementation
203
+ });
204
+
205
+ export default upstartEditor.vite;
206
+ ```
207
+
208
+ ---
209
+
210
+ ## Part 2: Runtime Entry Point (`vite-plugin-upstart-editor/runtime/index.ts`)
211
+
212
+ ### Purpose
213
+ Main entry point that initializes all editor features and manages edit/preview modes.
214
+
215
+ ### Requirements
216
+
217
+ **State management**:
218
+ ```typescript
219
+ let currentMode: 'preview' | 'edit' = 'preview'; // Default to preview mode
220
+
221
+ export function getCurrentMode() {
222
+ return currentMode;
223
+ }
224
+
225
+ export function setMode(mode: 'preview' | 'edit') {
226
+ currentMode = mode;
227
+
228
+ if (mode === 'edit') {
229
+ enableEditMode();
230
+ } else {
231
+ disableEditMode();
232
+ }
233
+ }
234
+ ```
235
+
236
+ **Listen for mode changes from parent**:
237
+ ```typescript
238
+ window.addEventListener('message', (event) => {
239
+ const message = event.data;
240
+
241
+ // Only accept messages from parent
242
+ if (message.source !== 'upstart-editor-parent') return;
243
+
244
+ if (message.type === 'set-mode') {
245
+ setMode(message.mode);
246
+ }
247
+ });
248
+ ```
249
+
250
+ **Export main initialization function**:
251
+ ```typescript
252
+ export function initUpstartEditor() {
253
+ console.log('[Upstart Editor] Initializing...');
254
+
255
+ // Start in preview mode (no handlers active)
256
+ currentMode = 'preview';
257
+
258
+ // Listen for mode changes from parent
259
+ window.addEventListener('message', handleParentMessage);
260
+
261
+ // Initialize features (but they only activate in edit mode)
262
+ initTextEditor();
263
+ initClickHandler();
264
+ initHoverOverlay();
265
+
266
+ // Notify parent that editor is ready
267
+ sendToParent({ type: 'editor-ready' });
268
+ }
269
+
270
+ function handleParentMessage(event: MessageEvent) {
271
+ const message = event.data;
272
+
273
+ if (message.source !== 'upstart-editor-parent') return;
274
+
275
+ if (message.type === 'set-mode') {
276
+ setMode(message.mode);
277
+ }
278
+ }
279
+
280
+ function enableEditMode() {
281
+ console.log('[Upstart Editor] Edit mode enabled');
282
+ // Event handlers will check getCurrentMode() before activating
283
+ }
284
+
285
+ function disableEditMode() {
286
+ console.log('[Upstart Editor] Preview mode enabled');
287
+ // Clean up any active editors
288
+ destroyAllActiveEditors();
289
+ hideOverlays();
290
+ }
291
+ ```
292
+
293
+ **Export individual functions** for advanced users:
294
+ ```typescript
295
+ export { initTextEditor } from './text-editor';
296
+ export { initClickHandler } from './click-handler';
297
+ export { initHoverOverlay } from './hover-overlay';
298
+ export { sendToParent } from './utils';
299
+ export type { EditorMessage, UpstartEditorMessage } from './types';
300
+ ```
301
+
302
+ **Graceful error handling**:
303
+ - Wrap initialization in try-catch
304
+ - Log errors clearly with `[Upstart Editor]` prefix
305
+ - Don't crash if elements aren't found
306
+
307
+ ---
308
+
309
+ ## Part 3: Text Editor (`vite-plugin-upstart-editor/runtime/text-editor.ts`)
310
+
311
+ ### Purpose
312
+ Enable inline text editing using TipTap editor library.
313
+
314
+ ### Core Functionality
315
+
316
+ **Find editable elements**:
317
+ ```typescript
318
+ const editables = document.querySelectorAll('[data-upstart-editable-text="true"]');
319
+ ```
320
+
321
+ **For each editable element**:
322
+
323
+ 1. **Add visual affordances** (only visible in edit mode):
324
+ - Hover: `outline: 1px dashed #3b82f6`
325
+ - Cursor: `cursor: text`
326
+ - Transition: `transition: outline 0.15s ease`
327
+
328
+ 2. **Enable editing on double-click** (only in edit mode):
329
+ ```typescript
330
+ element.addEventListener('dblclick', (e) => {
331
+ // IMPORTANT: Only activate in edit mode
332
+ if (getCurrentMode() !== 'edit') return;
333
+
334
+ e.preventDefault();
335
+ e.stopPropagation();
336
+
337
+ const hash = element.dataset.upstartHash;
338
+ if (!hash) return;
339
+
340
+ // Prevent duplicate editors
341
+ if (activeEditors.has(hash)) return;
342
+
343
+ activateEditor(element, hash);
344
+ });
345
+ ```
346
+
347
+ 3. **Show hover effects** (only in edit mode):
348
+ ```typescript
349
+ element.addEventListener('mouseenter', () => {
350
+ if (getCurrentMode() !== 'edit') return;
351
+ element.style.outline = '1px dashed #3b82f6';
352
+ });
353
+
354
+ element.addEventListener('mouseleave', () => {
355
+ if (!activeEditors.has(hash)) {
356
+ element.style.outline = '';
357
+ }
358
+ });
359
+ ```
360
+
361
+ 3. **Determine editor type**:
362
+ ```typescript
363
+ function shouldUseRichText(element: HTMLElement): boolean {
364
+ const tagName = element.tagName.toLowerCase();
365
+ const plainTextTags = ['button', 'a', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'label'];
366
+ return !plainTextTags.includes(tagName);
367
+ }
368
+ ```
369
+
370
+ ### TipTap Configuration
371
+
372
+ **Install these packages** (add to package.json dependencies):
373
+ ```json
374
+ {
375
+ "@tiptap/core": "^2.8.0",
376
+ "@tiptap/starter-kit": "^2.8.0",
377
+ "@tiptap/extension-placeholder": "^2.8.0",
378
+ "@tiptap/extension-bubble-menu": "^2.8.0"
379
+ }
380
+ ```
381
+
382
+ **Import statements**:
383
+ ```typescript
384
+ import { Editor } from '@tiptap/core';
385
+ import StarterKit from '@tiptap/starter-kit';
386
+ import Placeholder from '@tiptap/extension-placeholder';
387
+ import { BubbleMenu } from '@tiptap/extension-bubble-menu';
388
+ ```
389
+
390
+ **For Plain Text Elements** (buttons, links, headings):
391
+ ```typescript
392
+ const editor = new Editor({
393
+ element: htmlElement,
394
+ extensions: [
395
+ StarterKit.configure({
396
+ // Disable all formatting for plain text
397
+ heading: false,
398
+ bold: false,
399
+ italic: false,
400
+ strike: false,
401
+ blockquote: false,
402
+ bulletList: false,
403
+ orderedList: false,
404
+ listItem: false,
405
+ codeBlock: false,
406
+ horizontalRule: false,
407
+ }),
408
+ Placeholder.configure({
409
+ placeholder: 'Click to edit...',
410
+ }),
411
+ ],
412
+ content: htmlElement.innerHTML,
413
+ editorProps: {
414
+ attributes: {
415
+ class: 'upstart-editor-active',
416
+ style: 'outline: 2px solid #3b82f6; outline-offset: 2px;',
417
+ },
418
+ },
419
+ onUpdate: ({ editor }) => {
420
+ debouncedSave(hash, editor.getText());
421
+ },
422
+ onBlur: ({ editor }) => {
423
+ saveText(hash, editor.getText());
424
+ destroyEditor(hash);
425
+ },
426
+ onCreate: ({ editor }) => {
427
+ editor.commands.focus('end');
428
+ },
429
+ });
430
+ ```
431
+
432
+ **For Rich Text Elements** (paragraphs, divs, articles):
433
+ ```typescript
434
+ const editor = new Editor({
435
+ element: htmlElement,
436
+ extensions: [
437
+ StarterKit, // All formatting enabled
438
+ Placeholder.configure({
439
+ placeholder: 'Start typing...',
440
+ }),
441
+ BubbleMenu.configure({
442
+ // Bubble menu appears on text selection
443
+ element: createBubbleMenuElement(),
444
+ }),
445
+ ],
446
+ content: htmlElement.innerHTML,
447
+ editorProps: {
448
+ attributes: {
449
+ class: 'upstart-editor-active',
450
+ style: 'outline: 2px solid #3b82f6; outline-offset: 2px;',
451
+ },
452
+ },
453
+ onUpdate: ({ editor }) => {
454
+ debouncedSave(hash, editor.getHTML()); // Use HTML for rich text
455
+ },
456
+ onBlur: ({ editor }) => {
457
+ saveText(hash, editor.getHTML());
458
+ destroyEditor(hash);
459
+ },
460
+ onCreate: ({ editor }) => {
461
+ editor.commands.focus('end');
462
+ },
463
+ });
464
+ ```
465
+
466
+ ### Editor Instance Management
467
+
468
+ **Track active editors**:
469
+ ```typescript
470
+ const activeEditors = new Map<string, EditorInstance>();
471
+
472
+ interface EditorInstance {
473
+ editor: Editor;
474
+ element: HTMLElement;
475
+ hash: string;
476
+ }
477
+ ```
478
+
479
+ **Activate editor**:
480
+ ```typescript
481
+ function activateEditor(element: HTMLElement, hash: string) {
482
+ const isRichText = shouldUseRichText(element);
483
+
484
+ const editor = new Editor({
485
+ // ... configuration based on isRichText
486
+ });
487
+
488
+ // Store instance
489
+ activeEditors.set(hash, { editor, element, hash });
490
+ }
491
+ ```
492
+
493
+ **Destroy editor**:
494
+ ```typescript
495
+ function destroyEditor(hash: string) {
496
+ const instance = activeEditors.get(hash);
497
+ if (instance) {
498
+ instance.editor.destroy();
499
+ instance.element.style.outline = '';
500
+ activeEditors.delete(hash);
501
+ }
502
+ }
503
+ ```
504
+
505
+ ### Save Mechanism via postMessage
506
+
507
+ **Message Types** to send to parent:
508
+
509
+ ```typescript
510
+ // Type definitions for messages
511
+ type EditorMessage =
512
+ | { type: 'text-update', hash: string, newText: string }
513
+ | { type: 'text-save', hash: string, newText: string }
514
+ | { type: 'editor-ready' }
515
+ | { type: 'editor-error', error: string };
516
+ ```
517
+
518
+ **Debounced auto-save** (during typing):
519
+ ```typescript
520
+ let saveTimeout: number | undefined;
521
+
522
+ function debouncedSave(hash: string, content: string) {
523
+ if (saveTimeout) {
524
+ clearTimeout(saveTimeout);
525
+ }
526
+
527
+ saveTimeout = window.setTimeout(() => {
528
+ // Send update message (non-critical, for preview)
529
+ sendToParent({
530
+ type: 'text-update',
531
+ hash,
532
+ newText: content,
533
+ });
534
+ }, 1000); // 1 second delay
535
+ }
536
+ ```
537
+
538
+ **Immediate save** (on blur or Cmd+Enter):
539
+ ```typescript
540
+ function saveText(hash: string, newText: string) {
541
+ try {
542
+ // Send save message (critical, must update source file)
543
+ sendToParent({
544
+ type: 'text-save',
545
+ hash,
546
+ newText,
547
+ });
548
+
549
+ console.log('[Upstart Editor] Text save message sent:', hash);
550
+ } catch (error) {
551
+ console.error('[Upstart Editor] Failed to send save message:', error);
552
+
553
+ // Notify parent of error
554
+ sendToParent({
555
+ type: 'editor-error',
556
+ error: error instanceof Error ? error.message : 'Unknown error',
557
+ });
558
+ }
559
+ }
560
+ ```
561
+
562
+ **Helper function to send messages to parent**:
563
+ ```typescript
564
+ function sendToParent(message: EditorMessage) {
565
+ if (window.parent === window) {
566
+ // Not in iframe, log warning
567
+ console.warn('[Upstart Editor] Not running in iframe, cannot send message:', message);
568
+ return;
569
+ }
570
+
571
+ // Send to parent with origin check (use '*' for development, specific origin for production)
572
+ window.parent.postMessage(
573
+ {
574
+ source: 'upstart-editor',
575
+ ...message,
576
+ },
577
+ '*' // TODO: Use specific origin in production for security
578
+ );
579
+ }
580
+ ```
581
+
582
+ **Notify parent when editor is ready**:
583
+ ```typescript
584
+ // In initTextEditor(), after setup
585
+ export function initTextEditor() {
586
+ console.log('[Upstart Editor] Initializing text editor...');
587
+
588
+ // ... setup code ...
589
+
590
+ // Notify parent that editor is ready
591
+ sendToParent({ type: 'editor-ready' });
592
+ }
593
+ ```
594
+ ```
595
+
596
+ ### Additional Features
597
+
598
+ **Keyboard shortcuts**:
599
+ - `Escape`: Cancel editing (blur without saving)
600
+ - `Cmd/Ctrl + Enter`: Save and close editor
601
+
602
+ ```typescript
603
+ document.addEventListener('keydown', (e) => {
604
+ if (e.key === 'Escape') {
605
+ // Blur all active editors
606
+ activeEditors.forEach((instance) => {
607
+ instance.editor.commands.blur();
608
+ });
609
+ }
610
+
611
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
612
+ // Save and close
613
+ activeEditors.forEach((instance) => {
614
+ instance.editor.commands.blur();
615
+ });
616
+ }
617
+ });
618
+ ```
619
+
620
+ **Visual feedback states**:
621
+ - Default: No outline
622
+ - Hover: `1px dashed #3b82f6`
623
+ - Active editing: `2px solid #3b82f6` with slight offset
624
+ - Saving: Could add a "saving..." indicator (future enhancement)
625
+
626
+ ---
627
+
628
+ ## Part 4: Click Handler (`vite-plugin-upstart-editor/runtime/click-handler.ts`)
629
+
630
+ ### Purpose
631
+ Detect clicks on elements to trigger className editing in the parent window.
632
+
633
+ ### Requirements
634
+
635
+ **Click detection**:
636
+ ```typescript
637
+ import { getCurrentMode } from './index';
638
+ import { sendToParent } from './utils';
639
+
640
+ export function initClickHandler() {
641
+ console.log('[Upstart Editor] Initializing click handler...');
642
+
643
+ document.addEventListener('click', handleClick, true); // Use capture phase
644
+ }
645
+
646
+ function handleClick(e: MouseEvent) {
647
+ // Only handle clicks in edit mode
648
+ if (getCurrentMode() !== 'edit') return;
649
+
650
+ const target = e.target as HTMLElement;
651
+
652
+ // Find the nearest component element
653
+ const component = target.closest('[data-upstart-component]');
654
+
655
+ if (!component) return;
656
+
657
+ // Prevent default behavior (like link navigation)
658
+ e.preventDefault();
659
+ e.stopPropagation();
660
+
661
+ const htmlElement = component as HTMLElement;
662
+ const hash = htmlElement.dataset.upstartHash;
663
+ const componentName = htmlElement.dataset.upstartComponent;
664
+ const filePath = htmlElement.dataset.upstartFile;
665
+
666
+ if (!hash || !componentName) return;
667
+
668
+ // Get current className
669
+ const currentClassName = htmlElement.className;
670
+
671
+ // Get element bounds for positioning parent UI
672
+ const rect = htmlElement.getBoundingClientRect();
673
+
674
+ // Send click event to parent
675
+ sendToParent({
676
+ type: 'element-clicked',
677
+ hash,
678
+ componentName,
679
+ filePath: filePath || '',
680
+ currentClassName,
681
+ bounds: {
682
+ top: rect.top,
683
+ left: rect.left,
684
+ width: rect.width,
685
+ height: rect.height,
686
+ right: rect.right,
687
+ bottom: rect.bottom,
688
+ },
689
+ });
690
+
691
+ console.log('[Upstart Editor] Element clicked:', componentName, hash);
692
+ }
693
+ ```
694
+
695
+ **Cleanup function**:
696
+ ```typescript
697
+ export function cleanupClickHandler() {
698
+ document.removeEventListener('click', handleClick, true);
699
+ }
700
+ ```
701
+
702
+ **Important notes**:
703
+ - Use capture phase (`true` parameter) to catch clicks before they reach the target
704
+ - Prevent default behavior to stop navigation
705
+ - Only components with `data-upstart-component` are clickable for className editing
706
+ - Text elements with `data-upstart-editable-text` use double-click for text editing
707
+
708
+ ---
709
+
710
+ ## Part 5: Hover Overlay (`vite-plugin-upstart-editor/runtime/hover-overlay.ts`)
711
+
712
+ ### Purpose
713
+ Show visual feedback when hovering over components, indicating they're part of the editable system. Only active in edit mode.
714
+
715
+ ### Requirements
716
+
717
+ **Create overlay element**:
718
+ ```typescript
719
+ import { getCurrentMode } from './index';
720
+ import { sendToParent } from './utils';
721
+
722
+ let overlay: HTMLDivElement | null = null;
723
+ let currentTarget: HTMLElement | null = null;
724
+
725
+ function createOverlay() {
726
+ overlay = document.createElement('div');
727
+ overlay.id = 'upstart-hover-overlay';
728
+ overlay.style.cssText = `
729
+ position: absolute;
730
+ pointer-events: none;
731
+ border: 2px solid #3b82f6;
732
+ background: rgba(59, 130, 246, 0.05);
733
+ border-radius: 4px;
734
+ z-index: 9999;
735
+ transition: all 0.1s ease;
736
+ display: none;
737
+ `;
738
+ document.body.appendChild(overlay);
739
+ }
740
+ ```
741
+
742
+ **Position overlay over element**:
743
+ ```typescript
744
+ function positionOverlay(element: HTMLElement) {
745
+ if (!overlay) return;
746
+
747
+ const rect = element.getBoundingClientRect();
748
+
749
+ overlay.style.top = `${rect.top + window.scrollY}px`;
750
+ overlay.style.left = `${rect.left + window.scrollX}px`;
751
+ overlay.style.width = `${rect.width}px`;
752
+ overlay.style.height = `${rect.height}px`;
753
+ overlay.style.display = 'block';
754
+ }
755
+ ```
756
+
757
+ **Show overlay on hover** (only in edit mode):
758
+ ```typescript
759
+ document.addEventListener('mouseover', (e) => {
760
+ // Only show in edit mode
761
+ if (getCurrentMode() !== 'edit') return;
762
+
763
+ const target = e.target as HTMLElement;
764
+
765
+ // Only show on elements with data-upstart-component
766
+ if (target.hasAttribute('data-upstart-component')) {
767
+ if (!overlay) createOverlay();
768
+ currentTarget = target;
769
+ positionOverlay(target);
770
+
771
+ // Notify parent about hovered element
772
+ const hash = target.dataset.upstartHash;
773
+ if (hash) {
774
+ const rect = target.getBoundingClientRect();
775
+ sendToParent({
776
+ type: 'element-hovered',
777
+ hash,
778
+ bounds: {
779
+ top: rect.top,
780
+ left: rect.left,
781
+ width: rect.width,
782
+ height: rect.height,
783
+ right: rect.right,
784
+ bottom: rect.bottom,
785
+ },
786
+ });
787
+ }
788
+ }
789
+ });
790
+ ```
791
+
792
+ **Hide overlay on mouseout**:
793
+ ```typescript
794
+ document.addEventListener('mouseout', (e) => {
795
+ const target = e.target as HTMLElement;
796
+
797
+ if (target.hasAttribute('data-upstart-component')) {
798
+ if (overlay) {
799
+ overlay.style.display = 'none';
800
+ currentTarget = null;
801
+ }
802
+ }
803
+ });
804
+ ```
805
+
806
+ **Update position on scroll/resize**:
807
+ ```typescript
808
+ let rafId: number | null = null;
809
+
810
+ function updateOverlayPosition() {
811
+ if (currentTarget && overlay && overlay.style.display === 'block') {
812
+ positionOverlay(currentTarget);
813
+ }
814
+ rafId = null;
815
+ }
816
+
817
+ window.addEventListener('scroll', () => {
818
+ if (rafId === null) {
819
+ rafId = requestAnimationFrame(updateOverlayPosition);
820
+ }
821
+ }, { passive: true });
822
+
823
+ window.addEventListener('resize', () => {
824
+ if (rafId === null) {
825
+ rafId = requestAnimationFrame(updateOverlayPosition);
826
+ }
827
+ }, { passive: true });
828
+ ```
829
+
830
+ **Export initialization**:
831
+ ```typescript
832
+ export function initHoverOverlay() {
833
+ console.log('[Upstart Editor] Initializing hover overlay...');
834
+
835
+ // Event listeners are registered when this function is called
836
+ // Overlay is created lazily on first hover
837
+ // They only activate when getCurrentMode() === 'edit'
838
+ }
839
+
840
+ export function hideOverlays() {
841
+ if (overlay) {
842
+ overlay.style.display = 'none';
843
+ currentTarget = null;
844
+ }
845
+ }
846
+ ```
847
+
848
+ ---
849
+
850
+ ## Part 6: Types (`vite-plugin-upstart-editor/runtime/types.ts`)
851
+
852
+ ### Purpose
853
+ Shared TypeScript types for the editor system.
854
+
855
+ ### Required Types
856
+
857
+ ```typescript
858
+ import type { Editor } from '@tiptap/core';
859
+
860
+ /**
861
+ * Represents an active editor instance
862
+ */
863
+ export interface EditorInstance {
864
+ editor: Editor;
865
+ element: HTMLElement;
866
+ hash: string;
867
+ }
868
+
869
+ /**
870
+ * Messages sent from iframe to parent editor
871
+ */
872
+ export type EditorMessage =
873
+ | { type: 'text-update'; hash: string; newText: string }
874
+ | { type: 'text-save'; hash: string; newText: string }
875
+ | { type: 'editor-ready' }
876
+ | { type: 'editor-error'; error: string }
877
+ | { type: 'element-hovered'; hash: string; bounds: DOMRect }
878
+ | {
879
+ type: 'element-clicked';
880
+ hash: string;
881
+ componentName: string;
882
+ filePath: string;
883
+ currentClassName: string;
884
+ bounds: DOMRect;
885
+ };
886
+
887
+ /**
888
+ * Messages sent from parent to iframe
889
+ */
890
+ export type ParentMessage =
891
+ | { type: 'set-mode'; mode: 'edit' | 'preview' };
892
+
893
+ /**
894
+ * Message wrapper sent via postMessage from iframe
895
+ */
896
+ export interface UpstartEditorMessage {
897
+ source: 'upstart-editor';
898
+ type: EditorMessage['type'];
899
+ [key: string]: any;
900
+ }
901
+
902
+ /**
903
+ * Message wrapper sent via postMessage from parent
904
+ */
905
+ export interface UpstartParentMessage {
906
+ source: 'upstart-editor-parent';
907
+ type: ParentMessage['type'];
908
+ [key: string]: any;
909
+ }
910
+
911
+ /**
912
+ * Configuration options for the Upstart Editor
913
+ */
914
+ export interface UpstartEditorOptions {
915
+ /**
916
+ * Elements that should use rich text editing (with formatting)
917
+ * @default ['p', 'div', 'article', 'section']
918
+ */
919
+ richTextElements?: string[];
920
+
921
+ /**
922
+ * Elements that should use plain text editing (no formatting)
923
+ * @default ['button', 'a', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'label']
924
+ */
925
+ plainTextElements?: string[];
926
+
927
+ /**
928
+ * Enable bubble menu for text selection
929
+ * @default true
930
+ */
931
+ bubbleMenu?: boolean;
932
+
933
+ /**
934
+ * Placeholder text for empty editors
935
+ * @default 'Start typing...'
936
+ */
937
+ placeholder?: string;
938
+
939
+ /**
940
+ * Auto-save delay in milliseconds
941
+ * @default 1000
942
+ */
943
+ autoSaveDelay?: number;
944
+ }
945
+
946
+ /**
947
+ * Build-time plugin options
948
+ */
949
+ export interface UpstartEditorPluginOptions {
950
+ /**
951
+ * Enable or disable the editor
952
+ * @default false
953
+ */
954
+ enabled?: boolean;
955
+
956
+ /**
957
+ * Automatically inject editor initialization code
958
+ * @default true
959
+ */
960
+ autoInject?: boolean;
961
+ }
962
+
963
+ /**
964
+ * Editor mode
965
+ */
966
+ export type EditorMode = 'preview' | 'edit';
967
+ ```
968
+
969
+ ---
970
+
971
+ ## Part 7: Utility Functions (`vite-plugin-upstart-editor/runtime/utils.ts`)
972
+
973
+ ### Purpose
974
+ Shared utility functions for postMessage communication.
975
+
976
+ ### Requirements
977
+
978
+ ```typescript
979
+ import type { EditorMessage } from './types';
980
+
981
+ /**
982
+ * Send a message to the parent window
983
+ */
984
+ export function sendToParent(message: EditorMessage) {
985
+ if (window.parent === window) {
986
+ // Not in iframe, log warning
987
+ console.warn('[Upstart Editor] Not running in iframe, cannot send message:', message);
988
+ return;
989
+ }
990
+
991
+ // Send to parent with origin check (use '*' for development, specific origin for production)
992
+ window.parent.postMessage(
993
+ {
994
+ source: 'upstart-editor',
995
+ ...message,
996
+ },
997
+ '*' // TODO: Use specific origin in production for security
998
+ );
999
+ }
1000
+
1001
+ /**
1002
+ * Check if running inside an iframe
1003
+ */
1004
+ export function isInIframe(): boolean {
1005
+ return window.parent !== window;
1006
+ }
1007
+ ```
1008
+
1009
+ ---
1010
+
1011
+ ## Package Configuration
1012
+
1013
+ ### Update `package.json`
1014
+
1015
+ Add new dependencies:
1016
+ ```json
1017
+ {
1018
+ "dependencies": {
1019
+ "@tiptap/core": "^2.8.0",
1020
+ "@tiptap/starter-kit": "^2.8.0",
1021
+ "@tiptap/extension-placeholder": "^2.8.0",
1022
+ "@tiptap/extension-bubble-menu": "^2.8.0"
1023
+ }
1024
+ }
1025
+ ```
1026
+
1027
+ ### Update Build Configuration
1028
+
1029
+ If using `tsdown.config.ts`, add new entry points:
1030
+ ```typescript
1031
+ export default {
1032
+ entry: [
1033
+ 'src/vite-plugin-upstart-attrs.ts',
1034
+ 'src/vite-plugin-upstart-routes.ts',
1035
+ 'src/vite-plugin-upstart-theme.ts',
1036
+ 'src/upstart-editor-api.ts',
1037
+ // New entries
1038
+ 'src/vite-plugin-upstart-editor/plugin.ts',
1039
+ 'src/vite-plugin-upstart-editor/runtime/index.ts',
1040
+ 'src/vite-plugin-upstart-editor/runtime/text-editor.ts',
1041
+ 'src/vite-plugin-upstart-editor/runtime/click-handler.ts',
1042
+ 'src/vite-plugin-upstart-editor/runtime/hover-overlay.ts',
1043
+ 'src/vite-plugin-upstart-editor/runtime/types.ts',
1044
+ 'src/vite-plugin-upstart-editor/runtime/utils.ts',
1045
+ ],
1046
+ // ... rest of config
1047
+ };
1048
+ ```
1049
+
1050
+ ### Export in Main Package
1051
+
1052
+ Update the package exports to include the new plugin:
1053
+ ```json
1054
+ {
1055
+ "exports": {
1056
+ "./upstart-attrs": {
1057
+ "import": "./dist/vite-plugin-upstart-attrs.js",
1058
+ "types": "./dist/vite-plugin-upstart-attrs.d.ts"
1059
+ },
1060
+ "./upstart-editor": {
1061
+ "import": "./dist/vite-plugin-upstart-editor/plugin.js",
1062
+ "types": "./dist/vite-plugin-upstart-editor/plugin.d.ts"
1063
+ },
1064
+ "./upstart-editor/runtime": {
1065
+ "import": "./dist/vite-plugin-upstart-editor/runtime/index.js",
1066
+ "types": "./dist/vite-plugin-upstart-editor/runtime/index.d.ts"
1067
+ }
1068
+ }
1069
+ }
1070
+ ```
1071
+
1072
+ ---
1073
+
1074
+ ## User Experience Flow
1075
+
1076
+ 1. **Developer embeds user's app in iframe** within the parent editor application
1077
+ - The iframe loads a production build of the app (from `vite build`)
1078
+ - Plugin is initialized in **preview mode** by default
1079
+
1080
+ 2. **User switches to edit mode**:
1081
+ - Parent sends `set-mode` message with `mode: 'edit'`
1082
+ - Editor plugin activates all handlers
1083
+
1084
+ 3. **User sees app** with visual editing affordances:
1085
+ - Hover overlays appear on components
1086
+ - Text elements show cursor and outline hints
1087
+
1088
+ 4. **User hovers over components** (in edit mode):
1089
+ - Blue overlay appears in iframe
1090
+ - `element-hovered` message sent to parent with element bounds
1091
+ - Parent could show element info in sidebar
1092
+
1093
+ 5. **User clicks an element** (in edit mode):
1094
+ - `element-clicked` message sent to parent
1095
+ - Parent shows className editor UI (outside iframe)
1096
+ - User edits Tailwind classes in parent UI
1097
+ - Parent updates source file, rebuilds, reloads iframe
1098
+
1099
+ 6. **User double-clicks text** (in edit mode):
1100
+ - TipTap editor activates inside iframe
1101
+ - Plain text elements: Basic editor, no formatting
1102
+ - Rich text elements: Full editor with formatting toolbar
1103
+
1104
+ 7. **User types** (text editing):
1105
+ - Changes are debounced (1 second)
1106
+ - `text-update` message sent to parent (optional, for live preview)
1107
+
1108
+ 8. **User clicks away or presses Escape** (text editing):
1109
+ - `text-save` message sent to parent
1110
+ - Parent editor updates source file
1111
+ - Parent runs `vite build` to rebuild the app
1112
+ - Parent reloads the iframe with the new build
1113
+
1114
+ 9. **User switches to preview mode**:
1115
+ - Parent sends `set-mode` message with `mode: 'preview'`
1116
+ - All handlers deactivate
1117
+ - User can interact with page normally (click links, test functionality)
1118
+
1119
+ 10. **Parent editor responsibilities**:
1120
+ - Listen for `postMessage` events from iframe
1121
+ - Filter messages by `source: 'upstart-editor'`
1122
+ - Send mode changes to iframe
1123
+ - Show className editor UI on `element-clicked`
1124
+ - Update source files based on `text-save` or className changes
1125
+ - Run `vite build` to create new production build
1126
+ - Reload iframe (e.g., by updating iframe `src` with cache-busting query param)
1127
+
1128
+ **Parent Editor Message Handler Example**:
1129
+ ```typescript
1130
+ // In parent editor application
1131
+ window.addEventListener('message', async (event) => {
1132
+ const message = event.data;
1133
+
1134
+ // Verify message is from our editor
1135
+ if (message.source !== 'upstart-editor') return;
1136
+
1137
+ switch (message.type) {
1138
+ case 'editor-ready':
1139
+ console.log('Editor initialized in iframe');
1140
+ // Optionally set initial mode
1141
+ setIframeMode('preview'); // or 'edit'
1142
+ break;
1143
+
1144
+ case 'text-update':
1145
+ // Optional: Show live preview (without rebuilding)
1146
+ console.log('Text being edited:', message.hash);
1147
+ break;
1148
+
1149
+ case 'text-save':
1150
+ // Update source file
1151
+ await updateSourceFile(message.hash, message.newText);
1152
+
1153
+ // Rebuild the app
1154
+ await runViteBuild();
1155
+
1156
+ // Reload iframe with cache busting
1157
+ const iframe = document.querySelector('iframe');
1158
+ const currentSrc = iframe.src.split('?')[0];
1159
+ iframe.src = `${currentSrc}?t=${Date.now()}`;
1160
+ break;
1161
+
1162
+ case 'element-clicked':
1163
+ // Show className editor UI
1164
+ showClassNameEditor({
1165
+ hash: message.hash,
1166
+ componentName: message.componentName,
1167
+ currentClassName: message.currentClassName,
1168
+ bounds: message.bounds,
1169
+ });
1170
+ break;
1171
+
1172
+ case 'element-hovered':
1173
+ // Optional: Highlight in layer tree or show properties
1174
+ showElementProperties(message.hash, message.bounds);
1175
+ break;
1176
+
1177
+ case 'editor-error':
1178
+ console.error('Editor error:', message.error);
1179
+ break;
1180
+ }
1181
+ });
1182
+
1183
+ // Function to send mode changes to iframe
1184
+ function setIframeMode(mode: 'edit' | 'preview') {
1185
+ const iframe = document.querySelector('iframe');
1186
+ iframe?.contentWindow?.postMessage({
1187
+ source: 'upstart-editor-parent',
1188
+ type: 'set-mode',
1189
+ mode,
1190
+ }, '*');
1191
+ }
1192
+ ```
1193
+
1194
+ ---
1195
+
1196
+ ## Edge Cases to Handle
1197
+
1198
+ ### Multiple Editors
1199
+ - **Problem**: User might double-click multiple elements
1200
+ - **Solution**: Track active editors in a Map, prevent duplicate editors on same element
1201
+
1202
+ ### Memory Leaks
1203
+ - **Problem**: Editor instances not properly cleaned up
1204
+ - **Solution**: Always call `editor.destroy()` and remove from Map on blur
1205
+
1206
+ ### Event Bubbling
1207
+ - **Problem**: Clicks/double-clicks propagate to parent elements
1208
+ - **Solution**: Use `e.stopPropagation()` on editor activation
1209
+
1210
+ ### Scroll/Resize
1211
+ - **Problem**: Hover overlay doesn't update position
1212
+ - **Solution**: Use `requestAnimationFrame` to efficiently update position
1213
+
1214
+ ### Iframe Reload
1215
+ - **Problem**: Editor state is lost when iframe reloads after build
1216
+ - **Solution**: This is expected behavior - the iframe loads fresh HTML after each build
1217
+
1218
+ ### Nested Elements
1219
+ - **Problem**: Hovering nested components shows multiple overlays
1220
+ - **Solution**: Only show overlay for the directly hovered element (current implementation handles this)
1221
+
1222
+ ### Empty Text Elements
1223
+ - **Problem**: Can't click on empty elements
1224
+ - **Solution**: TipTap's Placeholder extension handles this
1225
+
1226
+ ### Not in Iframe
1227
+ - **Problem**: Plugin might run when not embedded in iframe
1228
+ - **Solution**: Check `window.parent === window` before sending messages, log warning if not in iframe
1229
+
1230
+ ### Cross-Origin Issues
1231
+ - **Problem**: postMessage might be blocked by CORS
1232
+ - **Solution**: Use `'*'` for development (with security warning), document how to configure specific origin for production
1233
+
1234
+ ### Message Loss
1235
+ - **Problem**: postMessage might be sent before parent is ready to receive
1236
+ - **Solution**: Parent should send ready signal first (optional), or use retry logic
1237
+
1238
+ ### Large Text Content
1239
+ - **Problem**: Very large text might cause performance issues with postMessage
1240
+ - **Solution**: Consider chunking or compression for large content (future enhancement)
1241
+
1242
+ ### Build Performance
1243
+ - **Problem**: Running `vite build` on every text change is slow
1244
+ - **Solution**:
1245
+ - Use debouncing (1 second delay before save)
1246
+ - Only trigger build on `text-save`, not on `text-update`
1247
+ - Parent editor should queue builds if multiple saves happen quickly
1248
+
1249
+ ---
1250
+
1251
+ ## Success Criteria
1252
+
1253
+ The implementation is successful when:
1254
+
1255
+ 1. ✅ Plugin injects code during `vite build` without manual user setup
1256
+ 2. ✅ Double-clicking text elements activates TipTap editor
1257
+ 3. ✅ Plain text elements get basic editor (no formatting)
1258
+ 4. ✅ Rich text elements get full editor (with formatting toolbar)
1259
+ 5. ✅ Changes trigger `text-update` messages (debounced) via postMessage
1260
+ 6. ✅ Blur triggers `text-save` messages (immediate) via postMessage
1261
+ 7. ✅ All messages include `source: 'upstart-editor'` identifier
1262
+ 8. ✅ Hover overlay shows on component elements
1263
+ 9. ✅ Hover triggers `element-hovered` messages with bounds
1264
+ 10. ✅ Overlay updates smoothly on scroll/resize
1265
+ 11. ✅ No memory leaks (editors are properly cleaned up)
1266
+ 12. ✅ Full TypeScript support with proper types
1267
+ 13. ✅ Works correctly in production builds (from `vite build`)
1268
+ 14. ✅ No global namespace pollution
1269
+ 15. ✅ Visual feedback is clear and non-intrusive
1270
+ 16. ✅ Keyboard shortcuts work (Escape, Cmd+Enter)
1271
+ 17. ✅ No duplicate editors on same element
1272
+ 18. ✅ Event propagation handled correctly
1273
+ 19. ✅ Graceful handling when not in iframe (warning logged)
1274
+ 20. ✅ `editor-ready` message sent when initialization complete
1275
+
1276
+ ---
1277
+
1278
+ ## Code Style Guidelines
1279
+
1280
+ Follow the same patterns as the attached `vite-plugin-upstart-attrs.ts`:
1281
+
1282
+ - Use `unplugin` with `createUnplugin`
1283
+ - Use early returns for clarity
1284
+ - Add comprehensive JSDoc comments
1285
+ - Use descriptive variable names
1286
+ - Handle errors gracefully with try-catch
1287
+ - Log with consistent prefix: `[Upstart Editor]`
1288
+ - Use TypeScript strict mode
1289
+ - Export types alongside functions
1290
+ - Use `const` for immutable values
1291
+ - Prefer `async/await` over Promises
1292
+
1293
+ ---
1294
+
1295
+ ## Additional Notes
1296
+
1297
+ - All runtime code must be in actual `.ts` files (NOT template strings)
1298
+ - Use Vite's module resolution during the build process
1299
+ - Keep the plugin build-time logic separate from runtime logic
1300
+ - Make the API extensible for future features (class editing, layout editing, etc.)
1301
+ - Provide clear error messages for debugging
1302
+ - The code should be production-ready and maintainable
1303
+ - The plugin is designed for production builds (`vite build`), not dev server
1304
+ - Each text change requires a full rebuild and iframe reload (no HMR)
1305
+
1306
+ ---
1307
+
1308
+ ## Deliverables
1309
+
1310
+ Please create the following files with complete, production-ready code:
1311
+
1312
+ 1. `src/vite-plugin-upstart-editor/plugin.ts` - Build-time Vite plugin
1313
+ 2. `src/vite-plugin-upstart-editor/runtime/index.ts` - Runtime entry point with mode management
1314
+ 3. `src/vite-plugin-upstart-editor/runtime/text-editor.ts` - TipTap text editing logic
1315
+ 4. `src/vite-plugin-upstart-editor/runtime/click-handler.ts` - Click detection for className editing
1316
+ 5. `src/vite-plugin-upstart-editor/runtime/hover-overlay.ts` - Visual hover overlay
1317
+ 6. `src/vite-plugin-upstart-editor/runtime/types.ts` - TypeScript type definitions
1318
+ 7. `src/vite-plugin-upstart-editor/runtime/utils.ts` - Utility functions for postMessage
1319
+
1320
+ Each file should:
1321
+ - Include comprehensive JSDoc comments
1322
+ - Handle all edge cases mentioned
1323
+ - Follow the code style of existing plugins
1324
+ - Be fully typed with TypeScript
1325
+ - Include error handling and logging
1326
+ - Be ready for production use
1327
+ - Respect preview/edit mode appropriately
1328
+
1329
+ ---
1330
+
1331
+ ## Security Considerations for postMessage
1332
+
1333
+ Since the plugin uses `window.parent.postMessage()` for iframe communication, there are important security considerations:
1334
+
1335
+ ### Development vs Production
1336
+
1337
+ **Development** (current implementation):
1338
+ ```typescript
1339
+ window.parent.postMessage(message, '*'); // Accept any origin
1340
+ ```
1341
+
1342
+ **Production** (future enhancement):
1343
+ ```typescript
1344
+ const ALLOWED_ORIGINS = ['https://editor.upstart.com', 'https://app.upstart.com'];
1345
+ window.parent.postMessage(message, ALLOWED_ORIGINS[0]);
1346
+ ```
1347
+
1348
+ ### Message Validation
1349
+
1350
+ The parent editor should validate incoming messages:
1351
+
1352
+ ```typescript
1353
+ window.addEventListener('message', (event) => {
1354
+ // 1. Check origin (in production)
1355
+ // if (!ALLOWED_ORIGINS.includes(event.origin)) return;
1356
+
1357
+ // 2. Verify message structure
1358
+ if (typeof event.data !== 'object' || !event.data) return;
1359
+
1360
+ // 3. Check source identifier
1361
+ if (event.data.source !== 'upstart-editor') return;
1362
+
1363
+ // 4. Validate message type
1364
+ const validTypes = ['text-update', 'text-save', 'editor-ready', 'editor-error', 'element-hovered'];
1365
+ if (!validTypes.includes(event.data.type)) return;
1366
+
1367
+ // Now safe to process
1368
+ handleEditorMessage(event.data);
1369
+ });
1370
+ ```
1371
+
1372
+ ### Recommendations
1373
+
1374
+ 1. **For now**: Use `'*'` origin for development simplicity
1375
+ 2. **Document**: Add clear comments warning about security implications
1376
+ 3. **Future**: Make origin configurable via plugin options
1377
+ 4. **Parent**: Always validate messages on the parent side
1378
+
1379
+ ---
1380
+
1381
+ ## Questions for Clarification (Optional)
1382
+
1383
+ If you need clarification on any of the following, please ask:
1384
+
1385
+ 1. Should the bubble menu for rich text have specific formatting buttons?
1386
+ 2. Should there be a visual "saving..." indicator during auto-save?
1387
+ 3. Should the editor support undo/redo history?
1388
+ 4. Should there be a setting to disable auto-save?
1389
+ 5. Should keyboard shortcuts be configurable?
1390
+
1391
+ Otherwise, use sensible defaults as specified in this document.