From 753d8658919d79f73693104bf32be00907934346 Mon Sep 17 00:00:00 2001 From: Haoqing Wang Date: Sat, 18 Apr 2026 12:19:00 +0800 Subject: [PATCH 1/2] feat(ui): add IconButton + Tooltip primitives and feedback tokens - IconButton: compact square button for toolbar/topbar use, with focus ring and disabled state aligned to existing Button. - Tooltip: pure-CSS tooltip with 400ms hover delay (no JS, no Radix). - Tokens: add --color-toast-success, --color-toast-error, --color-overlay for both light and dark palettes. Tier 1 only: no Radix, no animation libs. Signed-off-by: Haoqing Wang --- packages/ui/src/components/IconButton.tsx | 31 +++++++++++++++++++++++ packages/ui/src/components/Tooltip.tsx | 26 +++++++++++++++++++ packages/ui/src/index.ts | 4 +++ packages/ui/src/tokens.css | 9 +++++++ 4 files changed, 70 insertions(+) create mode 100644 packages/ui/src/components/IconButton.tsx create mode 100644 packages/ui/src/components/Tooltip.tsx diff --git a/packages/ui/src/components/IconButton.tsx b/packages/ui/src/components/IconButton.tsx new file mode 100644 index 00000000..db927c17 --- /dev/null +++ b/packages/ui/src/components/IconButton.tsx @@ -0,0 +1,31 @@ +import type { ButtonHTMLAttributes, ReactNode } from 'react'; + +export interface IconButtonProps extends ButtonHTMLAttributes { + size?: 'sm' | 'md'; + label: string; + children: ReactNode; +} + +const sizeClass: Record, string> = { + sm: 'h-7 w-7', + md: 'h-9 w-9', +}; + +export function IconButton({ + size = 'md', + label, + className = '', + children, + ...rest +}: IconButtonProps) { + return ( + + ); +} diff --git a/packages/ui/src/components/Tooltip.tsx b/packages/ui/src/components/Tooltip.tsx new file mode 100644 index 00000000..ce5823b3 --- /dev/null +++ b/packages/ui/src/components/Tooltip.tsx @@ -0,0 +1,26 @@ +import type { ReactNode } from 'react'; + +export interface TooltipProps { + label: string; + side?: 'top' | 'bottom'; + children: ReactNode; +} + +const sideClass: Record, string> = { + top: 'bottom-full mb-1.5 left-1/2 -translate-x-1/2', + bottom: 'top-full mt-1.5 left-1/2 -translate-x-1/2', +}; + +export function Tooltip({ label, side = 'bottom', children }: TooltipProps) { + return ( + + {children} + + {label} + + + ); +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 7be7a34f..999faab6 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1,4 +1,8 @@ export { Button } from './components/Button'; export { Card } from './components/Card'; +export { IconButton } from './components/IconButton'; +export { Tooltip } from './components/Tooltip'; export type { ButtonProps } from './components/Button'; export type { CardProps } from './components/Card'; +export type { IconButtonProps } from './components/IconButton'; +export type { TooltipProps } from './components/Tooltip'; diff --git a/packages/ui/src/tokens.css b/packages/ui/src/tokens.css index 2e3714cd..c2509d6a 100644 --- a/packages/ui/src/tokens.css +++ b/packages/ui/src/tokens.css @@ -34,6 +34,11 @@ --color-error: #b04030; --color-mcp: #4a6b8a; + /* Feedback */ + --color-toast-success: #4f7a52; + --color-toast-error: #b04030; + --color-overlay: rgba(31, 29, 24, 0.45); + /* Shadow */ --shadow-soft: 0 1px 2px rgba(31, 29, 24, 0.04); --shadow-card: 0 2px 8px rgba(31, 29, 24, 0.06); @@ -76,6 +81,10 @@ --color-error: #d96050; --color-mcp: #6a8aab; + --color-toast-success: #6b9c6e; + --color-toast-error: #d96050; + --color-overlay: rgba(0, 0, 0, 0.6); + --shadow-soft: 0 1px 2px rgba(0, 0, 0, 0.3); --shadow-card: 0 2px 8px rgba(0, 0, 0, 0.4); --shadow-elevated: 0 8px 24px rgba(0, 0, 0, 0.5); From 7f3bd6afea502fd06ea08c28678dbcca5e4bcd34 Mon Sep 17 00:00:00 2001 From: Haoqing Wang Date: Sat, 18 Apr 2026 12:19:17 +0800 Subject: [PATCH 2/2] feat(desktop): settings + theme + command palette + better preview states Reshape the renderer so the app feels like a real product even before any AI call succeeds. All Tier 1, no new prod deps. Added: - TopBar with breadcrumb + 3 icon-buttons (palette / theme / settings), using the macOS hiddenInset drag region. - Settings overlay with 4 tabs: Models (stub), Appearance (theme picker), Storage (config file path), Advanced (placeholder). - CommandPalette overlay (Cmd+K) with 4 actions: New Design, Toggle Theme, Open Settings, Export (stub). - ThemeToggle: persists to localStorage; index.html bootstraps the `.dark` class inline before React mounts to avoid FOUC. - ToastViewport (Zustand-backed, auto-dismiss 4s, stack bottom-right) surfacing generation success and errors. - PreviewPane with three explicit states: EmptyState (chips populate the prompt input), LoadingState (CSS shimmer), ErrorState (Retry + Copy Error card). - useKeyboard hook: central keymap registry on plain addEventListener. Cmd+Enter sends, Cmd+, opens Settings, Cmd+K opens palette, Esc closes overlays. - Sidebar extracted from App.tsx; App.tsx is now a slim composition. Store gains: theme, settingsOpen, commandPaletteOpen, toasts, lastError, plus retryLastPrompt + clearError. No silent fallbacks: every error path pushes a toast and surfaces in ErrorState. All UI uses var(--color-*) tokens; no hex literals; no new hardcoded px sizes added. Signed-off-by: Haoqing Wang --- apps/desktop/src/renderer/index.html | 10 + apps/desktop/src/renderer/src/App.tsx | 191 +++++++----------- .../src/components/CommandPalette.tsx | 175 ++++++++++++++++ .../renderer/src/components/PreviewPane.tsx | 48 +++++ .../src/renderer/src/components/Settings.tsx | 168 +++++++++++++++ .../src/renderer/src/components/Sidebar.tsx | 68 +++++++ .../renderer/src/components/ThemeToggle.tsx | 16 ++ .../src/renderer/src/components/Toast.tsx | 64 ++++++ .../src/renderer/src/components/TopBar.tsx | 53 +++++ .../src/renderer/src/hooks/useKeyboard.ts | 45 +++++ apps/desktop/src/renderer/src/index.css | 51 +++++ .../src/renderer/src/preview/EmptyState.tsx | 56 +++++ .../src/renderer/src/preview/ErrorState.tsx | 57 ++++++ .../src/renderer/src/preview/LoadingState.tsx | 22 ++ apps/desktop/src/renderer/src/store.ts | 122 ++++++++++- 15 files changed, 1025 insertions(+), 121 deletions(-) create mode 100644 apps/desktop/src/renderer/src/components/CommandPalette.tsx create mode 100644 apps/desktop/src/renderer/src/components/PreviewPane.tsx create mode 100644 apps/desktop/src/renderer/src/components/Settings.tsx create mode 100644 apps/desktop/src/renderer/src/components/Sidebar.tsx create mode 100644 apps/desktop/src/renderer/src/components/ThemeToggle.tsx create mode 100644 apps/desktop/src/renderer/src/components/Toast.tsx create mode 100644 apps/desktop/src/renderer/src/components/TopBar.tsx create mode 100644 apps/desktop/src/renderer/src/hooks/useKeyboard.ts create mode 100644 apps/desktop/src/renderer/src/preview/EmptyState.tsx create mode 100644 apps/desktop/src/renderer/src/preview/ErrorState.tsx create mode 100644 apps/desktop/src/renderer/src/preview/LoadingState.tsx diff --git a/apps/desktop/src/renderer/index.html b/apps/desktop/src/renderer/index.html index 61e7933a..9c339e8c 100644 --- a/apps/desktop/src/renderer/index.html +++ b/apps/desktop/src/renderer/index.html @@ -14,6 +14,16 @@
+ diff --git a/apps/desktop/src/renderer/src/App.tsx b/apps/desktop/src/renderer/src/App.tsx index e761430c..e0ef2917 100644 --- a/apps/desktop/src/renderer/src/App.tsx +++ b/apps/desktop/src/renderer/src/App.tsx @@ -1,131 +1,86 @@ -import { buildSrcdoc } from '@open-codesign/runtime'; -import { BUILTIN_DEMOS } from '@open-codesign/templates'; -import { Button } from '@open-codesign/ui'; -import { Send, Sparkles } from 'lucide-react'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; +import { CommandPalette } from './components/CommandPalette'; +import { PreviewPane } from './components/PreviewPane'; +import { Settings } from './components/Settings'; +import { Sidebar } from './components/Sidebar'; +import { ToastViewport } from './components/Toast'; +import { TopBar } from './components/TopBar'; +import { useKeyboard } from './hooks/useKeyboard'; import { useCodesignStore } from './store'; export function App() { - const messages = useCodesignStore((s) => s.messages); - const previewHtml = useCodesignStore((s) => s.previewHtml); - const isGenerating = useCodesignStore((s) => s.isGenerating); const sendPrompt = useCodesignStore((s) => s.sendPrompt); + const isGenerating = useCodesignStore((s) => s.isGenerating); + const openSettings = useCodesignStore((s) => s.openSettings); + const closeSettings = useCodesignStore((s) => s.closeSettings); + const openCommandPalette = useCodesignStore((s) => s.openCommandPalette); + const closeCommandPalette = useCodesignStore((s) => s.closeCommandPalette); + const settingsOpen = useCodesignStore((s) => s.settingsOpen); + const commandPaletteOpen = useCodesignStore((s) => s.commandPaletteOpen); + const [prompt, setPrompt] = useState(''); - function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - if (!prompt.trim() || isGenerating) return; - void sendPrompt(prompt); + function submit() { + const trimmed = prompt.trim(); + if (!trimmed || isGenerating) return; + void sendPrompt(trimmed); setPrompt(''); } - return ( -
- + const bindings = useMemo( + () => [ + { + combo: 'mod+enter', + handler: () => { + const trimmed = prompt.trim(); + if (!trimmed || isGenerating) return; + void sendPrompt(trimmed); + setPrompt(''); + }, + }, + { + combo: 'mod+,', + handler: () => openSettings(), + }, + { + combo: 'mod+k', + handler: () => openCommandPalette(), + }, + { + combo: 'escape', + handler: () => { + if (settingsOpen) closeSettings(); + else if (commandPaletteOpen) closeCommandPalette(); + }, + preventDefault: false, + }, + ], + [ + prompt, + isGenerating, + sendPrompt, + settingsOpen, + commandPaletteOpen, + openSettings, + openCommandPalette, + closeSettings, + closeCommandPalette, + ], + ); + useKeyboard(bindings); -
-
- - {previewHtml ? 'Preview' : 'No design yet'} - - - BYOK · local-first · multi-model - -
-
- {previewHtml ? ( -