Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
<title>LeetCanvas</title>
<style>
/* Win2000 system font stack applied globally */
* { font-family: "MS Sans Serif", "Microsoft Sans Serif", Tahoma, Geneva, Arial, sans-serif; }
</style>
<script>
(() => {
try {
Expand Down
10 changes: 7 additions & 3 deletions frontend/src/canvas/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -353,15 +353,19 @@ export function Canvas({ canvasId, userId }: CanvasProps) {
ref={viewportRef}
className="fixed inset-0 overflow-hidden bg-(--lc-canvas-bg)"
style={{
backgroundImage: `radial-gradient(circle, var(--lc-canvas-dot) ${gridDotRadiusPx}px, transparent ${gridDotRadiusPx}px)`,
backgroundSize: `${gridSpacingPx}px ${gridSpacingPx}px`,
backgroundImage: `
linear-gradient(rgba(0,102,102,0.4) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,102,102,0.4) 1px, transparent 1px),
radial-gradient(circle, var(--lc-canvas-dot) ${gridDotRadiusPx}px, transparent ${gridDotRadiusPx}px)
`,
backgroundSize: `${gridSpacingPx * 4}px ${gridSpacingPx * 4}px, ${gridSpacingPx * 4}px ${gridSpacingPx * 4}px, ${gridSpacingPx}px ${gridSpacingPx}px`,
backgroundPosition: `${transform.x}px ${transform.y}px`,
}}
onPointerDown={activeToolController.onCanvasPointerDown}
onPointerMove={activeToolController.onCanvasPointerMove}
onPointerUp={activeToolController.onCanvasPointerUp}
>
<div className="absolute right-4 top-4 z-60 flex items-start gap-3">
<div className="absolute right-3 top-3 z-60 flex items-start gap-2">
<CanvasPresenceBar users={users} localUserId={userId} />
<ThemePanel />
<ToolPanel
Expand Down
55 changes: 45 additions & 10 deletions frontend/src/canvas/CanvasPresenceBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,52 @@ interface CanvasPresenceBarProps {

export function CanvasPresenceBar({ users, localUserId }: CanvasPresenceBarProps) {
return (
<div className="flex items-center gap-5 border border-(--lc-border-default) bg-(--lc-surface-1) p-2">
{users.map((user) => (
<div key={user.id} className="flex items-center gap-2">
<div
className="w2k-window"
style={{
minWidth: 110,
background: "var(--w2k-btn-face)",
}}
>
<div className="w2k-titlebar">
<span style={{ fontSize: 10 }}>👥</span>
<span>Users</span>
</div>
<div style={{ padding: "4px 6px", display: "flex", flexDirection: "column", gap: 2 }}>
{users.length === 0 && (
<span style={{ fontSize: 10, color: "var(--w2k-gray-text)", fontFamily: '"MS Sans Serif", Tahoma, Arial, sans-serif' }}>
(no users)
</span>
)}
{users.map((user) => (
<div
title={user.id === localUserId ? "You" : user.id}
className={`h-3 w-3 rounded-full ${user.id === localUserId ? "ring-1 ring-(--lc-border-strong)" : ""}`}
style={{ backgroundColor: user.color }}
/>
<p className="text-(--lc-text-primary)">{user.id.slice(0, 5)}</p>
</div>
))}
key={user.id}
style={{
display: "flex",
alignItems: "center",
gap: 5,
fontFamily: '"MS Sans Serif", Tahoma, Arial, sans-serif',
fontSize: 11,
background: user.id === localUserId ? "var(--w2k-selected)" : "transparent",
color: user.id === localUserId ? "var(--w2k-white)" : "var(--w2k-black)",
padding: "1px 3px",
}}
>
<div
title={user.id === localUserId ? "You" : user.id}
style={{
width: 10,
height: 10,
borderRadius: "50%",
backgroundColor: user.color,
border: "1px solid var(--w2k-btn-dkshadow)",
flexShrink: 0,
}}
/>
<span>{user.id === localUserId ? "You" : user.id.slice(0, 5)}</span>
</div>
))}
</div>
</div>
);
}
20 changes: 15 additions & 5 deletions frontend/src/canvas/SelectionOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ function CornerHandle({
top: y - outerSize / 2,
width: outerSize,
height: outerSize,
backgroundColor: LOCAL_SELECTION_COLOR,
backgroundColor: "#d4d0c8",
borderTop: "2px solid #ffffff",
borderLeft: "2px solid #ffffff",
borderBottom: "2px solid #404040",
borderRight: "2px solid #404040",
display: "flex",
alignItems: "center",
justifyContent: "center",
Expand All @@ -60,7 +64,7 @@ function CornerHandle({
style={{
width: innerSize,
height: innerSize,
backgroundColor: "#ffffff",
backgroundColor: "#000080",
}}
/>
</div>
Expand All @@ -81,7 +85,8 @@ function ActionIconButton({
type="button"
title={title}
aria-label={title}
className="flex h-8 w-8 items-center justify-center border border-(--lc-border-strong) bg-(--lc-surface-1) text-(--lc-text-secondary) shadow-sm transition hover:border-(--lc-border-focus) hover:text-(--lc-accent)"
className="w2k-btn"
style={{ width: 22, height: 22, minHeight: 22, padding: 0, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 11 }}
onPointerDown={(e) => {
e.stopPropagation();
}}
Expand Down Expand Up @@ -241,7 +246,8 @@ export function SelectionOverlay({
top: selectedNode.y * transform.zoom + transform.y,
width: selectedNode.width * transform.zoom,
height: selectedNode.height * transform.zoom,
border: `2px solid ${localSelectionColor}`,
border: `2px solid #000080`,
outline: `1px dotted #ffffff`,
}}
/>

Expand Down Expand Up @@ -272,8 +278,12 @@ export function SelectionOverlay({
/>

<div
className="absolute flex flex-col gap-2"
className="w2k-window absolute"
style={{
display: "flex",
flexDirection: "column",
gap: 2,
padding: 3,
left:
(selectedNode.x + selectedNode.width) * transform.zoom +
transform.x +
Expand Down
61 changes: 32 additions & 29 deletions frontend/src/canvas/SpawnPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useState } from "react";
import { IconShare3 } from "@tabler/icons-react";
import type { NodeType } from "../shared/nodes";
import { NODE_CONTROL_OPTIONS } from "./ui/controlOptions";

Expand All @@ -8,52 +7,56 @@ interface SpawnPanelProps {
}

export function SpawnPanel({ onSpawn }: SpawnPanelProps) {
const [copyState, setCopyState] = useState<"idle" | "copied" | "failed">(
"idle",
);
const [copyState, setCopyState] = useState<"idle" | "copied" | "failed">("idle");

async function handleShareCanvas() {
const link = window.location.href;

try {
await navigator.clipboard.writeText(link);
setCopyState("copied");
} catch {
setCopyState("failed");
}

window.setTimeout(() => {
setCopyState("idle");
}, 1200);
window.setTimeout(() => setCopyState("idle"), 1200);
}

return (
<div className="flex flex-col gap-1">
<div className="flex flex-col gap-2 border border-(--lc-border-default) bg-(--lc-surface-1) p-3">
<div className="text-xs font-semibold text-(--lc-text-secondary)">Add node</div>
{NODE_CONTROL_OPTIONS.map(({ type, label, Icon }) => (
<div className="w2k-window" style={{ minWidth: 130 }}>
<div className="w2k-titlebar">
<span style={{ fontSize: 10 }}>📋</span>
<span>Add Node</span>
</div>
<div style={{ padding: "6px", display: "flex", flexDirection: "column", gap: 3 }}>
{NODE_CONTROL_OPTIONS.map(({ type, label }) => (
<button
key={type}
onClick={() => onSpawn(type)}
className="flex items-center gap-2 border border-(--lc-border-default) px-3 py-2 text-left text-sm text-(--lc-text-secondary) transition hover:border-(--lc-border-focus) hover:text-(--lc-accent)"
className="w2k-btn"
style={{ textAlign: "left", width: "100%" }}
>
<Icon size={16} stroke={1.8} />
<span>{label}</span>
{label}
</button>
))}
</div>

<button
onClick={handleShareCanvas}
className="mt-1 flex items-center gap-2 border border-(--lc-border-default) bg-(--lc-surface-1) px-3 py-2 text-left text-sm text-(--lc-text-secondary) transition hover:border-(--lc-border-focus) hover:text-(--lc-accent)"
>
<IconShare3 size={16} stroke={1.8} />
<span>Share Canvas</span>
</button>
<div className="text-[10px] text-(--lc-text-muted)">
{copyState === "copied" && "Link copied"}
{copyState === "failed" && "Clipboard blocked"}
{copyState === "idle" && "Copy join link"}
<hr className="w2k-separator" />
<button
onClick={handleShareCanvas}
className="w2k-btn"
style={{ textAlign: "left", width: "100%" }}
>
Share Canvas...
</button>
<div
style={{
fontSize: 10,
color: "var(--w2k-gray-text)",
fontFamily: '"MS Sans Serif", Tahoma, Arial, sans-serif',
minHeight: 14,
}}
>
{copyState === "copied" && "✓ Link copied to clipboard"}
{copyState === "failed" && "✗ Clipboard blocked"}
{copyState === "idle" && "\u00a0"}
</div>
</div>
</div>
);
Expand Down
30 changes: 17 additions & 13 deletions frontend/src/canvas/ThemePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import { IconMoon, IconSun } from "@tabler/icons-react";
import { useTheme } from "../theme/useTheme";

export function ThemePanel() {
const { theme, toggleTheme } = useTheme();
const isDark = theme === "dark";

return (
<div className="flex flex-col gap-2 border border-(--lc-border-default) bg-(--lc-surface-1) p-3">
<div className="text-xs font-semibold text-(--lc-text-secondary)">Theme</div>
<button
type="button"
aria-pressed={isDark}
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
onClick={toggleTheme}
className="flex items-center gap-2 border border-(--lc-border-default) bg-(--lc-surface-2) px-3 py-1.5 text-sm text-(--lc-text-secondary) transition hover:border-(--lc-border-focus) hover:text-(--lc-accent)"
>
{isDark ? <IconSun size={15} stroke={1.8} /> : <IconMoon size={15} stroke={1.8} />}
<span>{isDark ? "Dark" : "Light"}</span>
</button>
<div className="w2k-window" style={{ minWidth: 110 }}>
<div className="w2k-titlebar">
<span style={{ fontSize: 10 }}>🎨</span>
<span>Display</span>
</div>
<div style={{ padding: "6px" }}>
<button
type="button"
aria-pressed={isDark}
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
onClick={toggleTheme}
className="w2k-btn"
style={{ width: "100%" }}
>
{isDark ? "Light Mode" : "Dark Mode"}
</button>
</div>
</div>
);
}
63 changes: 33 additions & 30 deletions frontend/src/canvas/ToolPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,41 @@ interface Props {

export function ToolPanel({ tool, onToolChange, thickness, onThicknessChange }: Props) {
return (
<div className="flex flex-col gap-2 border border-(--lc-border-default) bg-(--lc-surface-1) p-3">
<div className="text-xs font-semibold text-(--lc-text-secondary)">Tool</div>
<div className="flex gap-1">
{TOOL_CONTROL_OPTIONS.map(({ tool: optionTool, label, Icon }) => (
<button
key={optionTool}
type="button"
onClick={() => onToolChange(optionTool)}
className={`flex items-center gap-2 border px-3 py-1.5 text-sm transition ${
tool === optionTool
? "border-(--lc-border-focus) bg-(--lc-surface-3) text-(--lc-accent)"
: "border-(--lc-border-default) text-(--lc-text-secondary) hover:border-(--lc-border-focus) hover:text-(--lc-accent)"
}`}
>
<Icon size={15} stroke={1.8} />
<span>{label}</span>
</button>
))}
<div className="w2k-window" style={{ minWidth: 140 }}>
<div className="w2k-titlebar">
<span style={{ fontSize: 10 }}>🖱</span>
<span>Tools</span>
</div>
{tool === "draw" && (
<div className="flex flex-col gap-1">
<div className="text-[10px] text-(--lc-text-muted)">Thickness: {thickness}</div>
<input
type="range"
min={1}
max={10}
value={thickness}
className="w-full"
onChange={(e) => onThicknessChange(Number(e.target.value))}
/>
<div style={{ padding: "6px 6px 6px 6px", display: "flex", flexDirection: "column", gap: 4 }}>
<div style={{ display: "flex", gap: 3 }}>
{TOOL_CONTROL_OPTIONS.map(({ tool: optionTool, label }) => (
<button
key={optionTool}
type="button"
onClick={() => onToolChange(optionTool)}
className={`w2k-btn${tool === optionTool ? " active" : ""}`}
style={{ flex: 1 }}
>
{label}
</button>
))}
</div>
)}
{tool === "draw" && (
<div style={{ marginTop: 4 }}>
<div className="w2k-label" style={{ fontSize: 10, marginBottom: 2 }}>
Thickness: {thickness}
</div>
<input
type="range"
min={1}
max={10}
value={thickness}
style={{ width: "100%" }}
onChange={(e) => onThicknessChange(Number(e.target.value))}
/>
</div>
)}
</div>
</div>
);
}
Loading