diff --git a/frontend/app/app.tsx b/frontend/app/app.tsx index 2b1cb274..d012c390 100644 --- a/frontend/app/app.tsx +++ b/frontend/app/app.tsx @@ -8,48 +8,41 @@ import * as rx from "rxjs"; import { clsx } from "clsx"; import { TabContent } from "@/app/tab/tab"; import { v4 as uuidv4 } from "uuid"; +import { globalStore, atoms } from "@/store/global"; import "/public/style.less"; -const jotaiStore = jotai.createStore(); - -const tabArr = [ - { name: "Tab 1", tabid: uuidv4() }, - { name: "Tab 2", tabid: uuidv4() }, - { name: "Tab 3", tabid: uuidv4() }, -]; - -const activeTabIdAtom = jotai.atom(tabArr[0].tabid); - const App = () => { return ( - + ); }; +const Tab = ({ tab }: { tab: TabData }) => { + const [activeTab, setActiveTab] = jotai.useAtom(atoms.activeTabId); + return ( +
setActiveTab(tab.tabid)}> + {tab.name} +
+ ); +}; + const TabBar = () => { - const [activeTab, setActiveTab] = jotai.useAtom(activeTabIdAtom); + const [activeTab, setActiveTab] = jotai.useAtom(atoms.activeTabId); + const tabs = jotai.useAtomValue(atoms.tabsAtom); return (
- {tabArr.map((tab, idx) => { - return ( -
setActiveTab(tab.tabid)} - > - {tab.name} -
- ); + {tabs.map((tab, idx) => { + return ; })}
); }; const Workspace = () => { - const activeTabId = jotai.useAtomValue(activeTabIdAtom); + const activeTabId = jotai.useAtomValue(atoms.activeTabId); return (
diff --git a/frontend/app/block/block.less b/frontend/app/block/block.less index a77bf775..07d35b71 100644 --- a/frontend/app/block/block.less +++ b/frontend/app/block/block.less @@ -10,23 +10,22 @@ height: 100%; overflow: hidden; min-height: 0; -} -.block .block-header { - display: flex; - flex-shrink: 0; - height: 30px; - width: 100%; - align-items: center; - justify-content: center; - background-color: var(--panel-bg-color); -} + .block-header { + display: flex; + flex-shrink: 0; + height: 30px; + width: 100%; + align-items: center; + justify-content: center; + background-color: var(--panel-bg-color); + } -.block .block-term-connectelem { - display: flex; - flex-grow: 1; - width: 100%; - background-color: green; - overflow: hidden; - min-height: 0; + .block-content { + display: flex; + flex-grow: 1; + width: 100%; + overflow: hidden; + min-height: 0; + } } diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index 7eabf973..22a00fac 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -3,74 +3,35 @@ import * as React from "react"; import * as jotai from "jotai"; -import { Terminal } from "@xterm/xterm"; -import type { ITheme } from "@xterm/xterm"; -import { FitAddon } from "@xterm/addon-fit"; +import { atoms } from "@/store/global"; + +import { TerminalView } from "@/app/view/term"; +import { PreviewView } from "@/app/view/preview"; -import "/public/xterm.css"; import "./block.less"; -function getThemeFromCSSVars(el: Element): ITheme { - const theme: ITheme = {}; - const elemStyle = getComputedStyle(el); - theme.foreground = elemStyle.getPropertyValue("--term-foreground"); - theme.background = elemStyle.getPropertyValue("--term-background"); - theme.black = elemStyle.getPropertyValue("--term-black"); - theme.red = elemStyle.getPropertyValue("--term-red"); - theme.green = elemStyle.getPropertyValue("--term-green"); - theme.yellow = elemStyle.getPropertyValue("--term-yellow"); - theme.blue = elemStyle.getPropertyValue("--term-blue"); - theme.magenta = elemStyle.getPropertyValue("--term-magenta"); - theme.cyan = elemStyle.getPropertyValue("--term-cyan"); - theme.white = elemStyle.getPropertyValue("--term-white"); - theme.brightBlack = elemStyle.getPropertyValue("--term-bright-black"); - theme.brightRed = elemStyle.getPropertyValue("--term-bright-red"); - theme.brightGreen = elemStyle.getPropertyValue("--term-bright-green"); - theme.brightYellow = elemStyle.getPropertyValue("--term-bright-yellow"); - theme.brightBlue = elemStyle.getPropertyValue("--term-bright-blue"); - theme.brightMagenta = elemStyle.getPropertyValue("--term-bright-magenta"); - theme.brightCyan = elemStyle.getPropertyValue("--term-bright-cyan"); - theme.brightWhite = elemStyle.getPropertyValue("--term-bright-white"); - theme.selectionBackground = elemStyle.getPropertyValue("--term-selection-background"); - theme.selectionInactiveBackground = elemStyle.getPropertyValue("--term-selection-background"); - theme.cursor = elemStyle.getPropertyValue("--term-selection-background"); - theme.cursorAccent = elemStyle.getPropertyValue("--term-cursor-accent"); - return theme; -} - const Block = ({ blockId }: { blockId: string }) => { const blockRef = React.useRef(null); - const connectElemRef = React.useRef(null); const [dims, setDims] = React.useState({ width: 0, height: 0 }); React.useEffect(() => { if (!blockRef.current) { return; } const rect = blockRef.current.getBoundingClientRect(); - const newWidth = parseInt(rect.width); - const newHeight = parseInt(rect.height); + const newWidth = Math.floor(rect.width); + const newHeight = Math.floor(rect.height); if (newWidth !== dims.width || newHeight !== dims.height) { setDims({ width: newWidth, height: newHeight }); } }, [blockRef.current]); - React.useEffect(() => { - const term = new Terminal({ - theme: getThemeFromCSSVars(blockRef.current), - fontSize: 12, - fontFamily: "Hack", - drawBoldTextInBrightColors: false, - fontWeight: "normal", - fontWeightBold: "bold", - }); - const fitAddon = new FitAddon(); - term.loadAddon(fitAddon); - term.open(connectElemRef.current); - fitAddon.fit(); - term.write("Hello, world!"); - return () => { - term.dispose(); - }; - }, [connectElemRef.current]); + let blockElem: JSX.Element = null; + const blockAtom = atoms.blockAtomFamily(blockId); + const blockData = jotai.useAtomValue(blockAtom); + if (blockData.view === "term") { + blockElem = ; + } else if (blockData.view === "preview") { + blockElem = ; + } return (
@@ -78,7 +39,9 @@ const Block = ({ blockId }: { blockId: string }) => { Block [{blockId.substring(0, 8)}] {dims.width}x{dims.height}
-
+
+ {blockElem} +
); }; diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts new file mode 100644 index 00000000..94eb6f44 --- /dev/null +++ b/frontend/app/store/global.ts @@ -0,0 +1,41 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as jotai from "jotai"; +import { atomFamily } from "jotai/utils"; +import { v4 as uuidv4 } from "uuid"; + +const globalStore = jotai.createStore(); + +const tabId1 = uuidv4(); +const tabId2 = uuidv4(); + +const blockId1 = uuidv4(); +const blockId2 = uuidv4(); +const blockId3 = uuidv4(); + +const tabArr: TabData[] = [ + { name: "Tab 1", tabid: tabId1, blockIds: [blockId1, blockId2] }, + { name: "Tab 2", tabid: tabId2, blockIds: [blockId3] }, +]; + +const blockAtomFamily = atomFamily>((blockId: string) => { + if (blockId === blockId1) { + return jotai.atom({ blockid: blockId1, view: "term" }); + } + if (blockId === blockId2) { + return jotai.atom({ blockid: blockId2, view: "preview" }); + } + if (blockId === blockId3) { + return jotai.atom({ blockid: blockId3, view: "term" }); + } + return jotai.atom(null); +}); + +const atoms = { + activeTabId: jotai.atom(tabId1), + tabsAtom: jotai.atom(tabArr), + blockAtomFamily, +}; + +export { globalStore, atoms }; diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx index a24414d1..dc9c2fbe 100644 --- a/frontend/app/tab/tab.tsx +++ b/frontend/app/tab/tab.tsx @@ -3,23 +3,29 @@ import * as React from "react"; import * as jotai from "jotai"; -import { Block } from "../block/block.tsx"; -import { v4 as uuidv4 } from "uuid"; +import { Block } from "@/app/block/block"; +import { atoms } from "@/store/global"; import "./tab.less"; -const TabContent = () => { - const blockId1 = React.useMemo(() => uuidv4(), []); - const blockId2 = React.useMemo(() => uuidv4(), []); +const blockId1 = "44113b0c-1528-4db1-94f0-2cafa1542941"; +const blockId2 = "6bd76ccb-76ae-4f29-aa64-35206767e1ac"; +const TabContent = ({ tabId }: { tabId: string }) => { + const tabs = jotai.useAtomValue(atoms.tabsAtom); + const tabData = tabs.find((tab) => tab.tabid === tabId); + if (!tabData) { + return
Tab not found
; + } return (
-
- -
-
- -
+ {tabData.blockIds.map((blockId: string) => { + return ( +
+ +
+ ); + })}
); }; diff --git a/frontend/app/view/preview.tsx b/frontend/app/view/preview.tsx new file mode 100644 index 00000000..c733c62c --- /dev/null +++ b/frontend/app/view/preview.tsx @@ -0,0 +1,17 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import * as jotai from "jotai"; + +import "./view.less"; + +const PreviewView = ({ blockId }: { blockId: string }) => { + return ( +
+
Preview
+
+ ); +}; + +export { PreviewView }; diff --git a/frontend/app/view/term.tsx b/frontend/app/view/term.tsx new file mode 100644 index 00000000..a8387f63 --- /dev/null +++ b/frontend/app/view/term.tsx @@ -0,0 +1,69 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import * as jotai from "jotai"; +import { Terminal } from "@xterm/xterm"; +import type { ITheme } from "@xterm/xterm"; +import { FitAddon } from "@xterm/addon-fit"; + +import "./view.less"; +import "/public/xterm.css"; + +function getThemeFromCSSVars(el: Element): ITheme { + const theme: ITheme = {}; + const elemStyle = getComputedStyle(el); + theme.foreground = elemStyle.getPropertyValue("--term-foreground"); + theme.background = elemStyle.getPropertyValue("--term-background"); + theme.black = elemStyle.getPropertyValue("--term-black"); + theme.red = elemStyle.getPropertyValue("--term-red"); + theme.green = elemStyle.getPropertyValue("--term-green"); + theme.yellow = elemStyle.getPropertyValue("--term-yellow"); + theme.blue = elemStyle.getPropertyValue("--term-blue"); + theme.magenta = elemStyle.getPropertyValue("--term-magenta"); + theme.cyan = elemStyle.getPropertyValue("--term-cyan"); + theme.white = elemStyle.getPropertyValue("--term-white"); + theme.brightBlack = elemStyle.getPropertyValue("--term-bright-black"); + theme.brightRed = elemStyle.getPropertyValue("--term-bright-red"); + theme.brightGreen = elemStyle.getPropertyValue("--term-bright-green"); + theme.brightYellow = elemStyle.getPropertyValue("--term-bright-yellow"); + theme.brightBlue = elemStyle.getPropertyValue("--term-bright-blue"); + theme.brightMagenta = elemStyle.getPropertyValue("--term-bright-magenta"); + theme.brightCyan = elemStyle.getPropertyValue("--term-bright-cyan"); + theme.brightWhite = elemStyle.getPropertyValue("--term-bright-white"); + theme.selectionBackground = elemStyle.getPropertyValue("--term-selection-background"); + theme.selectionInactiveBackground = elemStyle.getPropertyValue("--term-selection-background"); + theme.cursor = elemStyle.getPropertyValue("--term-selection-background"); + theme.cursorAccent = elemStyle.getPropertyValue("--term-cursor-accent"); + return theme; +} + +const TerminalView = ({ blockId }: { blockId: string }) => { + const connectElemRef = React.useRef(null); + + React.useEffect(() => { + if (!connectElemRef.current) { + return; + } + const term = new Terminal({ + theme: getThemeFromCSSVars(connectElemRef.current), + fontSize: 12, + fontFamily: "Hack", + drawBoldTextInBrightColors: false, + fontWeight: "normal", + fontWeightBold: "bold", + }); + const fitAddon = new FitAddon(); + term.loadAddon(fitAddon); + term.open(connectElemRef.current); + fitAddon.fit(); + term.write("Hello, world!"); + return () => { + term.dispose(); + }; + }, [connectElemRef.current]); + + return
; +}; + +export { TerminalView }; diff --git a/frontend/app/view/view.less b/frontend/app/view/view.less new file mode 100644 index 00000000..91aa4e32 --- /dev/null +++ b/frontend/app/view/view.less @@ -0,0 +1,21 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +.view-term { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + overflow: hidden; +} + +.view-preview { + display: flex; + flex-direction: row; + width: 100%; + height: 100%; + flex-grow: 1; + overflow: hidden; + align-items: center; + justify-content: center; +} diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts new file mode 100644 index 00000000..653c0278 --- /dev/null +++ b/frontend/types/custom.d.ts @@ -0,0 +1,17 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +declare global { + type TabData = { + name: string; + tabid: string; + blockIds: string[]; + }; + + type BlockData = { + blockid: string; + view: string; + }; +} + +export {}; diff --git a/package.json b/package.json index 4b6402f7..7118a4e3 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "preview": "vite preview" }, "devDependencies": { + "@types/react": "^18.3.2", + "@types/uuid": "^9.0.8", "@vitejs/plugin-react": "^4.2.1", "@wailsio/runtime": "latest", "less": "^4.2.0", diff --git a/tsconfig.json b/tsconfig.json index 126dad7e..e436aa65 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "include": ["frontend/**/*"], "compilerOptions": { - "target": "es5", + "target": "es6", "module": "commonjs", "jsx": "preserve", "esModuleInterop": true, @@ -15,8 +15,9 @@ "downlevelIteration": true, "baseUrl": "./", "paths": { - "@/app/*": ["frontend/app/*"], // Points to the src folder - "@/util/*": ["frontend/util/*"], // Points to the src folder + "@/app/*": ["frontend/app/*"], + "@/util/*": ["frontend/util/*"], + "@/store/*": ["frontend/app/store/*"], } } } diff --git a/vite.config.ts b/vite.config.ts index 3aca6227..68e94143 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,6 +6,7 @@ export default defineConfig({ plugins: [react({}), tsconfigPaths()], publicDir: "public", build: { + target: "es6", rollupOptions: { input: { app: "public/index.html", diff --git a/yarn.lock b/yarn.lock index 3b226bbe..5ccef854 100644 --- a/yarn.lock +++ b/yarn.lock @@ -474,6 +474,24 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/prop-types@*": + version "15.7.12" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" + integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== + +"@types/react@^18.3.2": + version "18.3.2" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.2.tgz#462ae4904973bc212fa910424d901e3d137dbfcd" + integrity sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + +"@types/uuid@^9.0.8": + version "9.0.8" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" + integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== + "@vitejs/plugin-react@^4.2.1": version "4.2.1" resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz#744d8e4fcb120fc3dbaa471dadd3483f5a304bb9" @@ -562,6 +580,11 @@ copy-anything@^2.0.1: dependencies: is-what "^3.14.1" +csstype@^3.0.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"