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

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
- the app's own canvas toolbar, shape library and page tabs replace
  draw.io's chrome
- new start screen, session rename and switcher, open .drawio files
- settings split into Models & keys, Appearance, Advanced and About
- phones switch between canvas and chat
- dark mode keeps the diagram on a white sheet and no longer flashes

With an external draw.io (NEXT_PUBLIC_DRAWIO_BASE_URL) the app falls back to
draw.io's own toolbar and 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-06 16:17:11 +09:00
parent 18a20aa1ca
commit 23d5c83b0d
105 changed files with 12499 additions and 7770 deletions
+260 -242
View File
@@ -8,7 +8,7 @@
@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);
@@ -43,157 +43,13 @@
--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);
@@ -206,12 +62,185 @@
--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;
--chart-1: #3b6fd8;
--chart-2: #1f9e86;
--chart-3: #e8892c;
--chart-4: #c2417a;
--chart-5: #2a8fb8;
--sidebar: #ffffff;
--sidebar-foreground: #14181d;
--sidebar-primary: #14181d;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #f1f3f5;
--sidebar-accent-foreground: #14181d;
--sidebar-border: #e4e7eb;
--sidebar-ring: #14181d;
--surface-0: #ffffff;
--surface-1: #f7f8fa;
--surface-2: #f1f3f5;
--surface-elevated: #ffffff;
--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-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);
}
/*
* 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 {
--canvas: #0f2440;
--background: #0c1a29;
--foreground: #e9f0f6;
--card: #16304f;
--card-foreground: #e9f0f6;
--popover: #1b3a5d;
--popover-foreground: #e9f0f6;
--primary: #e9f0f6;
--primary-foreground: #0c1a29;
--secondary: #1d3b5e;
--secondary-foreground: #e9f0f6;
--muted: #1d3b5e;
--muted-foreground: #a8bbce;
--faint: #8aa0b6;
--accent: #21426a;
--accent-foreground: #e9f0f6;
--destructive: #f07167;
--border: rgb(170 200 230 / 0.16);
--input: rgb(170 200 230 / 0.24);
--ring: #e9f0f6;
--marker: #f5b13d;
--marker-soft: rgb(245 177 61 / 0.16);
--marker-ink: #f7c46b;
--chart-1: #7aa2ff;
--chart-2: #4fd1b4;
--chart-3: #f5b13d;
--chart-4: #f08bb4;
--chart-5: #5cc3ea;
--sidebar: #16304f;
--sidebar-foreground: #e9f0f6;
--sidebar-primary: #e9f0f6;
--sidebar-primary-foreground: #0c1a29;
--sidebar-accent: #1d3b5e;
--sidebar-accent-foreground: #e9f0f6;
--sidebar-border: rgb(170 200 230 / 0.16);
--sidebar-ring: #e9f0f6;
--surface-0: #16304f;
--surface-1: #132b48;
--surface-2: #1d3b5e;
--surface-elevated: #1b3a5d;
--border-subtle: rgb(170 200 230 / 0.12);
--border-default: rgb(170 200 230 / 0.2);
--interactive-hover: #21426a;
--interactive-active: #264a75;
--success: #4fd18b;
--success-muted: rgb(79 209 139 / 0.14);
--shadow-float:
0 0 0 1px rgb(170 200 230 / 0.1), 0 14px 34px -12px rgb(0 0 0 / 0.55);
--shadow-pop:
0 0 0 1px rgb(170 200 230 / 0.14), 0 22px 56px -14px rgb(0 0 0 / 0.65);
}
@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,49 +250,61 @@
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-soft-lg {
box-shadow: var(--shadow-pop);
}
.shadow-dialog {
box-shadow: var(--shadow-pop);
}
}
/* Smooth page transitions */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(8px);
transform: translateY(6px);
}
to {
opacity: 1;
@@ -282,83 +323,44 @@
}
}
.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);
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
transform: translateY(0);
}
}
.animate-message-in {
animation: messageIn 0.25s ease-out forwards;
/* The first prompt moves from the middle of the canvas into the panel */
@keyframes panelIn {
from {
opacity: 0;
transform: translateX(12px) scale(0.985);
}
to {
opacity: 1;
transform: translateX(0) scale(1);
}
}
/* 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);
@keyframes heroOut {
from {
opacity: 1;
transform: translateY(0) scale(1);
}
to {
opacity: 0;
transform: translateY(-8px) scale(0.98);
}
}
.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);
transform: translate(-50%, -48%) scale(0.97);
}
to {
opacity: 1;
@@ -373,19 +375,10 @@
}
to {
opacity: 0;
transform: translate(-50%, -48%) scale(0.96);
transform: translate(-50%, -48%) scale(0.97);
}
}
.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,12 +393,37 @@
}
}
.animate-fade-in {
animation: fadeIn 0.25s var(--ease-out) forwards;
}
.animate-slide-in-right {
animation: slideInRight 0.3s 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-hero-out {
animation: heroOut 0.2s var(--ease-out) forwards;
}
.animate-dialog-in {
animation: dialog-in var(--duration-normal) var(--ease-out) forwards;
}
.animate-dialog-out {
animation: dialog-out 150ms var(--ease-out) forwards;
}
.animate-check-pop {
animation: check-pop 0.25s var(--ease-spring) forwards;
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
.animate-fade-in,
.animate-slide-in-right,
.animate-message-in,
.animate-panel-in,
.animate-hero-out,
.animate-dialog-in,
.animate-dialog-out,
.animate-check-pop {