This commit is contained in:
Arjun 2026-05-17 00:32:07 +05:30
parent 6492cf65b5
commit 5f7359d135
6 changed files with 231 additions and 78 deletions

View file

@ -0,0 +1,40 @@
# Rowboat Design Language
Rowboat should feel like a command center for people who live in notes, agents, email, meetings, and files all day. The launch direction is quiet, fast, and prosumer: dense enough for repeated work, warm enough to feel personal, and explicit about what the AI is doing.
## Principles
1. **Calm density**
Keep the interface compact and scannable. Use tighter rows, restrained borders, and low-contrast panels so users can keep many contexts open without the app feeling heavy.
2. **Command first**
Primary actions should feel like instant commands, not marketing CTAs. Side navigation, search, model selection, and composer controls use compact icon-led affordances with clear hover and selected states.
3. **Visible work state**
AI actions, sync, saving, meeting capture, and background tasks need clear status surfaces. Prefer small persistent indicators over large banners.
4. **Notes as the canvas**
The editor and conversation stay visually dominant. Chrome is supportive, not decorative. Avoid nested cards and oversized empty states in work surfaces.
5. **Warm precision**
The palette pairs ink, paper, and graphite neutrals with signal colors: teal for command/action, amber for attention, blue for structure, green for completion, and red for destructive state.
## Tokens
- Radius: `8px` for controls and cards, smaller where density matters.
- Backgrounds: warm paper in light mode, graphite ink in dark mode.
- Borders: one-step darker than surfaces, never pure gray unless inherited from OS content.
- Shadows: reserved for the composer, menus, dialogs, and active segmented controls.
- Type: system sans with tabular-feeling OpenType features enabled; no negative tracking.
## Core Surfaces
- **Sidebar:** persistent workflow switcher with calm selected states and a command-colored leading signal on active quick actions.
- **Titlebar/tabs:** slim, scan-first navigation. Active tabs get a bottom signal line, not a bulky filled pill.
- **Composer:** the highest-emphasis control outside the active canvas. It is slightly raised, bordered by the primary tone, and sharp enough to feel like an input terminal.
- **Messages:** user messages are compact structured blocks; assistant messages remain full-width and readable.
- **Status:** sync, saving, recording, and task activity stay small but always visible near the surface they affect.
## Launch Positioning
The visual story is: **Rowboat is the personal AI workspace for people whose work already spans meetings, mail, notes, browser tasks, and agents.** It should feel closer to a focused desktop tool than a chat website.

View file

@ -980,78 +980,94 @@
}
:root {
--radius: 0.625rem;
--background: var(--bg-color, oklch(1 0 0));
--foreground: var(--text-color, oklch(0.145 0 0));
--card: var(--bg-color, oklch(1 0 0));
--card-foreground: var(--text-color, oklch(0.145 0 0));
--popover: var(--bg-color, oklch(1 0 0));
--popover-foreground: var(--text-color, oklch(0.145 0 0));
--primary: var(--main-color, oklch(0.205 0 0));
--primary-foreground: var(--bg-color, oklch(0.985 0 0));
--secondary: var(--sub-alt-color, oklch(0.97 0 0));
--secondary-foreground: var(--text-color, oklch(0.205 0 0));
--muted: var(--sub-alt-color, oklch(0.97 0 0));
--muted-foreground: var(--sub-color, oklch(0.556 0 0));
--accent: var(--sub-color, oklch(0.97 0 0));
--accent-foreground: var(--text-color, oklch(0.205 0 0));
--destructive: var(--error-color, oklch(0.577 0.245 27.325));
--border: var(--sub-alt-color, oklch(0.922 0 0));
--input: var(--sub-alt-color, oklch(0.922 0 0));
--ring: var(--main-color, oklch(0.708 0 0));
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: var(--bg-color, oklch(0.985 0 0));
--sidebar-foreground: var(--text-color, oklch(0.145 0 0));
--sidebar-primary: var(--main-color, oklch(0.205 0 0));
--sidebar-primary-foreground: var(--bg-color, oklch(0.985 0 0));
--sidebar-accent: var(--sub-color, oklch(0.90 0 0));
--sidebar-accent-foreground: var(--text-color, oklch(0.205 0 0));
--sidebar-border: var(--sub-alt-color, oklch(0.922 0 0));
--sidebar-ring: var(--main-color, oklch(0.708 0 0));
--scrollbar-track: oklch(0.95 0 0);
--scrollbar-thumb: oklch(0.75 0 0);
--scrollbar-thumb-hover: oklch(0.65 0 0);
--radius: 0.5rem;
--background: var(--bg-color, oklch(0.982 0.0035 100));
--foreground: var(--text-color, oklch(0.205 0.018 255));
--card: var(--bg-color, oklch(0.998 0.0012 100));
--card-foreground: var(--text-color, oklch(0.205 0.018 255));
--popover: var(--bg-color, oklch(0.998 0.0012 100));
--popover-foreground: var(--text-color, oklch(0.205 0.018 255));
--primary: var(--main-color, oklch(0.355 0.055 228));
--primary-foreground: var(--bg-color, oklch(0.985 0.002 100));
--secondary: var(--sub-alt-color, oklch(0.94 0.007 100));
--secondary-foreground: var(--text-color, oklch(0.255 0.022 255));
--muted: var(--sub-alt-color, oklch(0.945 0.0065 100));
--muted-foreground: var(--sub-color, oklch(0.515 0.026 255));
--accent: var(--sub-color, oklch(0.91 0.0095 100));
--accent-foreground: var(--text-color, oklch(0.235 0.02 255));
--destructive: var(--error-color, oklch(0.57 0.19 24));
--border: var(--sub-alt-color, oklch(0.875 0.008 100));
--input: var(--sub-alt-color, oklch(0.89 0.0075 100));
--ring: var(--main-color, oklch(0.58 0.086 228));
--chart-1: oklch(0.55 0.14 230);
--chart-2: oklch(0.61 0.14 155);
--chart-3: oklch(0.62 0.16 42);
--chart-4: oklch(0.55 0.13 310);
--chart-5: oklch(0.66 0.12 92);
--sidebar: var(--bg-color, oklch(0.94 0.0065 104));
--sidebar-foreground: var(--text-color, oklch(0.245 0.02 255));
--sidebar-primary: var(--main-color, oklch(0.355 0.055 228));
--sidebar-primary-foreground: var(--bg-color, oklch(0.985 0.0018 104));
--sidebar-accent: var(--sub-color, oklch(0.902 0.008 104));
--sidebar-accent-foreground: var(--text-color, oklch(0.24 0.02 255));
--sidebar-border: var(--sub-alt-color, oklch(0.835 0.0072 104));
--sidebar-ring: var(--main-color, oklch(0.58 0.086 228));
--scrollbar-track: oklch(0.935 0.0055 100);
--scrollbar-thumb: oklch(0.72 0.0105 100);
--scrollbar-thumb-hover: oklch(0.62 0.013 100);
--rowboat-panel: oklch(0.972 0.0035 100);
--rowboat-raised: oklch(0.998 0.0012 100);
--rowboat-wash: color-mix(in oklab, var(--background) 88%, var(--primary) 12%);
--rowboat-hairline: color-mix(in oklab, var(--border) 78%, var(--foreground) 22%);
--rowboat-command: oklch(0.46 0.11 167);
--rowboat-attention: oklch(0.66 0.15 47);
--rowboat-shadow: 0 1px 2px rgb(31 38 48 / 0.07), 0 18px 40px rgb(31 38 48 / 0.08);
--rowboat-shadow-soft: 0 1px 2px rgb(31 38 48 / 0.05), 0 8px 24px rgb(31 38 48 / 0.06);
}
.dark {
--background: var(--bg-color, oklch(0.145 0 0));
--foreground: var(--text-color, oklch(0.985 0 0));
--card: var(--bg-color, oklch(0.205 0 0));
--card-foreground: var(--text-color, oklch(0.985 0 0));
--popover: var(--bg-color, oklch(0.205 0 0));
--popover-foreground: var(--text-color, oklch(0.985 0 0));
--primary: var(--main-color, oklch(0.922 0 0));
--primary-foreground: var(--bg-color, oklch(0.205 0 0));
--secondary: var(--sub-alt-color, oklch(0.269 0 0));
--secondary-foreground: var(--text-color, oklch(0.985 0 0));
--muted: var(--sub-alt-color, oklch(0.269 0 0));
--muted-foreground: var(--sub-color, oklch(0.708 0 0));
--accent: var(--sub-color, oklch(0.269 0 0));
--accent-foreground: var(--text-color, oklch(0.985 0 0));
--destructive: var(--error-color, oklch(0.704 0.191 22.216));
--border: var(--sub-alt-color, oklch(1 0 0 / 10%));
--input: var(--sub-alt-color, oklch(1 0 0 / 15%));
--ring: var(--main-color, oklch(0.556 0 0));
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: var(--bg-color, oklch(0.205 0 0));
--sidebar-foreground: var(--text-color, oklch(0.985 0 0));
--sidebar-primary: var(--main-color, oklch(0.488 0.243 264.376));
--sidebar-primary-foreground: var(--bg-color, oklch(0.985 0 0));
--sidebar-accent: var(--sub-color, oklch(0.35 0 0));
--sidebar-accent-foreground: var(--text-color, oklch(0.985 0 0));
--sidebar-border: var(--sub-alt-color, oklch(1 0 0 / 10%));
--sidebar-ring: var(--main-color, oklch(0.556 0 0));
--scrollbar-track: oklch(0.2 0 0);
--scrollbar-thumb: oklch(0.4 0 0);
--scrollbar-thumb-hover: oklch(0.5 0 0);
--background: var(--bg-color, oklch(0.18 0.012 255));
--foreground: var(--text-color, oklch(0.93 0.008 86));
--card: var(--bg-color, oklch(0.225 0.015 255));
--card-foreground: var(--text-color, oklch(0.93 0.008 86));
--popover: var(--bg-color, oklch(0.235 0.016 255));
--popover-foreground: var(--text-color, oklch(0.93 0.008 86));
--primary: var(--main-color, oklch(0.77 0.072 86));
--primary-foreground: var(--bg-color, oklch(0.17 0.012 255));
--secondary: var(--sub-alt-color, oklch(0.285 0.017 255));
--secondary-foreground: var(--text-color, oklch(0.92 0.008 86));
--muted: var(--sub-alt-color, oklch(0.285 0.017 255));
--muted-foreground: var(--sub-color, oklch(0.68 0.016 86));
--accent: var(--sub-color, oklch(0.34 0.023 248));
--accent-foreground: var(--text-color, oklch(0.95 0.006 86));
--destructive: var(--error-color, oklch(0.68 0.17 24));
--border: var(--sub-alt-color, oklch(0.36 0.02 255 / 0.72));
--input: var(--sub-alt-color, oklch(0.36 0.02 255 / 0.78));
--ring: var(--main-color, oklch(0.74 0.072 86));
--chart-1: oklch(0.72 0.1 225);
--chart-2: oklch(0.72 0.13 155);
--chart-3: oklch(0.76 0.14 48);
--chart-4: oklch(0.69 0.12 310);
--chart-5: oklch(0.78 0.11 92);
--sidebar: var(--bg-color, oklch(0.155 0.013 255));
--sidebar-foreground: var(--text-color, oklch(0.89 0.01 86));
--sidebar-primary: var(--main-color, oklch(0.77 0.072 86));
--sidebar-primary-foreground: var(--bg-color, oklch(0.17 0.012 255));
--sidebar-accent: var(--sub-color, oklch(0.25 0.017 255));
--sidebar-accent-foreground: var(--text-color, oklch(0.94 0.008 86));
--sidebar-border: var(--sub-alt-color, oklch(0.315 0.018 255 / 0.75));
--sidebar-ring: var(--main-color, oklch(0.74 0.072 86));
--scrollbar-track: oklch(0.21 0.014 255);
--scrollbar-thumb: oklch(0.42 0.02 255);
--scrollbar-thumb-hover: oklch(0.51 0.025 255);
--rowboat-panel: oklch(0.205 0.014 255);
--rowboat-raised: oklch(0.245 0.016 255);
--rowboat-wash: color-mix(in oklab, var(--background) 80%, var(--primary) 20%);
--rowboat-hairline: color-mix(in oklab, var(--border) 70%, var(--foreground) 30%);
--rowboat-command: oklch(0.66 0.095 168);
--rowboat-attention: oklch(0.78 0.13 58);
--rowboat-shadow: 0 1px 2px rgb(0 0 0 / 0.24), 0 22px 44px rgb(0 0 0 / 0.28);
--rowboat-shadow-soft: 0 1px 2px rgb(0 0 0 / 0.2), 0 12px 28px rgb(0 0 0 / 0.18);
}
@layer base {
@ -1063,6 +1079,9 @@
body {
@apply bg-background text-foreground;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
text-rendering: optimizeLegibility;
}
::-webkit-scrollbar {
@ -1084,6 +1103,100 @@
}
}
.rowboat-shell {
background:
linear-gradient(180deg, color-mix(in oklab, var(--background) 86%, var(--rowboat-wash) 14%) 0%, var(--background) 46%),
var(--background);
}
.rowboat-sidebar [data-sidebar="sidebar"] {
background:
linear-gradient(180deg, color-mix(in oklab, var(--sidebar) 90%, var(--primary) 10%) 0%, var(--sidebar) 38%),
var(--sidebar);
border-right: 1px solid var(--sidebar-border);
}
.rowboat-sidebar [data-sidebar="header"] {
gap: 0.625rem;
border-bottom: 1px solid color-mix(in oklab, var(--sidebar-border) 76%, transparent);
background: linear-gradient(180deg, color-mix(in oklab, var(--sidebar) 88%, var(--primary) 12%), var(--sidebar));
}
.rowboat-section-switcher {
border: 1px solid color-mix(in oklab, var(--sidebar-border) 78%, transparent);
background: color-mix(in oklab, var(--sidebar-accent) 58%, var(--sidebar) 42%);
box-shadow: inset 0 1px 0 color-mix(in oklab, var(--foreground) 9%, transparent);
}
.rowboat-section-switcher button {
letter-spacing: 0;
}
.rowboat-section-switcher button[class*="shadow-sm"] {
background: var(--rowboat-raised);
box-shadow: var(--rowboat-shadow-soft);
}
.rowboat-quick-actions button {
min-height: 2rem;
}
.rowboat-quick-actions button svg {
color: color-mix(in oklab, var(--rowboat-command) 72%, var(--sidebar-foreground) 28%);
}
.rowboat-tabbar {
background: color-mix(in oklab, var(--background) 78%, var(--muted) 22%);
}
.rowboat-titlebar {
background:
linear-gradient(180deg, color-mix(in oklab, var(--sidebar) 82%, var(--background) 18%), var(--background));
border-bottom-color: color-mix(in oklab, var(--border) 82%, transparent);
}
.rowboat-tab {
min-height: 2.5rem;
border-bottom: 1px solid transparent;
}
.rowboat-tab[class*="bg-background"] {
border-bottom-color: var(--primary);
background: linear-gradient(180deg, var(--rowboat-raised), var(--background));
box-shadow: inset 0 1px 0 color-mix(in oklab, var(--foreground) 8%, transparent);
}
.rowboat-composer-dock {
box-shadow: 0 -20px 44px color-mix(in oklab, var(--background) 78%, transparent);
}
.rowboat-chat-input {
border-color: color-mix(in oklab, var(--border) 74%, var(--primary) 26%);
background:
linear-gradient(180deg, color-mix(in oklab, var(--rowboat-raised) 96%, var(--primary) 4%), var(--rowboat-raised));
box-shadow: var(--rowboat-shadow);
}
.rowboat-chat-input:focus-within {
border-color: color-mix(in oklab, var(--ring) 76%, var(--border) 24%);
box-shadow:
0 0 0 1px color-mix(in oklab, var(--ring) 38%, transparent),
var(--rowboat-shadow);
}
[data-slot="message-content"] {
line-height: 1.62;
}
.is-user [data-slot="message-content"] {
background: color-mix(in oklab, var(--secondary) 88%, var(--rowboat-wash) 12%);
border: 1px solid color-mix(in oklab, var(--border) 68%, transparent);
}
.is-assistant [data-slot="message-content"] {
color: color-mix(in oklab, var(--foreground) 94%, var(--muted-foreground) 6%);
}
/* Markdown content base styles for Streamdown/MessageResponse */
@layer components {
/* Target elements inside MessageResponse wrapper */

View file

@ -663,7 +663,7 @@ function ContentHeader({
const isCollapsed = state === "collapsed"
return (
<header
className="titlebar-drag-region flex h-10 shrink-0 items-stretch border-b border-border bg-sidebar overflow-hidden"
className="rowboat-titlebar titlebar-drag-region flex h-10 shrink-0 items-stretch border-b border-border bg-sidebar overflow-hidden"
style={{
paddingLeft: isCollapsed ? (collapsedLeftPaddingPx ?? 196) : 12,
paddingRight: 12,
@ -4686,7 +4686,7 @@ function App() {
void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH })
}
}}>
<div className="flex h-svh w-full overflow-hidden">
<div className="rowboat-shell flex h-svh w-full overflow-hidden">
{/* Content sidebar with SidebarProvider for collapse functionality */}
<SidebarProvider
style={{
@ -5263,7 +5263,7 @@ function App() {
})}
</div>
<div className="sticky bottom-0 z-10 bg-background pb-12 pt-0 shadow-lg">
<div className="rowboat-composer-dock sticky bottom-0 z-10 bg-background pb-12 pt-0 shadow-lg">
<div className="pointer-events-none absolute inset-x-0 -top-6 h-6 bg-linear-to-t from-background to-transparent" />
<div className="mx-auto w-full max-w-4xl px-4">
{!hasConversation && (

View file

@ -434,7 +434,7 @@ function ChatInputInner({
}, [addFiles, isActive])
return (
<div className="rounded-lg border border-border bg-background shadow-none">
<div className="rowboat-chat-input rounded-lg border border-border bg-background shadow-none">
{attachments.length > 0 && (
<div className="flex flex-wrap gap-2 px-4 pb-1 pt-3">
{attachments.map((attachment) => {

View file

@ -566,13 +566,13 @@ export function SidebarContentPanel({
}, [])
return (
<Sidebar className="border-r-0" {...props}>
<Sidebar className="rowboat-sidebar border-r-0" {...props}>
<SidebarHeader className="titlebar-drag-region">
{/* Top spacer to clear the traffic lights + fixed toggle row */}
<div className="h-8" />
{/* Tab switcher - centered below the traffic lights row */}
<div className="flex items-center px-2 py-1.5">
<div className="titlebar-no-drag flex w-full rounded-lg bg-sidebar-accent/50 p-0.5">
<div className="rowboat-section-switcher titlebar-no-drag flex w-full rounded-lg bg-sidebar-accent/50 p-0.5">
{sectionTabs.map((tab) => (
<button
key={tab.id}
@ -590,7 +590,7 @@ export function SidebarContentPanel({
</div>
</div>
{/* Quick action buttons */}
<div className="titlebar-no-drag flex flex-col gap-0.5 px-2 pb-1">
<div className="rowboat-quick-actions titlebar-no-drag flex flex-col gap-0.5 px-2 pb-1">
{onNewChat && (
<button
type="button"

View file

@ -37,7 +37,7 @@ export function TabBar<T>({
return (
<div
className={cn(
'flex flex-1 self-stretch min-w-0',
'rowboat-tabbar flex flex-1 self-stretch min-w-0',
layout === 'scroll'
? 'overflow-x-auto overflow-y-hidden [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'
: 'overflow-hidden'
@ -57,7 +57,7 @@ export function TabBar<T>({
type="button"
onClick={() => onSwitchTab(tabId)}
className={cn(
'titlebar-no-drag group/tab relative flex items-center gap-1.5 px-3 self-stretch text-xs transition-colors',
'rowboat-tab titlebar-no-drag group/tab relative flex items-center gap-1.5 px-3 self-stretch text-xs transition-colors',
layout === 'scroll' ? 'min-w-[140px] max-w-[240px]' : 'min-w-0 max-w-[220px]',
isActive
? 'bg-background text-foreground'