@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.
- package/dist/vite-plugin-upstart-attrs.d.ts +29 -0
- package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-attrs.js +323 -0
- package/dist/vite-plugin-upstart-attrs.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/plugin.d.ts +15 -0
- package/dist/vite-plugin-upstart-editor/plugin.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-editor/plugin.js +55 -0
- package/dist/vite-plugin-upstart-editor/plugin.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.d.ts +12 -0
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +57 -0
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.d.ts +12 -0
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js +91 -0
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/index.d.ts +22 -0
- package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/index.js +62 -0
- package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts +15 -0
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.js +292 -0
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +126 -0
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/types.js +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/utils.d.ts +15 -0
- package/dist/vite-plugin-upstart-editor/runtime/utils.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/utils.js +26 -0
- package/dist/vite-plugin-upstart-editor/runtime/utils.js.map +1 -0
- package/dist/vite-plugin-upstart-theme.d.ts +22 -0
- package/dist/vite-plugin-upstart-theme.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-theme.js +179 -0
- package/dist/vite-plugin-upstart-theme.js.map +1 -0
- package/package.json +63 -0
- package/src/tests/fixtures/routes/default-layout.tsx +10 -0
- package/src/tests/fixtures/routes/dynamic-route.tsx +10 -0
- package/src/tests/fixtures/routes/missing-attributes.tsx +8 -0
- package/src/tests/fixtures/routes/missing-path.tsx +9 -0
- package/src/tests/fixtures/routes/valid-full.tsx +15 -0
- package/src/tests/fixtures/routes/valid-minimal.tsx +10 -0
- package/src/tests/fixtures/routes/with-comments.tsx +12 -0
- package/src/tests/fixtures/routes/with-nested-objects.tsx +15 -0
- package/src/tests/upstart-editor-api.test.ts +367 -0
- package/src/tests/vite-plugin-upstart-attrs.test.ts +1189 -0
- package/src/tests/vite-plugin-upstart-editor.test.ts +81 -0
- package/src/upstart-editor-api.ts +204 -0
- package/src/vite-plugin-upstart-attrs.ts +708 -0
- package/src/vite-plugin-upstart-editor/PLAN.md +1391 -0
- package/src/vite-plugin-upstart-editor/plugin.ts +73 -0
- package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +80 -0
- package/src/vite-plugin-upstart-editor/runtime/hover-overlay.ts +135 -0
- package/src/vite-plugin-upstart-editor/runtime/index.ts +90 -0
- package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +401 -0
- package/src/vite-plugin-upstart-editor/runtime/types.ts +120 -0
- package/src/vite-plugin-upstart-editor/runtime/utils.ts +34 -0
- 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.
|