commit a72653fcced8c91865118008ba036687575cd5ca Author: Joeri Exelmans Date: Sat Oct 4 23:13:55 2025 +0200 dirty but the initial concept is working diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d79ee7 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# bun-react-template + +To install dependencies: + +```bash +bun install +``` + +To start a development server: + +```bash +bun dev +``` + +To run for production: + +```bash +bun start +``` + +This project was created using `bun init` in bun v1.2.14. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/bun-env.d.ts b/bun-env.d.ts new file mode 100644 index 0000000..72f1c26 --- /dev/null +++ b/bun-env.d.ts @@ -0,0 +1,17 @@ +// Generated by `bun init` + +declare module "*.svg" { + /** + * A path to the SVG file + */ + const path: `${string}.svg`; + export = path; +} + +declare module "*.module.css" { + /** + * A record of class names to their corresponding CSS module classes + */ + const classes: { readonly [key: string]: string }; + export = classes; +} diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..1858091 --- /dev/null +++ b/bun.lock @@ -0,0 +1,38 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "bun-react-template", + "dependencies": { + "react": "^19", + "react-dom": "^19", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/react": "^19", + "@types/react-dom": "^19", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="], + + "@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="], + + "@types/react": ["@types/react@19.2.0", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA=="], + + "@types/react-dom": ["@types/react-dom@19.2.0", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg=="], + + "bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], + + "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="], + } +} diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..9819bf6 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[serve.static] +env = "BUN_PUBLIC_*" \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..e5c5353 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "bun-react-template", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "src/index.tsx", + "module": "src/index.tsx", + "scripts": { + "dev": "bun --hot src/index.tsx", + "build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'", + "start": "NODE_ENV=production bun src/index.tsx" + }, + "dependencies": { + "react": "^19", + "react-dom": "^19" + }, + "devDependencies": { + "@types/react": "^19", + "@types/react-dom": "^19", + "@types/bun": "latest" + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..8ea95b0 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,11 @@ +import "./index.css"; + +import { VisualEditor } from "./VisualEditor/VisualEditor"; + +export function App() { + return ( + + ); +} + +export default App; diff --git a/src/VisualEditor/VisualEditor.css b/src/VisualEditor/VisualEditor.css new file mode 100644 index 0000000..f628afa --- /dev/null +++ b/src/VisualEditor/VisualEditor.css @@ -0,0 +1,89 @@ +.svgCanvas { + cursor: crosshair; +} + +svg > text { + user-select: none; +} + +.selecting { + fill: rgba(0, 0, 255, 0.2); + stroke-width: 1px; + stroke:black; + stroke-dasharray: 7 6; +} + +.rountangle { + fill: rgba(255, 255, 255, 255); + /* fill: none; */ + stroke: black; + stroke-width: 2px; +} + +.rountangle:hover { + /* fill: lightgrey; */ + /* stroke-width: 4px; */ + cursor: grab; +} + +.rountangle.dragging { + /* fill: lightgrey; */ + /* stroke-width: 4px; */ + cursor: grabbing; +} + +/* .rountangle.selected { + fill: rgba(0, 0, 255, 0.2); + stroke: blue; + stroke-width: 4px; +} */ + +.lineHelper { + stroke: rgba(0, 0, 0, 0); + stroke-width: 16px; +} +.lineHelper:hover { + stroke: rgba(0, 255, 0, 0.2); + cursor: grab; +} + +.circleHelper { + fill: rgba(0, 0, 0, 0); +} +.circleHelper:hover { + fill: rgba(0, 255, 0, 0.2); + cursor: grab; +} + +.rountangle.or { + stroke-dasharray: 7 6; +} + +.arrow { + stroke: black; + stroke-width: 2px; +} + +/* .arrow.selected { + stroke: blue; + stroke-width: 4px; +} */ + + +#arrowEnd { + fill: context-stroke; +} + +.arrow:hover { + cursor: grab; +} + + + + +.selected { + fill: rgba(0, 0, 255, 0.2); + /* stroke-dasharray: 7 6; */ + stroke: blue; + stroke-width: 4px; +} \ No newline at end of file diff --git a/src/VisualEditor/VisualEditor.tsx b/src/VisualEditor/VisualEditor.tsx new file mode 100644 index 0000000..102ae31 --- /dev/null +++ b/src/VisualEditor/VisualEditor.tsx @@ -0,0 +1,565 @@ +import { MouseEventHandler, useEffect, useRef, useState } from "react"; +import { Line2D, Rect2D, Vec2D, addV2D, area, isEntirelyWithin, normalizeRect, scaleV2D, subtractV2D, transformLine, transformRect } from "./geometry"; + +import "./VisualEditor.css"; +import { getBBoxInSvgCoords } from "./svg_helper"; + + +type Rountangle = { + uid: string; + kind: "and" | "or"; +} & Rect2D; + +type Text = { + uid: string; + topLeft: Vec2D; + text: string; +}; + +type Arrow = { + uid: string; +} & Line2D; + +type VisualEditorState = { + rountangles: Rountangle[]; + texts: Text[]; + arrows: Arrow[]; + nextID: number; +}; + +const emptyState = { + rountangles: [], texts: [], arrows: [], nextID: 0, +}; + +const onOffStateMachine = { + rountangles: [ + { uid: "0", topLeft: {x: 100, y: 100}, size: {x: 100, y: 100}, kind: "and" }, + { uid: "1", topLeft: {x: 100, y: 300}, size: {x: 100, y: 100}, kind: "and" }, + ], + texts: [], + arrows: [ + { uid: "2", start: {x: 150, y: 200}, end: {x: 160, y: 300} }, + ], + nextID: 3, +}; + +type DraggingState = { + lastMousePos: Vec2D; +} | null; // null means: not dragging + +type ResizingState = { + lastMousePos: Vec2D; +} | null; // null means: not resizing + +type SelectingState = Rect2D | null; + + +// independently moveable parts of our shapes: +type RountanglePart = "left" | "top" | "right" | "bottom"; +type ArrowPart = "start" | "end"; + +type RountangleSelectable = { + kind: "rountangle"; + parts: RountanglePart[]; + uid: string; +} +type ArrowSelectable = { + kind: "arrow"; + parts: ArrowPart[]; + uid: string; +} +type Selectable = RountangleSelectable | ArrowSelectable; +type Selection = Selectable[]; + +const minStateSize = {x: 40, y: 40}; + +export function VisualEditor() { + const [state, setState] = useState(onOffStateMachine); + const [dragging, setDragging] = useState(null); + const [resizing, setResizing] = useState(null); + + const [mode, setMode] = useState<"state"|"transition"|"text">("state"); + + const [showHelp, setShowHelp] = useState(true); + + // uid's of selected rountangles + const [selection, setSelection] = useState([]); + + // not null while the user is making a selection + const [selectingState, setSelectingState] = useState(null); + + const refSVG = useRef(null); + + // useEffect(() => { + // console.log('selection:', selection); + // }, [selection]); + // useEffect(() => { + // console.log('state:', state); + // }, [state]); + // useEffect(() => { + // console.log('selectingState:', selectingState); + // }, [selectingState]); + + + + const onMouseDown: MouseEventHandler = (e) => { + console.log(e); + const currentPointer = {x: e.clientX, y: e.clientY}; + + if (e.button === 1) { + // ignore selection, always insert rountangle + setState(state => { + const newID = state.nextID.toString(); + setSelection([newID]); + return { + ...state, + rountangles: [...state.rountangles, { + uid: newID, + topLeft: currentPointer, + size: minStateSize, + kind: "and", + }], + nextID: state.nextID+1, + }; + }); + setResizing({lastMousePos: currentPointer}); + } + else if (e.button === 0) { + const uid = e.target?.dataset.uid; + const parts: string[] = e.target?.dataset.parts?.split(' ') || []; + if (uid) { + let allPartsInSelection = true; + for (const part of parts) { + if (!(selection.find(s => s.uid === uid)?.parts || []).includes(part)) { + allPartsInSelection = false; + break; + } + } + if (!allPartsInSelection) { + setSelection([{uid, parts, kind: "dontcare"}]); + } + + // left mouse button: select and drag + setDragging({ + lastMousePos: currentPointer, + }); + // // right mouse button: select and resize + // else if (e.button === 2) { + // setResizing({ + // lastMousePos: currentPointer, + // }); + // } + } + else { + setDragging(null); + setResizing(null); + setSelectingState({ + topLeft: currentPointer, + size: {x: 0, y: 0}, + }); + setSelection([]); + } + } + }; + + const onMouseMove = (e: MouseEvent) => { + const currentPointer = {x: e.clientX, y: e.clientY}; + if (dragging) { + setDragging(prevDragState => { + const pointerDelta = subtractV2D(currentPointer, dragging.lastMousePos); + const halfPointerDelta = scaleV2D(pointerDelta, 0.5); + setState(state => ({ + ...state, + rountangles: state.rountangles.map(r => { + const parts = selection.find(selected => selected.uid === r.uid)?.parts || []; + if (parts.length === 0) { + return r; + } + return { + uid: r.uid, + kind: r.kind, + ...transformRect(r, parts, halfPointerDelta), + }; + }), + arrows: state.arrows.map(a => { + const parts = selection.find(selected => selected.uid === a.uid)?.parts || []; + if (parts.length === 0) { + return a; + } + return { + uid: a.uid, + ...transformLine(a, parts, halfPointerDelta), + } + }) + })); + return {lastMousePos: currentPointer}; + }); + } + else if (resizing) { + setResizing(prevResizeState => { + const pointerDelta = subtractV2D(currentPointer, prevResizeState!.lastMousePos); + const halfPointerDelta = scaleV2D(pointerDelta, 0.5); + setState(state => ({ + ...state, + rountangles: state.rountangles.map(r => { + if (selection.includes(r.uid)) { + const newSize = addV2D(r.size, halfPointerDelta); + return { + ...r, + size: { + x: Math.max(newSize.x, minStateSize.x), + y: Math.max(newSize.y, minStateSize.y), + }, + }; + } + else { + return r; // no change + } + }), + })); + return {lastMousePos: currentPointer}; + }); + } + else if (selectingState) { + setSelectingState(ss => { + const selectionSize = subtractV2D(currentPointer, ss!.topLeft); + return { + ...ss!, + size: selectionSize, + }; + }); + } + }; + + const onMouseUp = (e: MouseEvent) => { + setDragging(null); + setResizing(null); + setSelectingState(ss => { + if (ss) { + // we were making a selection + const normalizedSS = normalizeRect(ss); + + const shapes = Array.from(refSVG.current?.querySelectorAll("rect, line, circle") || []) as SVGGraphicsElement[]; + + const shapesInSelection = shapes.filter(el => { + const bbox = getBBoxInSvgCoords(el, refSVG.current!); + return isEntirelyWithin(bbox, normalizedSS); + }).filter(el => !el.classList.contains("corner")); + + const uidToParts = new Map(); + for (const shape of shapesInSelection) { + const uid = shape.dataset.uid; + if (uid) { + const parts: Set = uidToParts.get(uid) || new Set(); + for (const part of shape.dataset.parts?.split(' ') || []) { + parts.add(part); + } + uidToParts.set(uid, parts); + } + } + + setSelection(() => [...uidToParts.entries()].map(([uid,parts]) => ({ + kind: "rountangle", + uid, + parts: [...parts], + }))); + + // const selected = [ + // ...state.rountangles.filter(rountangle => + // isEntirelyWithin(rountangle, normalizedSS)), + + // ...state.arrows.filter(arrow => isEntirelyWithin({ + // topLeft: arrow.start, + // size: subtractV2D(arrow.end, arrow.start), + // }, normalizedSS)), + // ]; + // setSelection(selected.map(r => r.uid)); + } + return null; // no longer selecting + }); + // sort: smaller rountangles are drawn on top + setState(state => ({ + ...state, + rountangles: state.rountangles.toSorted((a,b) => area(b) - area(a)), + })); + }; + + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Delete") { + // delete selection + setSelection(selection => { + setState(state => ({ + ...state, + rountangles: state.rountangles.filter(r => !selection.includes(r.uid)), + })); + return []; + }); + } + if (e.key === "o") { + console.log('turn selected states into OR-states...'); + // selected states become OR-states + setSelection(selection => { + setState(state => ({ + ...state, + rountangles: state.rountangles.map(r => selection.includes(r.uid) ? ({...r, kind: "or"}) : r), + })); + return selection; + }) + } + if (e.key === "a") { + // selected states become AND-states + setSelection(selection => { + setState(state => ({ + ...state, + rountangles: state.rountangles.map(r => selection.includes(r.uid) ? ({...r, kind: "and"}) : r), + })); + return selection; + }); + } + if (e.key === "h") { + setShowHelp(showHelp => !showHelp); + } + if (e.key === "s") { + setMode("state"); + } + if (e.key === "t") { + setMode("transition"); + } + if (e.key === "x") { + setMode("text"); + } + }; + + useEffect(() => { + // mousemove and mouseup are global event handlers so they keep working when pointer is outside of browser window + window.addEventListener("mouseup", onMouseUp); + window.addEventListener("mousemove", onMouseMove); + window.addEventListener("keydown", onKeyDown); + return () => { + window.removeEventListener("keydown", onKeyDown); + window.removeEventListener("mousemove", onMouseMove); + window.removeEventListener("mouseup", onMouseUp); + }; + }, [selectingState, dragging, resizing]); + + return e.preventDefault()} + ref={refSVG} + > + + + + + + + {state.rountangles.map(rountangle => r.uid === rountangle.uid)?.parts || []} + />)} + + {state.arrows.map(arrow => a.uid === arrow.uid)?.parts || []} + /> + )} + + {selectingState && } + + {showHelp && <> + + Left mouse button: Select/Drag. + + + Right mouse button: Resize. + + + Middle mouse button: Insert [S]tates / [T]ransitions / Te[X]t (current mode: {mode}) + + [Del] Delete selection. + + + [O] Turn selected states into OR-states. + + + [A] Turn selected states into AND-states. + + + [H] Show/hide this help. + + } + + ; +} + +const cornerOffset = 4; +const cornerRadius = 16; + +export function RountangleSVG(props: {rountangle: Rountangle, dragging: boolean, selected: string[]}) { + const {topLeft, size, uid} = props.rountangle; + return + + + + + + + + + + + + + + + + + {uid} + ; +} + +export function ArrowSVG(props: {arrow: Arrow, dragging: boolean, selected: string[]}) { + const {start, end, uid} = props.arrow; + return + + + + + + ; +} + +export function Selecting(props: SelectingState) { + const normalizedRect = normalizeRect(props!); + return ; +} \ No newline at end of file diff --git a/src/VisualEditor/geometry.ts b/src/VisualEditor/geometry.ts new file mode 100644 index 0000000..6d0319b --- /dev/null +++ b/src/VisualEditor/geometry.ts @@ -0,0 +1,93 @@ +export type Vec2D = { + x: number; + y: number; +}; + +export type Rect2D = { + topLeft: Vec2D; + size: Vec2D; +}; + +export type Line2D = { + start: Vec2D; + end: Vec2D; +}; + +// make sure size is always positive +export function normalizeRect(rect: Rect2D) { + return { + topLeft: { + x: rect.size.x < 0 ? (rect.topLeft.x + rect.size.x) : rect.topLeft.x, + y: rect.size.y < 0 ? (rect.topLeft.y + rect.size.y) : rect.topLeft.y, + }, + size: { + x: rect.size.x < 0 ? -rect.size.x : rect.size.x, + y: rect.size.y < 0 ? -rect.size.y : rect.size.y, + } + }; +} + +export function isEntirelyWithin(child: Rect2D, parent: Rect2D) { + return ( + child.topLeft.x >= parent.topLeft.x + && child.topLeft.y >= parent.topLeft.y + && child.topLeft.x + child.size.x <= parent.topLeft.x + parent.size.x + && child.topLeft.y + child.size.y <= parent.topLeft.y + parent.size.y + ); +} + +export function isWithin(p: Vec2D, r: Rect2D) { + return ( + p.x >= r.topLeft.x && p.x <= r.topLeft.x + r.size.x + && p.y >= r.topLeft.y && p.y <= r.topLeft.y + r.size.y + ); +} + +export function addV2D(a: Vec2D, b: Vec2D) { + return { + x: a.x + b.x, + y: a.y + b.y, + }; +} + +export function subtractV2D(a: Vec2D, b: Vec2D) { + return { + x: a.x - b.x, + y: a.y - b.y, + }; +} + +export function scaleV2D(p: Vec2D, scale: number) { + return { + x: p.x * scale, + y: p.y * scale, + }; +} + +export function area(rect: Rect2D) { + return rect.size.x * rect.size.y; +} + +export function transformRect(rect: Rect2D, parts: string[], delta: Vec2D): Rect2D { + return { + topLeft: { + x: parts.includes("left") ? rect.topLeft.x + delta.x : rect.topLeft.x, + y: parts.includes("top") ? rect.topLeft.y + delta.y : rect.topLeft.y, + }, + size: { + x: rect.size.x + + (parts.includes("right") ? delta.x : 0) + - (parts.includes("left") ? delta.x : 0), + y: rect.size.y + + (parts.includes("bottom") ? delta.y : 0) + - (parts.includes("top") ? delta.y : 0), + }, + }; +} + +export function transformLine(line: Line2D, parts: string[], delta: Vec2D): Line2D { + return { + start: parts.includes("start") ? addV2D(line.start, {x: delta.x, y: delta.y}) : line.start, + end: parts.includes("end") ? addV2D(line.end, {x: delta.x, y: delta.y}) : line.end, + }; +} \ No newline at end of file diff --git a/src/VisualEditor/svg_helper.ts b/src/VisualEditor/svg_helper.ts new file mode 100644 index 0000000..4a863ba --- /dev/null +++ b/src/VisualEditor/svg_helper.ts @@ -0,0 +1,30 @@ +import { Rect2D } from "./geometry"; + +// author: ChatGPT +export function getBBoxInSvgCoords(el: SVGGraphicsElement, svg: SVGSVGElement): Rect2D { + const b = el.getBBox(); + const m = el.getCTM()!; + const toSvg = (x: number, y: number) => { + const p = svg.createSVGPoint(); + p.x = x; p.y = y; + return p.matrixTransform(m); + }; + const pts = [ + toSvg(b.x, b.y), + toSvg(b.x + b.width, b.y), + toSvg(b.x, b.y + b.height), + toSvg(b.x + b.width, b.y + b.height) + ]; + const xs = pts.map(p => p.x); + const ys = pts.map(p => p.y); + return { + topLeft: { + x: Math.min(...xs), + y: Math.min(...ys), + }, + size: { + x: Math.max(...xs) - Math.min(...xs), + y: Math.max(...ys) - Math.min(...ys), + }, + }; +} diff --git a/src/frontend.tsx b/src/frontend.tsx new file mode 100644 index 0000000..446e60e --- /dev/null +++ b/src/frontend.tsx @@ -0,0 +1,26 @@ +/** + * This file is the entry point for the React app, it sets up the root + * element and renders the App component to the DOM. + * + * It is included in `src/index.html`. + */ + +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App"; + +const elem = document.getElementById("root")!; +const app = ( + + + +); + +if (import.meta.hot) { + // With hot module reloading, `import.meta.hot.data` is persisted. + const root = (import.meta.hot.data.root ??= createRoot(elem)); + root.render(app); +} else { + // The hot module reloading API is not available in production. + createRoot(elem).render(app); +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..b67b04d --- /dev/null +++ b/src/index.css @@ -0,0 +1,8 @@ +html, body { + margin: 0; + height: 100%; +} + +div#root { + height: 100%; +} \ No newline at end of file diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..6af91be --- /dev/null +++ b/src/index.html @@ -0,0 +1,13 @@ + + + + + + + StateBuddy + + +
+ + + diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..1064998 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,19 @@ +import { serve } from "bun"; +import index from "./index.html"; + +const server = serve({ + routes: { + // Serve index.html for all unmatched routes. + "/*": index, + }, + + development: process.env.NODE_ENV !== "production" && { + // Enable browser hot reloading in development + hmr: true, + + // Echo console logs from the browser to the server + console: true, + }, +}); + +console.log(`🚀 Server running at ${server.url}`); diff --git a/src/logo.svg b/src/logo.svg new file mode 100644 index 0000000..7ef1500 --- /dev/null +++ b/src/logo.svg @@ -0,0 +1 @@ +Bun Logo \ No newline at end of file diff --git a/src/react.svg b/src/react.svg new file mode 100644 index 0000000..1ab815a --- /dev/null +++ b/src/react.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5aafbe9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "Preserve", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "allowJs": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "exclude": ["dist", "node_modules"] +}