@upstart.gg/vite-plugins 0.0.139

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 (63) 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 +286 -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-routes.d.ts +20 -0
  33. package/dist/vite-plugin-upstart-routes.d.ts.map +1 -0
  34. package/dist/vite-plugin-upstart-routes.js +143 -0
  35. package/dist/vite-plugin-upstart-routes.js.map +1 -0
  36. package/dist/vite-plugin-upstart-theme.d.ts +22 -0
  37. package/dist/vite-plugin-upstart-theme.d.ts.map +1 -0
  38. package/dist/vite-plugin-upstart-theme.js +180 -0
  39. package/dist/vite-plugin-upstart-theme.js.map +1 -0
  40. package/package.json +63 -0
  41. package/src/tests/fixtures/routes/default-layout.tsx +10 -0
  42. package/src/tests/fixtures/routes/dynamic-route.tsx +10 -0
  43. package/src/tests/fixtures/routes/missing-attributes.tsx +8 -0
  44. package/src/tests/fixtures/routes/missing-path.tsx +9 -0
  45. package/src/tests/fixtures/routes/valid-full.tsx +15 -0
  46. package/src/tests/fixtures/routes/valid-minimal.tsx +10 -0
  47. package/src/tests/fixtures/routes/with-comments.tsx +12 -0
  48. package/src/tests/fixtures/routes/with-nested-objects.tsx +15 -0
  49. package/src/tests/upstart-editor-api.test.ts +367 -0
  50. package/src/tests/vite-plugin-upstart-attrs.test.ts +957 -0
  51. package/src/tests/vite-plugin-upstart-editor.test.ts +81 -0
  52. package/src/tests/vite-plugin-upstart-routes.test.ts +220 -0
  53. package/src/upstart-editor-api.ts +204 -0
  54. package/src/vite-plugin-upstart-attrs.ts +616 -0
  55. package/src/vite-plugin-upstart-editor/PLAN.md +1391 -0
  56. package/src/vite-plugin-upstart-editor/plugin.ts +73 -0
  57. package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +80 -0
  58. package/src/vite-plugin-upstart-editor/runtime/hover-overlay.ts +135 -0
  59. package/src/vite-plugin-upstart-editor/runtime/index.ts +90 -0
  60. package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +401 -0
  61. package/src/vite-plugin-upstart-editor/runtime/types.ts +120 -0
  62. package/src/vite-plugin-upstart-editor/runtime/utils.ts +34 -0
  63. package/src/vite-plugin-upstart-theme.ts +321 -0
@@ -0,0 +1,401 @@
1
+ import { Editor } from "@tiptap/core";
2
+ import type { Extension } from "@tiptap/core";
3
+ import { BubbleMenu } from "@tiptap/extension-bubble-menu";
4
+ import Placeholder from "@tiptap/extension-placeholder";
5
+ import StarterKit from "@tiptap/starter-kit";
6
+ import { getCurrentMode } from "./index.js";
7
+ import { sendToParent } from "./utils.js";
8
+ import type { EditorInstance, UpstartEditorOptions } from "./types.js";
9
+
10
+ const DEFAULT_OPTIONS: Required<UpstartEditorOptions> = {
11
+ richTextElements: ["p", "div", "article", "section"],
12
+ plainTextElements: ["button", "a", "span", "h1", "h2", "h3", "h4", "h5", "h6", "label"],
13
+ bubbleMenu: true,
14
+ placeholder: "Start typing...",
15
+ autoSaveDelay: 1000,
16
+ };
17
+
18
+ const activeEditors = new Map<string, EditorInstance>();
19
+ const saveTimeouts = new Map<string, number>();
20
+ const styleCache = new WeakMap<
21
+ HTMLElement,
22
+ { outline: string; outlineOffset: string; cursor: string; transition: string }
23
+ >();
24
+ let isInitialized = false;
25
+ let shortcutsRegistered = false;
26
+
27
+ /**
28
+ * Initialize TipTap text editing for elements marked as editable.
29
+ */
30
+ export function initTextEditor(options: UpstartEditorOptions = {}): void {
31
+ if (typeof window === "undefined") {
32
+ return;
33
+ }
34
+
35
+ if (isInitialized) {
36
+ return;
37
+ }
38
+
39
+ try {
40
+ const resolvedOptions = { ...DEFAULT_OPTIONS, ...options };
41
+ const editables = document.querySelectorAll<HTMLElement>('[data-upstart-editable-text="true"]');
42
+
43
+ editables.forEach((element) => setupEditableElement(element, resolvedOptions));
44
+
45
+ registerKeyboardShortcuts();
46
+ isInitialized = true;
47
+
48
+ console.log("[Upstart Editor] Text editor initialized");
49
+ } catch (error) {
50
+ console.error("[Upstart Editor] Failed to initialize text editor:", error);
51
+ sendToParent({
52
+ type: "editor-error",
53
+ error: error instanceof Error ? error.message : "Unknown error",
54
+ });
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Destroy all active editors.
60
+ */
61
+ export function destroyAllActiveEditors(): void {
62
+ for (const hash of activeEditors.keys()) {
63
+ destroyEditor(hash);
64
+ }
65
+ }
66
+
67
+ function setupEditableElement(element: HTMLElement, options: Required<UpstartEditorOptions>): void {
68
+ const hash = getEditableHash(element);
69
+ if (!hash) {
70
+ return;
71
+ }
72
+
73
+ cacheStyles(element);
74
+ element.style.transition = "outline 0.15s ease";
75
+
76
+ element.addEventListener("dblclick", (event) => {
77
+ if (getCurrentMode() !== "edit") {
78
+ return;
79
+ }
80
+
81
+ event.preventDefault();
82
+ event.stopPropagation();
83
+
84
+ if (activeEditors.has(hash)) {
85
+ return;
86
+ }
87
+
88
+ activateEditor(element, hash, options);
89
+ });
90
+
91
+ element.addEventListener("mouseenter", () => {
92
+ if (getCurrentMode() !== "edit") {
93
+ return;
94
+ }
95
+
96
+ if (!activeEditors.has(hash)) {
97
+ applyHoverStyles(element);
98
+ }
99
+ });
100
+
101
+ element.addEventListener("mouseleave", () => {
102
+ if (!activeEditors.has(hash)) {
103
+ restoreStyles(element);
104
+ }
105
+ });
106
+ }
107
+
108
+ function activateEditor(element: HTMLElement, hash: string, options: Required<UpstartEditorOptions>): void {
109
+ const isRichText = shouldUseRichText(element, options);
110
+ const editor = isRichText
111
+ ? createRichTextEditor(element, hash, options)
112
+ : createPlainTextEditor(element, hash, options);
113
+
114
+ applyActiveStyles(element);
115
+ activeEditors.set(hash, { editor, element, hash });
116
+ }
117
+
118
+ function createPlainTextEditor(
119
+ element: HTMLElement,
120
+ hash: string,
121
+ options: Required<UpstartEditorOptions>,
122
+ ): Editor {
123
+ return new Editor({
124
+ element,
125
+ extensions: [
126
+ StarterKit.configure({
127
+ heading: false,
128
+ bold: false,
129
+ italic: false,
130
+ strike: false,
131
+ blockquote: false,
132
+ bulletList: false,
133
+ orderedList: false,
134
+ listItem: false,
135
+ codeBlock: false,
136
+ horizontalRule: false,
137
+ }),
138
+ Placeholder.configure({
139
+ placeholder: "Click to edit...",
140
+ }),
141
+ ],
142
+ content: element.textContent ?? "",
143
+ editorProps: {
144
+ attributes: {
145
+ class: "upstart-editor-active",
146
+ style: "outline: 2px solid #3b82f6; outline-offset: 2px;",
147
+ },
148
+ },
149
+ onUpdate: ({ editor }) => {
150
+ debouncedSave(hash, editor.getText(), options.autoSaveDelay);
151
+ },
152
+ onBlur: ({ editor }) => {
153
+ saveText(hash, editor.getText());
154
+ destroyEditor(hash);
155
+ },
156
+ onCreate: ({ editor }) => {
157
+ editor.commands.focus("end");
158
+ },
159
+ });
160
+ }
161
+
162
+ function createRichTextEditor(
163
+ element: HTMLElement,
164
+ hash: string,
165
+ options: Required<UpstartEditorOptions>,
166
+ ): Editor {
167
+ const bubbleMenuElement = createBubbleMenuElement();
168
+ const extensions: Extension[] = [
169
+ StarterKit,
170
+ Placeholder.configure({
171
+ placeholder: options.placeholder,
172
+ }),
173
+ ];
174
+
175
+ if (options.bubbleMenu) {
176
+ extensions.push(
177
+ BubbleMenu.configure({
178
+ element: bubbleMenuElement,
179
+ }),
180
+ );
181
+ }
182
+
183
+ const editor = new Editor({
184
+ element,
185
+ extensions,
186
+ content: element.innerHTML,
187
+ editorProps: {
188
+ attributes: {
189
+ class: "upstart-editor-active",
190
+ style: "outline: 2px solid #3b82f6; outline-offset: 2px;",
191
+ },
192
+ },
193
+ onUpdate: ({ editor: updatedEditor }) => {
194
+ debouncedSave(hash, updatedEditor.getHTML(), options.autoSaveDelay);
195
+ },
196
+ onBlur: ({ editor: updatedEditor }) => {
197
+ saveText(hash, updatedEditor.getHTML());
198
+ destroyEditor(hash);
199
+ },
200
+ onCreate: ({ editor: createdEditor }) => {
201
+ if (options.bubbleMenu) {
202
+ wireBubbleMenu(bubbleMenuElement, createdEditor);
203
+ }
204
+ createdEditor.commands.focus("end");
205
+ },
206
+ });
207
+
208
+ return editor;
209
+ }
210
+
211
+ function shouldUseRichText(element: HTMLElement, options: Required<UpstartEditorOptions>): boolean {
212
+ const tagName = element.tagName.toLowerCase();
213
+
214
+ if (options.plainTextElements.includes(tagName)) {
215
+ return false;
216
+ }
217
+
218
+ if (options.richTextElements.includes(tagName)) {
219
+ return true;
220
+ }
221
+
222
+ return true;
223
+ }
224
+
225
+ function debouncedSave(hash: string, content: string, delay: number): void {
226
+ const existingTimeout = saveTimeouts.get(hash);
227
+ if (existingTimeout) {
228
+ clearTimeout(existingTimeout);
229
+ }
230
+
231
+ const timeoutId = window.setTimeout(() => {
232
+ sendToParent({
233
+ type: "text-update",
234
+ hash,
235
+ newText: content,
236
+ });
237
+ }, delay);
238
+
239
+ saveTimeouts.set(hash, timeoutId);
240
+ }
241
+
242
+ function saveText(hash: string, newText: string): void {
243
+ try {
244
+ sendToParent({
245
+ type: "text-save",
246
+ hash,
247
+ newText,
248
+ });
249
+
250
+ console.log("[Upstart Editor] Text save message sent:", hash);
251
+ } catch (error) {
252
+ console.error("[Upstart Editor] Failed to send save message:", error);
253
+ sendToParent({
254
+ type: "editor-error",
255
+ error: error instanceof Error ? error.message : "Unknown error",
256
+ });
257
+ }
258
+ }
259
+
260
+ function destroyEditor(hash: string): void {
261
+ const instance = activeEditors.get(hash);
262
+ if (!instance) {
263
+ return;
264
+ }
265
+
266
+ instance.editor.destroy();
267
+ restoreStyles(instance.element);
268
+ activeEditors.delete(hash);
269
+
270
+ const existingTimeout = saveTimeouts.get(hash);
271
+ if (existingTimeout) {
272
+ clearTimeout(existingTimeout);
273
+ saveTimeouts.delete(hash);
274
+ }
275
+ }
276
+
277
+ function registerKeyboardShortcuts(): void {
278
+ if (shortcutsRegistered) {
279
+ return;
280
+ }
281
+
282
+ document.addEventListener("keydown", (event) => {
283
+ if (event.key === "Escape") {
284
+ blurAllEditors();
285
+ }
286
+
287
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
288
+ blurAllEditors();
289
+ }
290
+ });
291
+
292
+ shortcutsRegistered = true;
293
+ }
294
+
295
+ function blurAllEditors(): void {
296
+ activeEditors.forEach((instance) => {
297
+ instance.editor.commands.blur();
298
+ });
299
+ }
300
+
301
+ function getEditableHash(element: HTMLElement): string | null {
302
+ return element.dataset.upstartHash ?? null;
303
+ }
304
+
305
+ function cacheStyles(element: HTMLElement): void {
306
+ if (styleCache.has(element)) {
307
+ return;
308
+ }
309
+
310
+ styleCache.set(element, {
311
+ outline: element.style.outline || "",
312
+ outlineOffset: element.style.outlineOffset || "",
313
+ cursor: element.style.cursor || "",
314
+ transition: element.style.transition || "",
315
+ });
316
+ }
317
+
318
+ function applyHoverStyles(element: HTMLElement): void {
319
+ cacheStyles(element);
320
+ element.style.outline = "1px dashed #3b82f6";
321
+ element.style.outlineOffset = "2px";
322
+ element.style.cursor = "text";
323
+ }
324
+
325
+ function applyActiveStyles(element: HTMLElement): void {
326
+ cacheStyles(element);
327
+ element.style.outline = "2px solid #3b82f6";
328
+ element.style.outlineOffset = "2px";
329
+ element.style.cursor = "text";
330
+ }
331
+
332
+ function restoreStyles(element: HTMLElement): void {
333
+ const cached = styleCache.get(element);
334
+ if (!cached) {
335
+ element.style.outline = "";
336
+ element.style.outlineOffset = "";
337
+ element.style.cursor = "";
338
+ element.style.transition = "";
339
+ return;
340
+ }
341
+
342
+ element.style.outline = cached.outline;
343
+ element.style.outlineOffset = cached.outlineOffset;
344
+ element.style.cursor = cached.cursor;
345
+ element.style.transition = cached.transition;
346
+ }
347
+
348
+ function createBubbleMenuElement(): HTMLDivElement {
349
+ const menu = document.createElement("div");
350
+ menu.className = "upstart-editor-bubble-menu";
351
+ menu.style.cssText =
352
+ "display: flex; gap: 6px; padding: 6px 8px; background: #111827; color: #ffffff; " +
353
+ "border-radius: 6px; font-size: 12px; box-shadow: 0 6px 16px rgba(0,0,0,0.2);";
354
+
355
+ const buttons = [
356
+ { label: "B", command: "bold" },
357
+ { label: "I", command: "italic" },
358
+ { label: "S", command: "strike" },
359
+ ];
360
+
361
+ for (const { label, command } of buttons) {
362
+ const button = document.createElement("button");
363
+ button.type = "button";
364
+ button.textContent = label;
365
+ button.dataset.command = command;
366
+ button.style.cssText =
367
+ "background: transparent; border: none; color: inherit; cursor: pointer; font-weight: 600;";
368
+ menu.appendChild(button);
369
+ }
370
+
371
+ return menu;
372
+ }
373
+
374
+ function wireBubbleMenu(menu: HTMLDivElement, editor: Editor): void {
375
+ menu.addEventListener("mousedown", (event) => {
376
+ event.preventDefault();
377
+ });
378
+
379
+ menu.addEventListener("click", (event) => {
380
+ const target = event.target as HTMLElement | null;
381
+ const command = target?.dataset.command;
382
+
383
+ if (!command) {
384
+ return;
385
+ }
386
+
387
+ const chain = editor.chain().focus();
388
+
389
+ if (command === "bold") {
390
+ chain.toggleBold().run();
391
+ }
392
+
393
+ if (command === "italic") {
394
+ chain.toggleItalic().run();
395
+ }
396
+
397
+ if (command === "strike") {
398
+ chain.toggleStrike().run();
399
+ }
400
+ });
401
+ }
@@ -0,0 +1,120 @@
1
+ import type { Editor } from "@tiptap/core";
2
+
3
+ /**
4
+ * Editor mode
5
+ */
6
+ export type EditorMode = "preview" | "edit";
7
+
8
+ /**
9
+ * Element bounds used for UI positioning.
10
+ */
11
+ export interface ElementBounds {
12
+ top: number;
13
+ left: number;
14
+ width: number;
15
+ height: number;
16
+ right: number;
17
+ bottom: number;
18
+ }
19
+
20
+ /**
21
+ * Represents an active editor instance.
22
+ */
23
+ export interface EditorInstance {
24
+ editor: Editor;
25
+ element: HTMLElement;
26
+ hash: string;
27
+ }
28
+
29
+ /**
30
+ * Messages sent from iframe to parent editor.
31
+ */
32
+ export type EditorMessage =
33
+ | { type: "text-update"; hash: string; newText: string }
34
+ | { type: "text-save"; hash: string; newText: string }
35
+ | { type: "editor-ready" }
36
+ | { type: "editor-error"; error: string }
37
+ | { type: "element-hovered"; hash: string; bounds: ElementBounds }
38
+ | {
39
+ type: "element-clicked";
40
+ hash: string;
41
+ componentName: string;
42
+ filePath: string;
43
+ currentClassName: string;
44
+ bounds: ElementBounds;
45
+ };
46
+
47
+ /**
48
+ * Messages sent from parent to iframe.
49
+ */
50
+ export type ParentMessage = { type: "set-mode"; mode: EditorMode };
51
+
52
+ /**
53
+ * Message wrapper sent via postMessage from iframe.
54
+ */
55
+ export interface UpstartEditorMessage {
56
+ source: "upstart-editor";
57
+ type: EditorMessage["type"];
58
+ [key: string]: any;
59
+ }
60
+
61
+ /**
62
+ * Message wrapper sent via postMessage from parent.
63
+ */
64
+ export interface UpstartParentMessage {
65
+ source: "upstart-editor-parent";
66
+ type: ParentMessage["type"];
67
+ [key: string]: any;
68
+ }
69
+
70
+ /**
71
+ * Configuration options for the Upstart Editor runtime.
72
+ */
73
+ export interface UpstartEditorOptions {
74
+ /**
75
+ * Elements that should use rich text editing (with formatting).
76
+ * @default ['p', 'div', 'article', 'section']
77
+ */
78
+ richTextElements?: string[];
79
+
80
+ /**
81
+ * Elements that should use plain text editing (no formatting).
82
+ * @default ['button', 'a', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'label']
83
+ */
84
+ plainTextElements?: string[];
85
+
86
+ /**
87
+ * Enable bubble menu for text selection.
88
+ * @default true
89
+ */
90
+ bubbleMenu?: boolean;
91
+
92
+ /**
93
+ * Placeholder text for empty editors.
94
+ * @default 'Start typing...'
95
+ */
96
+ placeholder?: string;
97
+
98
+ /**
99
+ * Auto-save delay in milliseconds.
100
+ * @default 1000
101
+ */
102
+ autoSaveDelay?: number;
103
+ }
104
+
105
+ /**
106
+ * Build-time plugin options.
107
+ */
108
+ export interface UpstartEditorPluginOptions {
109
+ /**
110
+ * Enable or disable the editor.
111
+ * @default false
112
+ */
113
+ enabled?: boolean;
114
+
115
+ /**
116
+ * Automatically inject editor initialization code.
117
+ * @default true
118
+ */
119
+ autoInject?: boolean;
120
+ }
@@ -0,0 +1,34 @@
1
+ import type { EditorMessage } from "./types";
2
+
3
+ /**
4
+ * Send a message to the parent window.
5
+ */
6
+ export function sendToParent(message: EditorMessage): void {
7
+ if (typeof window === "undefined") {
8
+ return;
9
+ }
10
+
11
+ if (window.parent === window) {
12
+ console.warn("[Upstart Editor] Not running in iframe, cannot send message:", message);
13
+ return;
14
+ }
15
+
16
+ window.parent.postMessage(
17
+ {
18
+ source: "upstart-editor",
19
+ ...message,
20
+ },
21
+ "*",
22
+ );
23
+ }
24
+
25
+ /**
26
+ * Check if running inside an iframe.
27
+ */
28
+ export function isInIframe(): boolean {
29
+ if (typeof window === "undefined") {
30
+ return false;
31
+ }
32
+
33
+ return window.parent !== window;
34
+ }