feat(ui): canvas-first redesign with a self-hosted draw.io (#957)

The canvas now fills the window and the chat floats on its right. draw.io is
served from our own origin (public/drawio, downloaded at build time and
checked against the release's SHA-256), so the app can drive the editor
directly:

- every AI change becomes a version card with a thumbnail; compare, restore
  or undo it, and Ctrl+Z on the canvas takes an AI change back in one step
- shapes the AI just changed are highlighted on the canvas
- selected shapes appear as a chip and are sent to the model
- draw.io's own simple UI: its toolbar, menus (auto layout, Mermaid, CSV
  import) and page tabs; the shape library starts closed
- new start screen, session rename and switcher, open .drawio files
- settings split into Models & keys, Appearance, Advanced and About
- Bedrock in the model settings also takes an API key instead of access keys
- phones switch between canvas and chat
- dark mode uses draw.io's own dark mode, with neutral greys around it

With an external draw.io (NEXT_PUBLIC_DRAWIO_BASE_URL) the features above
that need same origin are off.

The chat logic moved from the 1500-line chat panel into a chat engine plus
small UI components; zustand holds canvas, versions, settings and UI state.
This commit is contained in:
Dayuan Jiang
2026-10-08 16:20:15 +09:00
committed by GitHub
parent 18a20aa1ca
commit c513d1a0c2
110 changed files with 17891 additions and 6944 deletions
+200 -293
View File
@@ -8,21 +8,8 @@
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
--font-sans: var(--font-latin), var(--font-cjk), system-ui, sans-serif;
--font-mono: var(--font-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
@@ -43,161 +30,16 @@
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
:root {
--radius: 0.75rem;
/* Clean Light Modern Palette */
--background: oklch(0.985 0.002 240);
--foreground: oklch(0.23 0.02 260);
--card: oklch(1 0 0);
--card-foreground: oklch(0.23 0.02 260);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.23 0.02 260);
/* Dark primary - slightly lighter */
--primary: oklch(0.35 0.01 260);
--primary-foreground: oklch(0.99 0 0);
/* Warm gray secondary */
--secondary: oklch(0.96 0.005 260);
--secondary-foreground: oklch(0.35 0.02 260);
/* Light muted tones */
--muted: oklch(0.965 0.005 260);
--muted-foreground: oklch(0.5 0.02 260);
/* Soft lavender accent */
--accent: oklch(0.94 0.03 280);
--accent-foreground: oklch(0.35 0.08 270);
/* Muted rose destructive */
--destructive: oklch(0.45 0.12 10);
/* Subtle borders */
--border: oklch(0.92 0.01 260);
--input: oklch(0.94 0.01 260);
--ring: oklch(0.25 0.01 260);
/* Chart colors - harmonious palette */
--chart-1: oklch(0.55 0.18 265);
--chart-2: oklch(0.65 0.15 170);
--chart-3: oklch(0.7 0.18 45);
--chart-4: oklch(0.6 0.2 330);
--chart-5: oklch(0.5 0.15 200);
/* Sidebar */
--sidebar: oklch(0.99 0.002 260);
--sidebar-foreground: oklch(0.23 0.02 260);
--sidebar-primary: oklch(0.55 0.18 265);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.96 0.02 270);
--sidebar-accent-foreground: oklch(0.35 0.05 265);
--sidebar-border: oklch(0.93 0.01 260);
--sidebar-ring: oklch(0.55 0.18 265);
}
.dark {
--background: oklch(0.15 0.015 260);
--foreground: oklch(0.95 0.01 260);
--card: oklch(0.2 0.015 260);
--card-foreground: oklch(0.95 0.01 260);
--popover: oklch(0.2 0.015 260);
--popover-foreground: oklch(0.95 0.01 260);
--primary: oklch(0.7 0.16 265);
--primary-foreground: oklch(0.15 0.02 260);
--secondary: oklch(0.25 0.015 260);
--secondary-foreground: oklch(0.9 0.01 260);
--muted: oklch(0.25 0.015 260);
--muted-foreground: oklch(0.65 0.02 260);
--accent: oklch(0.3 0.04 280);
--accent-foreground: oklch(0.9 0.03 270);
--destructive: oklch(0.55 0.12 10);
--border: oklch(0.28 0.015 260);
--input: oklch(0.25 0.015 260);
--ring: oklch(0.7 0.16 265);
--chart-1: oklch(0.7 0.16 265);
--chart-2: oklch(0.7 0.13 170);
--chart-3: oklch(0.75 0.16 45);
--chart-4: oklch(0.7 0.18 330);
--chart-5: oklch(0.6 0.13 200);
--sidebar: oklch(0.18 0.015 260);
--sidebar-foreground: oklch(0.95 0.01 260);
--sidebar-primary: oklch(0.7 0.16 265);
--sidebar-primary-foreground: oklch(0.15 0.02 260);
--sidebar-accent: oklch(0.25 0.03 270);
--sidebar-accent-foreground: oklch(0.9 0.02 265);
--sidebar-border: oklch(0.28 0.015 260);
--sidebar-ring: oklch(0.7 0.16 265);
}
/* ============================================
REFINED MINIMAL DESIGN SYSTEM
============================================ */
:root {
/* Surface layers for depth */
--surface-0: oklch(1 0 0);
--surface-1: oklch(0.985 0.002 240);
--surface-2: oklch(0.97 0.004 240);
--surface-elevated: oklch(1 0 0);
/* Subtle borders */
--border-subtle: oklch(0.94 0.008 260);
--border-default: oklch(0.91 0.012 260);
/* Interactive states */
--interactive-hover: oklch(0.96 0.015 260);
--interactive-active: oklch(0.93 0.02 265);
/* Success state */
--success: oklch(0.65 0.18 145);
--success-muted: oklch(0.95 0.03 145);
/* Animation timing */
--duration-fast: 120ms;
--duration-normal: 200ms;
--duration-slow: 300ms;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
}
.dark {
--surface-0: oklch(0.15 0.015 260);
--surface-1: oklch(0.18 0.015 260);
--surface-2: oklch(0.22 0.015 260);
--surface-elevated: oklch(0.25 0.015 260);
--border-subtle: oklch(0.25 0.012 260);
--border-default: oklch(0.3 0.015 260);
--interactive-hover: oklch(0.25 0.02 265);
--interactive-active: oklch(0.3 0.025 270);
--success: oklch(0.7 0.16 145);
--success-muted: oklch(0.25 0.04 145);
}
/* Expose surface colors to Tailwind */
@theme inline {
/* App-specific tokens */
--color-canvas: var(--canvas);
--color-faint: var(--faint);
--color-marker: var(--marker);
--color-marker-soft: var(--marker-soft);
--color-marker-ink: var(--marker-ink);
--color-surface-0: var(--surface-0);
--color-surface-1: var(--surface-1);
--color-surface-2: var(--surface-2);
--color-surface-elevated: var(--surface-elevated);
--color-border-subtle: var(--border-subtle);
--color-border-default: var(--border-default);
--color-interactive-hover: var(--interactive-hover);
@@ -206,12 +48,151 @@
--color-success-muted: var(--success-muted);
}
/*
* Light: a quiet light table. Ink for primary actions, a highlighter
* yellow reserved for what the AI just changed.
*/
:root {
--radius: 0.75rem;
--canvas: #f4f5f7;
--background: #f4f5f7;
--foreground: #14181d;
--card: #ffffff;
--card-foreground: #14181d;
--popover: #ffffff;
--popover-foreground: #14181d;
--primary: #14181d;
--primary-foreground: #ffffff;
--secondary: #f1f3f5;
--secondary-foreground: #14181d;
--muted: #f1f3f5;
--muted-foreground: #5d6672;
--faint: #687180;
--accent: #eef0f3;
--accent-foreground: #14181d;
--destructive: #c9372c;
--border: #e4e7eb;
--input: #dfe3e8;
--ring: #14181d;
--marker: #ffd84d;
--marker-soft: #fff4bf;
--marker-ink: #6b5200;
--surface-0: #ffffff;
--surface-1: #f7f8fa;
--surface-2: #f1f3f5;
--border-subtle: #eceef1;
--border-default: #e4e7eb;
--interactive-hover: #f1f3f5;
--interactive-active: #e7eaee;
--success: #1f9e5a;
--success-muted: #e3f5ea;
--shadow-float:
0 1px 2px rgb(20 24 29 / 0.05), 0 10px 30px -8px rgb(20 24 29 / 0.14);
--shadow-pop:
0 1px 2px rgb(20 24 29 / 0.06), 0 18px 48px -12px rgb(20 24 29 / 0.22);
--duration-normal: 200ms;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
}
/*
* Han characters are drawn differently in Simplified Chinese, Traditional
* Chinese and Japanese: each language lists its own system fonts first.
*/
:root {
--font-cjk:
"PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC",
"Microsoft YaHei", "Hiragino Sans", "Meiryo";
}
:root:lang(ja) {
--font-cjk:
"Hiragino Sans", "Hiragino Kaku Gothic ProN", "Noto Sans CJK JP",
"Yu Gothic UI", "Meiryo", "PingFang SC";
}
:root:lang(zh-Hant) {
--font-cjk:
"PingFang TC", "Noto Sans CJK TC", "Microsoft JhengHei",
"Hiragino Sans", "PingFang SC";
}
/*
* Dark: a blueprint-blue table. Surfaces get lighter the higher they float.
* The diagram stays on a white sheet so it looks the way it will export;
* primary actions are paper-white, and amber only marks AI changes.
*/
.dark {
/* Neutral greys: the canvas matches draw.io's own dark workspace */
--canvas: #1b1d1e;
--background: #1b1d1e;
--foreground: #ececec;
--card: #242627;
--card-foreground: #ececec;
--popover: #2a2c2d;
--popover-foreground: #ececec;
--primary: #ececec;
--primary-foreground: #1b1d1e;
--secondary: #2d2f30;
--secondary-foreground: #ececec;
--muted: #2d2f30;
--muted-foreground: #a7a9aa;
--faint: #8a8c8d;
--accent: #353738;
--accent-foreground: #ececec;
--destructive: #f07167;
--border: rgb(255 255 255 / 0.1);
--input: rgb(255 255 255 / 0.16);
--ring: #ececec;
--marker: #f5b13d;
--marker-soft: rgb(245 177 61 / 0.16);
--marker-ink: #f7c46b;
--surface-0: #242627;
--surface-1: #202223;
--surface-2: #2d2f30;
--border-subtle: rgb(255 255 255 / 0.08);
--border-default: rgb(255 255 255 / 0.13);
--interactive-hover: #353738;
--interactive-active: #3c3e3f;
--success: #4fd18b;
--success-muted: rgb(79 209 139 / 0.14);
--shadow-float:
0 0 0 1px rgb(255 255 255 / 0.07), 0 14px 34px -12px rgb(0 0 0 / 0.6);
--shadow-pop:
0 0 0 1px rgb(255 255 255 / 0.1), 0 22px 56px -14px rgb(0 0 0 / 0.7);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
html {
color-scheme: light;
}
html.dark {
color-scheme: dark;
}
body {
@apply bg-background text-foreground font-sans;
font-size: 14px;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
::selection {
background: var(--marker-soft);
color: inherit;
}
}
@@ -221,46 +202,66 @@
width: 100% !important;
}
/* Custom scrollbar */
@layer utilities {
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: oklch(0.85 0.01 260) transparent;
scrollbar-color: color-mix(in oklab, var(--foreground) 18%, transparent)
transparent;
}
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background-color: oklch(0.85 0.01 260);
background-color: color-mix(
in oklab,
var(--foreground) 18%,
transparent
);
border-radius: 3px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background-color: oklch(0.75 0.01 260);
background-color: color-mix(
in oklab,
var(--foreground) 30%,
transparent
);
}
/* Dark mode scrollbar */
.dark .scrollbar-thin {
scrollbar-color: oklch(0.35 0.015 260) transparent;
/* Diagram pictures always render as on paper, also in dark mode
(draw.io SVGs use light-dark() colors) */
.sheet-light {
color-scheme: light;
}
.dark .scrollbar-thin::-webkit-scrollbar-thumb {
background-color: oklch(0.35 0.015 260);
.shadow-float {
box-shadow: var(--shadow-float);
}
.dark .scrollbar-thin::-webkit-scrollbar-thumb:hover {
background-color: oklch(0.45 0.015 260);
.shadow-pop {
box-shadow: var(--shadow-pop);
}
/* Older names kept for components that still use them */
.shadow-soft {
box-shadow: var(--shadow-float);
}
.shadow-dialog {
box-shadow: var(--shadow-pop);
}
}
/* Smooth page transitions */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes messageIn {
from {
opacity: 0;
transform: translateY(8px);
@@ -271,121 +272,18 @@
}
}
@keyframes slideInRight {
/* The first prompt moves from the middle of the canvas into the panel */
@keyframes panelIn {
from {
opacity: 0;
transform: translateX(16px);
transform: translateX(12px) scale(0.985);
}
to {
opacity: 1;
transform: translateX(0);
transform: translateX(0) scale(1);
}
}
.animate-fade-in {
animation: fadeIn 0.3s ease-out forwards;
}
.animate-slide-in-right {
animation: slideInRight 0.3s ease-out forwards;
}
/* Message bubble animations */
@keyframes messageIn {
from {
opacity: 0;
transform: translateY(12px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.animate-message-in {
animation: messageIn 0.25s ease-out forwards;
}
/* Subtle floating shadow for cards */
.shadow-soft {
box-shadow:
0 1px 2px oklch(0.23 0.02 260 / 0.04),
0 4px 12px oklch(0.23 0.02 260 / 0.06),
0 8px 24px oklch(0.23 0.02 260 / 0.04);
}
.shadow-soft-lg {
box-shadow:
0 2px 4px oklch(0.23 0.02 260 / 0.04),
0 8px 20px oklch(0.23 0.02 260 / 0.08),
0 16px 40px oklch(0.23 0.02 260 / 0.06);
}
/* Gradient text utility */
.text-gradient-primary {
background: linear-gradient(
135deg,
oklch(0.55 0.18 265),
oklch(0.6 0.2 290)
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* ============================================
REFINED DIALOG STYLES
============================================ */
/* Refined dialog shadow - multi-layer soft shadow */
.shadow-dialog {
box-shadow:
0 0 0 1px oklch(0 0 0 / 0.03),
0 2px 4px oklch(0 0 0 / 0.02),
0 12px 24px oklch(0 0 0 / 0.06),
0 24px 48px oklch(0 0 0 / 0.04);
}
.dark .shadow-dialog {
box-shadow:
0 0 0 1px oklch(1 0 0 / 0.05),
0 2px 4px oklch(0 0 0 / 0.2),
0 12px 24px oklch(0 0 0 / 0.3),
0 24px 48px oklch(0 0 0 / 0.2);
}
/* Dialog animations */
@keyframes dialog-in {
from {
opacity: 0;
transform: translate(-50%, -48%) scale(0.96);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes dialog-out {
from {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
to {
opacity: 0;
transform: translate(-50%, -48%) scale(0.96);
}
}
.animate-dialog-in {
animation: dialog-in var(--duration-normal) var(--ease-out) forwards;
}
.animate-dialog-out {
animation: dialog-out 150ms var(--ease-out) forwards;
}
/* Check pop animation for validation success */
@keyframes check-pop {
0% {
transform: scale(0.8);
@@ -400,14 +298,23 @@
}
}
.animate-fade-in {
animation: fadeIn 0.25s var(--ease-out) forwards;
}
.animate-message-in {
animation: messageIn 0.22s var(--ease-out) forwards;
}
.animate-panel-in {
animation: panelIn 0.32s var(--ease-out) both;
}
.animate-check-pop {
animation: check-pop 0.25s var(--ease-spring) forwards;
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
.animate-dialog-in,
.animate-dialog-out,
.animate-fade-in,
.animate-message-in,
.animate-panel-in,
.animate-check-pop {
animation: none;
}