From a1d58ebb29c9a5f0e7e7d15c50835f182a4ee003 Mon Sep 17 00:00:00 2001 From: Kiet <31864905+Kitenite@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:48:23 -0400 Subject: [PATCH] Write code in real-time (#407) --- README.md | 1 + app/electron/main/code/diff/index.ts | 1 - app/electron/main/code/diff/transform.ts | 2 +- app/electron/preload/webview/changes/index.ts | 4 +- app/electron/preload/webview/dom.ts | 2 +- app/electron/preload/webview/elements/text.ts | 14 +- app/electron/preload/webview/events/index.ts | 3 +- app/electron/preload/webview/index.ts | 7 +- app/src/lib/editor/engine/ast/index.ts | 13 +- app/src/lib/editor/engine/code/index.ts | 47 +- app/src/lib/editor/engine/history/index.ts | 2 +- app/src/lib/editor/engine/index.ts | 7 +- app/src/lib/editor/eventHandler.ts | 3 +- .../inputs/primitives/ColorInput/index.tsx | 4 +- .../routes/editor/LayersPanel/LayersTab.tsx | 4 +- app/src/routes/editor/TopBar/index.tsx | 2 - app/src/routes/editor/WebviewArea/Frame.tsx | 1 - demos/next/components/dashboard.tsx | 1129 +++++++++-------- demos/remix/app/routes/_index.tsx | 95 +- 19 files changed, 746 insertions(+), 595 deletions(-) diff --git a/README.md b/README.md index 63188af0b..31b4336d0 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,7 @@ Projects we're inspired by: * [Responsively](https://github.com/responsively-org/responsively-app) * [Supabase](https://github.com/supabase/supabase) * [ShadCN](https://github.com/shadcn-ui/ui) +* [hymhub/css-to-tailwind](https://github.com/hymhub/css-to-tailwind) ## License diff --git a/app/electron/main/code/diff/index.ts b/app/electron/main/code/diff/index.ts index 90455ae69..b1086815d 100644 --- a/app/electron/main/code/diff/index.ts +++ b/app/electron/main/code/diff/index.ts @@ -54,7 +54,6 @@ function processGroupedRequests(groupedRequests: Map): C const generated = generateCode(ast, generateOptions, codeBlock); diffs.push({ original, generated, path }); } - return diffs; } diff --git a/app/electron/main/code/diff/transform.ts b/app/electron/main/code/diff/transform.ts index cdb789e26..199db451e 100644 --- a/app/electron/main/code/diff/transform.ts +++ b/app/electron/main/code/diff/transform.ts @@ -218,6 +218,6 @@ function updateNodeTextContent(node: t.JSXElement, textContent: string): void { if (textNode) { textNode.value = textContent; } else { - console.error('Text node not found'); + node.children.unshift(t.jsxText(textContent)); } } diff --git a/app/electron/preload/webview/changes/index.ts b/app/electron/preload/webview/changes/index.ts index b6e8f5b93..a2f4020d5 100644 --- a/app/electron/preload/webview/changes/index.ts +++ b/app/electron/preload/webview/changes/index.ts @@ -104,7 +104,7 @@ export class CssStyleChange { enter: (decl: Declaration) => { if (decl.property === property) { decl.value = { type: 'Raw', value: value }; - if (value === '' || value === 'none') { + if (value === '') { rule.block.children = rule.block.children.filter( (decl: Declaration) => decl.property !== property, ); @@ -115,7 +115,7 @@ export class CssStyleChange { }); if (!found) { - if (value === '' || value === 'none') { + if (value === '') { rule.block.children = rule.block.children.filter( (decl: Declaration) => decl.property !== property, ); diff --git a/app/electron/preload/webview/dom.ts b/app/electron/preload/webview/dom.ts index 91339e8f7..9d5aa375f 100644 --- a/app/electron/preload/webview/dom.ts +++ b/app/electron/preload/webview/dom.ts @@ -7,7 +7,7 @@ import { LayerNode } from '/common/models/element/layers'; export function processDom(root: HTMLElement = document.body) { const layerTree = buildLayerTree(root); if (!layerTree) { - console.error('Error building layer tree'); + console.error('Error building layer tree, root element is null'); return; } ipcRenderer.sendToHost(WebviewChannels.DOM_READY, layerTree); diff --git a/app/electron/preload/webview/elements/text.ts b/app/electron/preload/webview/elements/text.ts index 915d888b6..75bb069b6 100644 --- a/app/electron/preload/webview/elements/text.ts +++ b/app/electron/preload/webview/elements/text.ts @@ -1,5 +1,10 @@ import { publishEditText } from '../events/publish'; -import { getDomElement, restoreElementStyle, saveTimestamp } from './helpers'; +import { + getDomElement, + getImmediateTextContent, + restoreElementStyle, + saveTimestamp, +} from './helpers'; import { EditorAttributes } from '/common/constants'; import { getUniqueSelector } from '/common/helpers'; import { TextDomElement } from '/common/models/element'; @@ -73,7 +78,7 @@ export function stopEditingText(): void { if (!el) { return; } - cleanUpElementAfterDragging(el); + cleanUpElementAfterEditing(el); publishEditText(getDomElement(el, true)); } @@ -113,7 +118,7 @@ function prepareElementForEditing(el: HTMLElement) { } } -function cleanUpElementAfterDragging(el: HTMLElement) { +function cleanUpElementAfterEditing(el: HTMLElement) { restoreElementStyle(el); removeEditingAttributes(el); saveTimestamp(el); @@ -135,7 +140,8 @@ export function clearTextEditedElements() { function updateTextContent(el: HTMLElement, content: string): void { if (!el.hasAttribute(EditorAttributes.DATA_ONLOOK_ORIGINAL_CONTENT)) { - el.setAttribute(EditorAttributes.DATA_ONLOOK_ORIGINAL_CONTENT, el.textContent || ''); + const originalContent = getImmediateTextContent(el); + el.setAttribute(EditorAttributes.DATA_ONLOOK_ORIGINAL_CONTENT, originalContent || ''); } el.textContent = content; } diff --git a/app/electron/preload/webview/events/index.ts b/app/electron/preload/webview/events/index.ts index 6045d7d96..9f68d905a 100644 --- a/app/electron/preload/webview/events/index.ts +++ b/app/electron/preload/webview/events/index.ts @@ -77,10 +77,9 @@ function listenForEditEvents() { }); ipcRenderer.on(WebviewChannels.CLEAN_AFTER_WRITE_TO_CODE, () => { - change.clearStyleSheet(); removeInsertedElements(); clearMovedElements(); clearTextEditedElements(); - setTimeout(processDom, 500); + processDom(); }); } diff --git a/app/electron/preload/webview/index.ts b/app/electron/preload/webview/index.ts index 38af6e4dc..91f687347 100644 --- a/app/electron/preload/webview/index.ts +++ b/app/electron/preload/webview/index.ts @@ -3,11 +3,16 @@ import { processDom } from './dom'; import { listenForEvents } from './events'; function handleBodyReady() { - processDom(); + keepDomUpdated(); setApi(); listenForEvents(); } +function keepDomUpdated() { + processDom(); + setInterval(() => processDom(), 5000); +} + const handleDocumentBody = setInterval(() => { window.onerror = function logError(errorMsg, url, lineNumber) { console.log(`Unhandled error: ${errorMsg} ${url} ${lineNumber}`); diff --git a/app/src/lib/editor/engine/ast/index.ts b/app/src/lib/editor/engine/ast/index.ts index dc7a6c7b3..176a6cef7 100644 --- a/app/src/lib/editor/engine/ast/index.ts +++ b/app/src/lib/editor/engine/ast/index.ts @@ -8,15 +8,19 @@ import { TemplateNode } from '/common/models/element/templateNode'; export class AstManager { private doc: Document | undefined; - displayLayers: LayerNode[] = []; + private displayLayers: LayerNode[] = []; templateNodeMap: TemplateNodeMap = new TemplateNodeMap(); constructor() { makeAutoObservable(this); } - updateLayers(newLayers: LayerNode[]) { - this.displayLayers = newLayers; + get layers() { + return this.displayLayers; + } + + set layers(layers: LayerNode[]) { + this.displayLayers = layers; } replaceElement(selector: string, newNode: LayerNode) { @@ -78,7 +82,6 @@ export class AstManager { } setMapRoot(rootElement: Element) { - this.clear(); this.setDoc(rootElement.ownerDocument); if (isOnlookInDoc(rootElement.ownerDocument)) { @@ -165,6 +168,6 @@ export class AstManager { clear() { this.templateNodeMap = new TemplateNodeMap(); - this.updateLayers([]); + this.displayLayers = []; } } diff --git a/app/src/lib/editor/engine/code/index.ts b/app/src/lib/editor/engine/code/index.ts index 13b25be30..865b73d38 100644 --- a/app/src/lib/editor/engine/code/index.ts +++ b/app/src/lib/editor/engine/code/index.ts @@ -1,19 +1,36 @@ import { sendAnalytics } from '@/lib/utils'; import { CssToTailwindTranslator, ResultCode } from 'css-to-tailwind-translator'; import { WebviewTag } from 'electron'; +import { debounce } from 'lodash'; +import { makeAutoObservable, reaction } from 'mobx'; import { twMerge } from 'tailwind-merge'; import { AstManager } from '../ast'; +import { HistoryManager } from '../history'; import { WebviewManager } from '../webview'; -import { EditorAttributes, MainChannels } from '/common/constants'; +import { EditorAttributes, MainChannels, WebviewChannels } from '/common/constants'; import { CodeDiff, CodeDiffRequest } from '/common/models/code'; import { InsertedElement, MovedElement, TextEditedElement } from '/common/models/element/domAction'; import { TemplateNode } from '/common/models/element/templateNode'; export class CodeManager { + isExecuting = false; + isQueued = false; + constructor( private webviewManager: WebviewManager, private astManager: AstManager, - ) {} + private historyManager: HistoryManager, + ) { + makeAutoObservable(this); + this.listenForUndoChange(); + } + + listenForUndoChange() { + reaction( + () => this.historyManager.length, + () => this.generateAndWriteCodeDiffs(), + ); + } viewSource(templateNode?: TemplateNode): void { if (!templateNode) { @@ -24,6 +41,32 @@ export class CodeManager { sendAnalytics('view source code'); } + generateAndWriteCodeDiffs = debounce(this.undebouncedGenerateAndWriteCodeDiffs, 1000); + + async undebouncedGenerateAndWriteCodeDiffs(): Promise { + if (this.isExecuting) { + this.isQueued = true; + return; + } + const codeDiffs = await this.generateCodeDiffs(); + if (codeDiffs.length === 0) { + console.error('No code diffs found.'); + return; + } + const res = await window.api.invoke(MainChannels.WRITE_CODE_BLOCKS, codeDiffs); + if (res) { + this.webviewManager.getAll().forEach((webview) => { + webview.send(WebviewChannels.CLEAN_AFTER_WRITE_TO_CODE); + }); + } + + this.isExecuting = false; + if (this.isQueued) { + this.isQueued = false; + this.generateAndWriteCodeDiffs(); + } + } + async generateCodeDiffs(): Promise { const webviews = [...this.webviewManager.getAll().values()]; if (webviews.length === 0) { diff --git a/app/src/lib/editor/engine/history/index.ts b/app/src/lib/editor/engine/history/index.ts index b4d2dd2e1..809b5634b 100644 --- a/app/src/lib/editor/engine/history/index.ts +++ b/app/src/lib/editor/engine/history/index.ts @@ -122,7 +122,7 @@ export class HistoryManager { switch (action.type) { case 'update-style': - sendAnalytics('edit action', { + sendAnalytics('style action', { type: action.type, style: action.style, new_value: action.change.updated, diff --git a/app/src/lib/editor/engine/index.ts b/app/src/lib/editor/engine/index.ts index 61033f40d..6d07049ca 100644 --- a/app/src/lib/editor/engine/index.ts +++ b/app/src/lib/editor/engine/index.ts @@ -30,7 +30,6 @@ export class EditorEngine { private projectInfoManager: ProjectInfoManager = new ProjectInfoManager(); private canvasManager: CanvasManager; private domManager: DomManager = new DomManager(this.astManager); - private codeManager: CodeManager = new CodeManager(this.webviewManager, this.astManager); private elementManager: ElementManager = new ElementManager( this.overlayManager, this.astManager, @@ -50,6 +49,11 @@ export class EditorEngine { this.historyManager, this.astManager, ); + private codeManager: CodeManager = new CodeManager( + this.webviewManager, + this.astManager, + this.historyManager, + ); constructor(private projectsManager: ProjectsManager) { makeAutoObservable(this); @@ -136,7 +140,6 @@ export class EditorEngine { } async refreshLayers() { - this.ast.clear(); const webviews = this.webviews.webviews; if (webviews.size === 0) { return; diff --git a/app/src/lib/editor/eventHandler.ts b/app/src/lib/editor/eventHandler.ts index 0e1a1a522..164b6adc5 100644 --- a/app/src/lib/editor/eventHandler.ts +++ b/app/src/lib/editor/eventHandler.ts @@ -32,11 +32,10 @@ export class WebviewEventHandler { console.error('No args found for dom ready event'); return; } - this.editorEngine.ast.clear(); const body = await this.editorEngine.dom.getBodyFromWebview(webview); this.editorEngine.dom.setDom(webview.id, body); const layerTree = e.args[0] as LayerNode; - this.editorEngine.ast.updateLayers([layerTree as LayerNode]); + this.editorEngine.ast.layers = [layerTree as LayerNode]; }; } diff --git a/app/src/routes/editor/EditPanel/inputs/primitives/ColorInput/index.tsx b/app/src/routes/editor/EditPanel/inputs/primitives/ColorInput/index.tsx index 212f715c1..ad8a0102e 100644 --- a/app/src/routes/editor/EditPanel/inputs/primitives/ColorInput/index.tsx +++ b/app/src/routes/editor/EditPanel/inputs/primitives/ColorInput/index.tsx @@ -26,7 +26,7 @@ const ColorInput = observer( }, [elementStyle]); function isNoneInput() { - return inputString === 'initial' || inputString === ''; + return inputString === 'initial' || inputString === '' || inputString === 'transparent'; } function sendStyleUpdate(newValue: string) { @@ -71,7 +71,7 @@ const ColorInput = observer( - - - - - - - - - - Dashboard - - - - - - Orders - - - - - Recent Orders - - - -
- - -
- - - - - - My Account - - Settings - Support - - Logout - - - -
-
-
- - - Your Orders - - Introducing Our Dynamic Orders Dashboard for Seamless - Management and Insightful Analysis. - - - - - - - - - This Week - $1,329 - - -
- +25% from last week -
-
- - - -
- - - This Month - $5,329 - - -
- +10% from last month -
-
- - - -
+ return ( +
+ + + + + +
+
+ + + + + + + + + + + + + Dashboard + + + + + + Orders + + + + + Recent Orders + + + +
+ +
- -
- - Week - Month - Year - -
- - - - - - Filter by - - - Fulfilled - - - Declined - - - Refunded - - - - -
-
- - - - Orders - - Recent orders from your store. + + + + + + My Account + + Settings + Support + + Logout + + +
+
+
+
+ + + Your Orders + + Introducing Our Dynamic Orders Dashboard for Seamless + Management and Insightful Analysis. + + + + + + + This Week + $1,329 + + +
+ +25% from last week +
+
+ + + +
+ + + This Month + $5,329 + - - - - Customer - - Type - - - Status - - - Date - - Amount - - - - - -
Liam Johnson
-
- liam@example.com -
-
- - Sale - - - - Fulfilled - - - - 2023-06-23 - - $250.00 -
- - -
Olivia Smith
-
- olivia@example.com -
-
- - Refund - - - - Declined - - - - 2023-06-24 - - $150.00 -
- - -
Noah Williams
-
- noah@example.com -
-
- - Subscription - - - - Fulfilled - - - - 2023-06-25 - - $350.00 -
- - -
Emma Brown
-
- emma@example.com -
-
- - Sale - - - - Fulfilled - - - - 2023-06-26 - - $450.00 -
- - -
Liam Johnson
-
- liam@example.com -
-
- - Sale - - - - Fulfilled - - - - 2023-06-23 - - $250.00 -
- - -
Liam Johnson
-
- liam@example.com -
-
- - Sale - - - - Fulfilled - - - - 2023-06-23 - - $250.00 -
- - -
Olivia Smith
-
- olivia@example.com -
-
- - Refund - - - - Declined - - - - 2023-06-24 - - $150.00 -
- - -
Emma Brown
-
- emma@example.com -
-
- - Sale - - - - Fulfilled - - - - 2023-06-26 - - $450.00 -
-
-
+
+ +10% from last month +
+ + +
- - -
-
- - -
- - Order Oe31b70H -
+ +
+ + Week + Month + Year + +
+ + + + + + Filter by + + + Fulfilled + + + Declined + + + Refunded + + + + - - Date: November 23, 2023 +
-
- - - -
+
+ + +
+ + Order Oe31b70H + - - - Edit - Export - - Trash - - -
-
- -
-
Order Details
-
    -
  • - - Glimmer Lamps x2 - - $250.00 -
  • -
  • - - Aqua Filters x1 + + Date: November 23, 2023 +
+
+
- -
-
-
Shipping Information
-
- Liam Johnson - 1234 Main St. - Anytown, CA 12345 -
+ + + + + + + Edit + Export + + Trash + +
-
-
Billing Information
-
- Same as shipping address -
+ + +
+
Order Details
+
    +
  • + + Glimmer Lamps x2 + + $250.00 +
  • +
  • + + Aqua Filters x1 + + $49.00 +
  • +
+ +
    +
  • + Subtotal + $299.00 +
  • +
  • + Shipping + $5.00 +
  • +
  • + Tax + $25.00 +
  • +
  • + Total + $329.00 +
  • +
-
- -
-
Customer Information
-
-
-
Customer
-
Liam Johnson
-
-
-
Email
-
- liam@acme.com -
+ +
+
+
Shipping Information
+
+ Liam Johnson + 1234 Main St. + Anytown, CA 12345 +
-
-
Phone
-
- +1 234 567 890 -
+
+
Billing Information
+
+ Same as shipping address +
-
-
- -
-
Payment Information
-
-
-
- - Visa -
-
**** **** **** 4532
-
-
-
- - -
- Updated - -
- - - - - - - - - - -
- -
-
+
+ +
+
Customer Information
+
+
+
Customer
+
Liam Johnson
+
+
+
Email
+
+ liam@acme.com +
+
+
+
Phone
+
+ +1 234 567 890 +
+
+
+
+ +
+
Payment Information
+
+
+
+ + Visa +
+
**** **** **** 4532
+
+
+
+ + +
+ Updated + +
+ + + + + + + + + + +
+ +
+
+ - ; + ) } diff --git a/demos/remix/app/routes/_index.tsx b/demos/remix/app/routes/_index.tsx index e6d8f0177..310346031 100644 --- a/demos/remix/app/routes/_index.tsx +++ b/demos/remix/app/routes/_index.tsx @@ -2,54 +2,55 @@ import type { MetaFunction } from "@remix-run/node"; import React, { Fragment } from "react"; export const meta: MetaFunction = () => { - return [ - { title: "New Remix App" }, - { name: "description", content: "Welcome to Remix!" }, - ]; + return [ + { title: "New Remix App" }, + { name: "description", content: "Welcome to Remix!" }, + ]; }; export default function Index() { - return ( -
- <> - - -

Welcome to Remix

-
-
- - -
- ); + return ( +
+ <> + + +

Welcome to Remix

+
+
+ + +
+ ); }