feat(ui): "Graphite & Honey" redesign — warm dark, monospace-first

- new design system in globals.css: warm graphite surfaces, ivory text, honey
  accent; semantic status/data-type/syntax tokens replacing hardcoded colors
- IBM Plex Mono as the universal UI font (sans + mono), tabular numerals
- custom CodeMirror SQL theme (src/lib/editor-theme.ts) matching the palette
- data grid: zebra striping + honey row hover, stronger sticky header
- route status dots, JSON syntax, EXPLAIN cost, schema-tree icons and the
  read/write toggle through the new tokens
- TUSK wordmark in the toolbar
This commit is contained in:
2026-05-23 15:02:19 +03:00
parent c73339bb4c
commit da0001e77e
16 changed files with 346 additions and 209 deletions

View File

@@ -6,7 +6,10 @@
<title>Tusk</title> <title>Tusk</title>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" /> <link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400;1,500&display=swap"
rel="stylesheet"
/>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -111,13 +111,13 @@ function UsageBadge({ usage }: { usage: ContextUsage | undefined }) {
let toneClass = "text-muted-foreground/70"; let toneClass = "text-muted-foreground/70";
if (ratio >= 0.85) toneClass = "text-destructive"; if (ratio >= 0.85) toneClass = "text-destructive";
else if (ratio >= 0.6) toneClass = "text-amber-500"; else if (ratio >= 0.6) toneClass = "text-warning";
else if (ratio >= 0.3) toneClass = "text-emerald-500/80"; else if (ratio >= 0.3) toneClass = "text-success/80";
const trackClass = "h-1.5 w-12 overflow-hidden rounded-full bg-muted"; const trackClass = "h-1.5 w-12 overflow-hidden rounded-full bg-muted";
let fillClass = "bg-emerald-500/70"; let fillClass = "bg-success/70";
if (ratio >= 0.85) fillClass = "bg-destructive"; if (ratio >= 0.85) fillClass = "bg-destructive";
else if (ratio >= 0.6) fillClass = "bg-amber-500"; else if (ratio >= 0.6) fillClass = "bg-warning";
return ( return (
<Tooltip> <Tooltip>

View File

@@ -3,6 +3,7 @@ import { sql, PostgreSQL, StandardSQL } from "@codemirror/lang-sql";
import { keymap } from "@codemirror/view"; import { keymap } from "@codemirror/view";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useAppStore } from "@/stores/app-store"; import { useAppStore } from "@/stores/app-store";
import { tuskEditorExtensions } from "@/lib/editor-theme";
interface Props { interface Props {
value: string; value: string;
@@ -44,6 +45,7 @@ export function SqlEditor({ value, onChange, onExecute, onFormat, schema }: Prop
const dialect = flavor === "clickhouse" ? StandardSQL : PostgreSQL; const dialect = flavor === "clickhouse" ? StandardSQL : PostgreSQL;
const defaultSchema = flavor === "clickhouse" ? undefined : "public"; const defaultSchema = flavor === "clickhouse" ? undefined : "public";
return [ return [
tuskEditorExtensions,
sql({ sql({
dialect, dialect,
schema: sqlNamespace, schema: sqlNamespace,
@@ -80,7 +82,6 @@ export function SqlEditor({ value, onChange, onExecute, onFormat, schema }: Prop
value={value} value={value}
onChange={handleChange} onChange={handleChange}
extensions={extensions} extensions={extensions}
theme="dark"
// height="100%" propagates down to .cm-editor so the inner .cm-scroller // height="100%" propagates down to .cm-editor so the inner .cm-scroller
// can render a vertical scrollbar; without it, long queries overflow the // can render a vertical scrollbar; without it, long queries overflow the
// flex container and the editor cannot be scrolled. // flex container and the editor cannot be scrolled.

View File

@@ -57,9 +57,9 @@ export function HistoryPanel() {
> >
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{entry.status === "success" ? ( {entry.status === "success" ? (
<CheckCircle className="h-3 w-3 shrink-0 text-green-500" /> <CheckCircle className="h-3 w-3 shrink-0 text-success" />
) : ( ) : (
<XCircle className="h-3 w-3 shrink-0 text-red-500" /> <XCircle className="h-3 w-3 shrink-0 text-destructive" />
)} )}
<span className="truncate font-mono text-foreground"> <span className="truncate font-mono text-foreground">
{entry.sql.length > 80 {entry.sql.length > 80

View File

@@ -39,8 +39,8 @@ export function ReadOnlyToggle() {
size="xs" size="xs"
className={`gap-1.5 font-medium ${ className={`gap-1.5 font-medium ${
isReadOnly isReadOnly
? "text-amber-500 hover:bg-amber-500/10 hover:text-amber-500" ? "text-warning hover:bg-warning/10 hover:text-warning"
: "text-emerald-500 hover:bg-emerald-500/10 hover:text-emerald-500" : "text-success hover:bg-success/10 hover:text-success"
}`} }`}
onClick={handleToggle} onClick={handleToggle}
disabled={toggleMutation.isPending} disabled={toggleMutation.isPending}

View File

@@ -42,7 +42,7 @@ export function StatusBar({ rowCount, executionTime }: Props) {
<span <span
className={`inline-block h-2 w-2 rounded-full ${ className={`inline-block h-2 w-2 rounded-full ${
isConnected isConnected
? "bg-emerald-500 shadow-[0_0_6px_theme(--color-emerald-500/40)]" ? "bg-success ring-2 ring-success/25"
: "bg-muted-foreground/30" : "bg-muted-foreground/30"
}`} }`}
/> />
@@ -56,8 +56,8 @@ export function StatusBar({ rowCount, executionTime }: Props) {
<span <span
className={`rounded px-1 py-px text-[10px] font-semibold tracking-wide ${ className={`rounded px-1 py-px text-[10px] font-semibold tracking-wide ${
(readOnlyMap[activeConnectionId] ?? true) (readOnlyMap[activeConnectionId] ?? true)
? "bg-amber-500/10 text-amber-500" ? "bg-warning/10 text-warning"
: "bg-emerald-500/10 text-emerald-500" : "bg-success/10 text-success"
}`} }`}
> >
{(readOnlyMap[activeConnectionId] ?? true) ? "READ" : "WRITE"} {(readOnlyMap[activeConnectionId] ?? true) ? "READ" : "WRITE"}
@@ -86,7 +86,7 @@ export function StatusBar({ rowCount, executionTime }: Props) {
<span <span
className={`inline-block h-1.5 w-1.5 rounded-full transition-colors ${ className={`inline-block h-1.5 w-1.5 rounded-full transition-colors ${
mcpStatus?.running mcpStatus?.running
? "bg-emerald-500 shadow-[0_0_4px_theme(--color-emerald-500/40)]" ? "bg-success ring-2 ring-success/25"
: "bg-muted-foreground/20" : "bg-muted-foreground/20"
}`} }`}
/> />

View File

@@ -66,6 +66,15 @@ export function Toolbar() {
"--strip-color": activeColor ?? "transparent", "--strip-color": activeColor ?? "transparent",
} as React.CSSProperties} } as React.CSSProperties}
> >
<span
className="tusk-wordmark select-none px-1 text-[12px] text-primary"
style={{ textShadow: "0 0 10px oklch(0.808 0.124 82 / 35%)" }}
>
TUSK
</span>
<div className="mx-1 h-4 w-px bg-border" />
<Button <Button
variant="ghost" variant="ghost"
size="xs" size="xs"

View File

@@ -131,7 +131,7 @@ export function MemoryPanel() {
)} )}
{dirty && ( {dirty && (
<div className="border-t border-border/40 px-3 py-1.5 text-[10px] text-amber-500/80"> <div className="border-t border-border/40 px-3 py-1.5 text-[10px] text-warning/80">
Unsaved changes Unsaved changes
</div> </div>
)} )}

View File

@@ -3,11 +3,11 @@ import { ChevronRight, ChevronDown } from "lucide-react";
import type { ExplainNode, ExplainResult } from "@/types"; import type { ExplainNode, ExplainResult } from "@/types";
function getCostColor(cost: number, maxCost: number): string { function getCostColor(cost: number, maxCost: number): string {
if (maxCost === 0) return "#22c55e"; if (maxCost === 0) return "var(--success)";
const ratio = cost / maxCost; const ratio = cost / maxCost;
if (ratio < 0.33) return "#22c55e"; if (ratio < 0.33) return "var(--success)";
if (ratio < 0.66) return "#eab308"; if (ratio < 0.66) return "var(--warning)";
return "#ef4444"; return "var(--destructive)";
} }
function getMaxCost(node: ExplainNode): number { function getMaxCost(node: ExplainNode): number {

View File

@@ -9,15 +9,15 @@ function syntaxHighlight(json: string): string {
return json.replace( return json.replace(
/("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, /("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,
(match) => { (match) => {
let cls = "text-blue-500 dark:text-blue-400"; // number let cls = "text-info"; // number
if (match.startsWith('"')) { if (match.startsWith('"')) {
if (match.endsWith(":")) { if (match.endsWith(":")) {
cls = "text-foreground"; // key cls = "text-foreground"; // key
} else { } else {
cls = "text-green-600 dark:text-green-400"; // string cls = "text-success"; // string
} }
} else if (/true|false/.test(match)) { } else if (/true|false/.test(match)) {
cls = "text-purple-500 dark:text-purple-400"; // boolean cls = "text-violet"; // boolean
} else if (match === "null") { } else if (match === "null") {
cls = "text-muted-foreground italic"; // null cls = "text-muted-foreground italic"; // null
} }

View File

@@ -190,7 +190,9 @@ export function ResultsTable({
return ( return (
<div <div
key={row.id} key={row.id}
className="tusk-grid-row absolute left-0 flex transition-colors" className={`tusk-grid-row absolute left-0 flex transition-colors ${
virtualRow.index % 2 === 1 ? "tusk-grid-row-odd" : ""
}`}
style={{ style={{
top: `${virtualRow.start}px`, top: `${virtualRow.start}px`,
height: `${virtualRow.size}px`, height: `${virtualRow.size}px`,
@@ -201,7 +203,7 @@ export function ResultsTable({
return ( return (
<div <div
key={cell.id} key={cell.id}
className="shrink-0 border-b border-r border-border/20 text-xs" className="shrink-0 border-r border-border/15 text-xs"
style={{ width: w, minWidth: w }} style={{ width: w, minWidth: w }}
> >
{flexRender( {flexRender(

View File

@@ -148,7 +148,7 @@ function QueryRow({
onDoubleClick={onOpen} onDoubleClick={onOpen}
> >
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Bookmark className="h-3 w-3 shrink-0 text-blue-400" /> <Bookmark className="h-3 w-3 shrink-0 text-info" />
<span className="truncate font-medium text-foreground"> <span className="truncate font-medium text-foreground">
{query.name} {query.name}
</span> </span>

View File

@@ -281,7 +281,7 @@ function SchemaNode({
)} )}
</span> </span>
{expanded ? ( {expanded ? (
<FolderOpen className="h-3.5 w-3.5 text-amber-500/70" /> <FolderOpen className="h-3.5 w-3.5 text-primary/70" />
) : ( ) : (
<Database className="h-3.5 w-3.5 text-muted-foreground/50" /> <Database className="h-3.5 w-3.5 text-muted-foreground/50" />
)} )}
@@ -328,10 +328,10 @@ function SchemaNode({
} }
const categoryIcons = { const categoryIcons = {
tables: <Table2 className="h-3.5 w-3.5 text-sky-400/80" />, tables: <Table2 className="h-3.5 w-3.5 text-data-table" />,
views: <Eye className="h-3.5 w-3.5 text-emerald-400/80" />, views: <Eye className="h-3.5 w-3.5 text-data-view" />,
functions: <FunctionSquare className="h-3.5 w-3.5 text-violet-400/80" />, functions: <FunctionSquare className="h-3.5 w-3.5 text-data-function" />,
sequences: <Hash className="h-3.5 w-3.5 text-amber-400/80" />, sequences: <Hash className="h-3.5 w-3.5 text-data-sequence" />,
}; };
function CategoryNode({ function CategoryNode({

View File

@@ -206,14 +206,14 @@ export function TableDataView({ connectionId, schema, table }: Props) {
}} }}
> >
{isReadOnly && ( {isReadOnly && (
<span className="flex items-center gap-1 rounded bg-yellow-500/10 px-1.5 py-0.5 text-[10px] font-semibold text-yellow-600 dark:text-yellow-500"> <span className="flex items-center gap-1 rounded bg-warning/10 px-1.5 py-0.5 text-[10px] font-semibold text-warning">
<Lock className="h-3 w-3" /> <Lock className="h-3 w-3" />
Read-Only Read-Only
</span> </span>
)} )}
{!isReadOnly && usesCtid && ( {!isReadOnly && usesCtid && (
<span <span
className="rounded bg-orange-500/10 px-1.5 py-0.5 text-[10px] font-medium text-orange-600 dark:text-orange-400" className="rounded bg-warning/10 px-1.5 py-0.5 text-[10px] font-medium text-warning"
title="This table has no primary key. Edits use physical row ID (ctid), which may change after VACUUM or concurrent writes." title="This table has no primary key. Edits use physical row ID (ctid), which may change after VACUUM or concurrent writes."
> >
No PK using ctid No PK using ctid

102
src/lib/editor-theme.ts Normal file
View File

@@ -0,0 +1,102 @@
import { EditorView } from "@codemirror/view";
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
import { tags as t } from "@lezer/highlight";
import type { Extension } from "@codemirror/state";
/* ───────────────────────────────────────────────────────────
Tusk "Graphite & Honey" CodeMirror theme.
Palette mirrors the design tokens in styles/globals.css so the
SQL editor sits seamlessly inside the warm graphite workstation.
─────────────────────────────────────────────────────────── */
const c = {
bg: "oklch(0.186 0.006 75)",
surface: "oklch(0.205 0.007 75)",
fg: "oklch(0.9 0.013 80)",
faint: "oklch(0.52 0.012 75)",
gutter: "oklch(0.46 0.011 75)",
gutterActive: "oklch(0.74 0.012 80)",
cursor: "oklch(0.84 0.13 82)",
selection: "oklch(0.808 0.124 82 / 22%)",
activeLine: "oklch(0.808 0.124 82 / 5%)",
activeGutter: "oklch(0.808 0.124 82 / 9%)",
matchBg: "oklch(0.808 0.124 82 / 18%)",
// syntax
keyword: "oklch(0.82 0.125 82)", // honey — SELECT, FROM, WHERE
string: "oklch(0.76 0.13 152)", // green
number: "oklch(0.74 0.12 222)", // cyan-blue
func: "oklch(0.74 0.15 305)", // violet — built-ins
type: "oklch(0.74 0.12 200)", // teal-cyan — types
comment: "oklch(0.5 0.012 75)", // muted, italic
operator: "oklch(0.74 0.013 80)",
bracket: "oklch(0.66 0.012 78)",
invalid: "oklch(0.66 0.2 24)",
};
const tuskEditorTheme = EditorView.theme(
{
"&": {
color: c.fg,
backgroundColor: c.bg,
},
".cm-content": {
caretColor: c.cursor,
fontFamily: "var(--font-mono)",
padding: "8px 0",
},
".cm-cursor, .cm-dropCursor": { borderLeftColor: c.cursor, borderLeftWidth: "2px" },
"&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":
{ backgroundColor: c.selection },
".cm-activeLine": { backgroundColor: c.activeLine },
".cm-gutters": {
backgroundColor: c.bg,
color: c.gutter,
border: "none",
borderRight: "1px solid oklch(0.34 0.011 75 / 50%)",
},
".cm-activeLineGutter": {
backgroundColor: c.activeGutter,
color: c.gutterActive,
},
".cm-foldGutter .cm-gutterElement": { color: c.faint },
".cm-selectionMatch": { backgroundColor: c.matchBg },
"&.cm-focused .cm-matchingBracket": {
backgroundColor: c.matchBg,
outline: "1px solid oklch(0.808 0.124 82 / 45%)",
borderRadius: "2px",
},
".cm-line": { padding: "0 4px 0 8px" },
".cm-scroller": { fontFamily: "var(--font-mono)" },
".cm-panels": { backgroundColor: c.surface, color: c.fg },
".cm-searchMatch": {
backgroundColor: "oklch(0.78 0.135 65 / 28%)",
outline: "1px solid oklch(0.78 0.135 65 / 50%)",
},
".cm-searchMatch.cm-searchMatch-selected": {
backgroundColor: "oklch(0.808 0.124 82 / 35%)",
},
},
{ dark: true }
);
const tuskHighlightStyle = HighlightStyle.define([
{ tag: [t.keyword, t.operatorKeyword, t.modifier], color: c.keyword, fontWeight: "500" },
{ tag: [t.string, t.special(t.string), t.character], color: c.string },
{ tag: [t.number, t.bool, t.null], color: c.number },
{ tag: [t.function(t.variableName), t.function(t.propertyName)], color: c.func },
{ tag: [t.typeName, t.className, t.namespace], color: c.type },
{ tag: [t.comment, t.lineComment, t.blockComment], color: c.comment, fontStyle: "italic" },
{ tag: [t.operator, t.compareOperator, t.arithmeticOperator, t.logicOperator], color: c.operator },
{ tag: [t.bracket, t.paren, t.squareBracket, t.brace, t.punctuation], color: c.bracket },
{ tag: [t.propertyName, t.attributeName], color: c.fg },
{ tag: [t.variableName, t.name], color: c.fg },
{ tag: [t.definitionKeyword], color: c.keyword, fontWeight: "500" },
{ tag: [t.invalid], color: c.invalid, textDecoration: "underline wavy" },
]);
/** Full Tusk editor theme: base UI styling + SQL syntax highlighting. */
export const tuskEditorExtensions: Extension = [
tuskEditorTheme,
syntaxHighlighting(tuskHighlightStyle),
];

View File

@@ -5,18 +5,18 @@
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
/* ═══════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════
TUSK — "Twilight" Design System TUSK — "Graphite & Honey" Design System
Soft dark with blue undertones and teal accents Warm graphite workstation · ivory text · honey accent
Monospace-first, tuned for long data + query sessions.
═══════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════ */
@theme inline { @theme inline {
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 3px);
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) - 1px);
--radius-lg: var(--radius); --radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px); --radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px); --radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--color-card: var(--card); --color-card: var(--card);
@@ -49,72 +49,87 @@
--color-sidebar-border: var(--sidebar-border); --color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring); --color-sidebar-ring: var(--sidebar-ring);
/* Custom semantic tokens */ /* Brand + semantic status tokens */
--color-tusk-teal: var(--tusk-teal); --color-honey: var(--honey);
--color-tusk-purple: var(--tusk-purple); --color-success: var(--success);
--color-tusk-amber: var(--tusk-amber); --color-warning: var(--warning);
--color-tusk-rose: var(--tusk-rose); --color-info: var(--info);
--color-tusk-surface: var(--tusk-surface); --color-violet: var(--violet);
/* Font families */ /* Database object categories (schema tree, syntax, etc.) */
--font-sans: "Outfit", system-ui, -apple-system, sans-serif; --color-data-table: var(--data-table);
--font-mono: "JetBrains Mono", "Fira Code", "Cascadia Code", ui-monospace, monospace; --color-data-view: var(--data-view);
--color-data-function: var(--data-function);
--color-data-sequence: var(--data-sequence);
/* Font families — monospace everywhere, "like for development" */
--font-sans: "IBM Plex Mono", "JetBrains Mono", ui-monospace, "SF Mono",
"Cascadia Code", monospace;
--font-mono: "IBM Plex Mono", "JetBrains Mono", ui-monospace, "SF Mono",
"Cascadia Code", monospace;
} }
:root { :root {
--radius: 0.5rem; --radius: 0.5rem;
/* Soft twilight palette — comfortable, not eye-straining */ /* ── Warm graphite surfaces (hue ~75, very low chroma) ─────────
--background: oklch(0.2 0.012 250); Layered from deepest (app) to highest (popover). Never pure
--foreground: oklch(0.9 0.005 250); black — warm charcoal reduces halation over long sessions. */
--card: oklch(0.23 0.012 250); --background: oklch(0.176 0.006 75);
--card-foreground: oklch(0.9 0.005 250); --foreground: oklch(0.912 0.013 80);
--popover: oklch(0.25 0.014 250); --card: oklch(0.205 0.007 75);
--popover-foreground: oklch(0.9 0.005 250); --card-foreground: oklch(0.912 0.013 80);
--popover: oklch(0.232 0.008 75);
--popover-foreground: oklch(0.93 0.012 80);
/* Teal primary — slightly softer for the lighter background */ /* ── Honey primary — warm, inviting, easy on the eyes ───────── */
--primary: oklch(0.72 0.14 170); --primary: oklch(0.808 0.124 82);
--primary-foreground: oklch(0.18 0.015 250); --primary-foreground: oklch(0.21 0.03 80);
/* Surfaces — gentle stepping */ /* ── Subtle fills + hover surfaces ──────────────────────────── */
--secondary: oklch(0.27 0.012 250); --secondary: oklch(0.255 0.008 75);
--secondary-foreground: oklch(0.85 0.008 250); --secondary-foreground: oklch(0.88 0.012 80);
--muted: oklch(0.27 0.012 250); --muted: oklch(0.255 0.008 75);
--muted-foreground: oklch(0.62 0.015 250); --muted-foreground: oklch(0.638 0.013 78);
--accent: oklch(0.28 0.014 250); --accent: oklch(0.285 0.01 75);
--accent-foreground: oklch(0.9 0.005 250); --accent-foreground: oklch(0.93 0.012 80);
/* Status */ --destructive: oklch(0.655 0.196 24);
--destructive: oklch(0.65 0.2 15);
/* Borders & inputs — more visible, less transparent */ /* ── Borders + inputs — visible but quiet ───────────────────── */
--border: oklch(0.34 0.015 250 / 70%); --border: oklch(0.34 0.011 75 / 65%);
--input: oklch(0.36 0.015 250 / 60%); --input: oklch(0.36 0.011 75 / 55%);
--ring: oklch(0.72 0.14 170 / 40%); --ring: oklch(0.808 0.124 82 / 45%);
/* Chart palette */ /* ── Charts — honey-led harmonious set ──────────────────────── */
--chart-1: oklch(0.72 0.14 170); --chart-1: oklch(0.808 0.124 82);
--chart-2: oklch(0.68 0.14 200); --chart-2: oklch(0.7 0.12 220);
--chart-3: oklch(0.78 0.14 85); --chart-3: oklch(0.74 0.13 152);
--chart-4: oklch(0.62 0.18 290); --chart-4: oklch(0.7 0.15 305);
--chart-5: oklch(0.68 0.16 30); --chart-5: oklch(0.7 0.16 28);
/* Sidebar <EFBFBD><EFBFBD> same family, slightly offset */ /* ── Sidebar — one notch deeper than the main canvas ────────── */
--sidebar: oklch(0.215 0.012 250); --sidebar: oklch(0.192 0.006 75);
--sidebar-foreground: oklch(0.9 0.005 250); --sidebar-foreground: oklch(0.9 0.012 80);
--sidebar-primary: oklch(0.72 0.14 170); --sidebar-primary: oklch(0.808 0.124 82);
--sidebar-primary-foreground: oklch(0.9 0.005 250); --sidebar-primary-foreground: oklch(0.21 0.03 80);
--sidebar-accent: oklch(0.28 0.014 250); --sidebar-accent: oklch(0.285 0.01 75);
--sidebar-accent-foreground: oklch(0.9 0.005 250); --sidebar-accent-foreground: oklch(0.93 0.012 80);
--sidebar-border: oklch(0.34 0.015 250 / 70%); --sidebar-border: oklch(0.34 0.011 75 / 60%);
--sidebar-ring: oklch(0.72 0.14 170 / 40%); --sidebar-ring: oklch(0.808 0.124 82 / 45%);
/* Tusk semantic tokens */ /* ── Brand + semantic status ────────────────────────────────── */
--tusk-teal: oklch(0.72 0.14 170); --honey: oklch(0.808 0.124 82);
--tusk-purple: oklch(0.62 0.2 290); --success: oklch(0.74 0.14 152);
--tusk-amber: oklch(0.78 0.14 85); --warning: oklch(0.78 0.135 65);
--tusk-rose: oklch(0.65 0.2 15); --info: oklch(0.72 0.12 222);
--tusk-surface: oklch(0.26 0.012 250); --violet: oklch(0.72 0.15 305);
/* ── Database object categories ─────────────────────────────── */
--data-table: oklch(0.72 0.12 222);
--data-view: oklch(0.74 0.13 152);
--data-function: oklch(0.72 0.15 305);
--data-sequence: oklch(0.79 0.13 70);
} }
/* ═══════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════
@@ -129,6 +144,8 @@
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
font-family: var(--font-sans); font-family: var(--font-sans);
font-feature-settings: "ss02" 1, "zero" 1; /* slashed zero, alt glyphs */
letter-spacing: -0.006em;
overflow: hidden; overflow: hidden;
user-select: none; user-select: none;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
@@ -136,39 +153,53 @@
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
/* Monospace for code and data */ /* Tabular figures everywhere data is shown — columns line up */
code, pre, .font-mono, code, pre, .font-mono, table, input, [data-slot="sql-editor"], .cm-editor {
[data-slot="sql-editor"], font-variant-numeric: tabular-nums;
.cm-editor {
font-family: var(--font-mono);
} }
/* Smoother scrollbars */ /* Smoother, quieter scrollbars */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 6px; width: 7px;
height: 6px; height: 7px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: oklch(0.42 0.015 250 / 45%); background: oklch(0.42 0.01 75 / 45%);
border-radius: 3px; border-radius: 4px;
border: 1px solid transparent;
background-clip: padding-box;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: oklch(0.5 0.015 250 / 60%); background: oklch(0.52 0.012 75 / 60%);
background-clip: padding-box;
} }
::-webkit-scrollbar-corner { ::-webkit-scrollbar-corner {
background: transparent; background: transparent;
} }
} }
/* ═══════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════
Noise texture overlay — very subtle depth Wordmark — heavy, tracked-out mono
═══════════════════════════════════════════════════════ */
.tusk-wordmark {
font-weight: 700;
letter-spacing: 0.22em;
text-transform: uppercase;
}
/* Section / eyebrow labels — small uppercase mono */
.tusk-eyebrow {
text-transform: uppercase;
letter-spacing: 0.13em;
font-weight: 600;
}
/* ═══════════════════════════════════════════════════════
Noise texture overlay — very subtle warm depth
═══════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════ */
.tusk-noise::before { .tusk-noise::before {
@@ -177,38 +208,42 @@
inset: 0; inset: 0;
z-index: 9999; z-index: 9999;
pointer-events: none; pointer-events: none;
opacity: 0.018; opacity: 0.022;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
background-repeat: repeat; background-repeat: repeat;
background-size: 256px 256px; background-size: 256px 256px;
} }
/* Faint warm light bloom from the top — atmosphere, not decoration */
.tusk-noise::after {
content: "";
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background:
radial-gradient(120% 80% at 50% -20%, oklch(0.808 0.124 82 / 4.5%), transparent 60%);
}
/* ═══════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════
Glow effects — softer for lighter background Glow effects — honey-led
═══════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════ */
.tusk-glow-teal { .tusk-glow-honey {
box-shadow: 0 0 10px oklch(0.72 0.14 170 / 12%), box-shadow: 0 0 12px oklch(0.808 0.124 82 / 14%),
0 0 3px oklch(0.72 0.14 170 / 8%); 0 0 3px oklch(0.808 0.124 82 / 9%);
}
.tusk-glow-honey-subtle {
box-shadow: 0 0 7px oklch(0.808 0.124 82 / 7%);
} }
.tusk-glow-purple { /* ═══════════════════════════════════════════════════════
box-shadow: 0 0 10px oklch(0.62 0.2 290 / 15%), Active tab indicator — top honey bar
0 0 3px oklch(0.62 0.2 290 / 10%);
}
.tusk-glow-teal-subtle {
box-shadow: 0 0 6px oklch(0.72 0.14 170 / 6%);
}
/* ═══════════════════════════════════════════════<E29590><E29590>═══════
Active tab indicator — top glow bar
═══════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════ */
.tusk-tab-active { .tusk-tab-active {
position: relative; position: relative;
} }
.tusk-tab-active::after { .tusk-tab-active::after {
content: ""; content: "";
position: absolute; position: absolute;
@@ -216,35 +251,34 @@
left: 0; left: 0;
right: 0; right: 0;
height: 2px; height: 2px;
background: linear-gradient(90deg, oklch(0.72 0.14 170), oklch(0.68 0.14 200)); background: linear-gradient(90deg, oklch(0.808 0.124 82), oklch(0.79 0.13 70));
border-radius: 0 0 2px 2px; box-shadow: 0 0 8px oklch(0.808 0.124 82 / 35%);
} }
/* ═══════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════
AI feature branding — purple glow language AI feature branding — violet language
═══════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════ */
.tusk-ai-bar { .tusk-ai-bar {
background: linear-gradient( background: linear-gradient(
135deg, 135deg,
oklch(0.62 0.2 290 / 5%) 0%, oklch(0.72 0.15 305 / 6%) 0%,
oklch(0.62 0.2 290 / 2%) 50%, oklch(0.72 0.15 305 / 2%) 50%,
oklch(0.72 0.14 170 / 3%) 100% oklch(0.808 0.124 82 / 3%) 100%
); );
border-bottom: 1px solid oklch(0.62 0.2 290 / 12%); border-bottom: 1px solid oklch(0.72 0.15 305 / 14%);
} }
.tusk-ai-icon { .tusk-ai-icon {
color: oklch(0.68 0.18 290); color: oklch(0.74 0.15 305);
filter: drop-shadow(0 0 3px oklch(0.62 0.2 290 / 30%)); filter: drop-shadow(0 0 4px oklch(0.72 0.15 305 / 35%));
} }
/* ═══════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════
Transitions — smooth everything Transitions
═══════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════ */
button, a, [role="tab"], [role="menuitem"], [data-slot="button"] { button, a, [role="tab"], [role="menuitem"], [data-slot="button"] {
transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1); transition: all 140ms cubic-bezier(0.4, 0, 0.2, 1);
} }
/* ═══════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════
@@ -255,8 +289,8 @@ button, a, [role="tab"], [role="menuitem"], [data-slot="button"] {
[data-radix-popper-content-wrapper] [role="listbox"], [data-radix-popper-content-wrapper] [role="listbox"],
[data-radix-popper-content-wrapper] [role="menu"], [data-radix-popper-content-wrapper] [role="menu"],
[data-state="open"][data-side] { [data-state="open"][data-side] {
backdrop-filter: blur(16px) saturate(1.2); backdrop-filter: blur(16px) saturate(1.15);
-webkit-backdrop-filter: blur(16px) saturate(1.2); -webkit-backdrop-filter: blur(16px) saturate(1.15);
} }
/* ═══════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════
@@ -266,47 +300,64 @@ button, a, [role="tab"], [role="menuitem"], [data-slot="button"] {
.tusk-sidebar-tab-active { .tusk-sidebar-tab-active {
position: relative; position: relative;
} }
.tusk-sidebar-tab-active::after { .tusk-sidebar-tab-active::after {
content: ""; content: "";
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 25%; left: 22%;
right: 25%; right: 22%;
height: 2px; height: 2px;
background: oklch(0.72 0.14 170); background: oklch(0.808 0.124 82);
border-radius: 2px 2px 0 0; border-radius: 2px 2px 0 0;
box-shadow: 0 0 6px oklch(0.808 0.124 82 / 40%);
} }
/* ═══════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════
Data grid refinements Data grid — the surface that matters most
═══════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════ */
.tusk-grid-header { .tusk-grid-header {
background: oklch(0.23 0.012 250); background: oklch(0.232 0.008 75);
border-bottom: 1px solid oklch(0.34 0.015 250 / 80%); border-bottom: 1px solid oklch(0.4 0.012 75 / 70%);
box-shadow: 0 1px 0 oklch(0 0 0 / 25%);
} }
.tusk-grid-row {
border-bottom: 1px solid oklch(0.3 0.008 75 / 35%);
}
/* Zebra striping for fast horizontal scanning */
.tusk-grid-row-odd {
background: oklch(0.205 0.007 75 / 45%);
}
.tusk-grid-row:hover { .tusk-grid-row:hover {
background: oklch(0.72 0.14 170 / 5%); background: oklch(0.808 0.124 82 / 8%);
box-shadow: inset 2px 0 0 oklch(0.808 0.124 82 / 55%);
} }
.tusk-grid-cell-null { .tusk-grid-cell-null {
color: oklch(0.5 0.015 250); color: oklch(0.52 0.012 75);
font-style: italic; font-style: italic;
} }
.tusk-grid-cell-highlight { .tusk-grid-cell-highlight {
background: oklch(0.78 0.14 85 / 10%); background: oklch(0.808 0.124 82 / 16%);
box-shadow: inset 0 0 0 1px oklch(0.808 0.124 82 / 35%);
} }
/* ═══════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════
Status bar Status bar / toolbar chrome
═══════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════ */
.tusk-status-bar { .tusk-status-bar {
background: oklch(0.215 0.012 250); background: oklch(0.192 0.006 75);
border-top: 1px solid oklch(0.34 0.015 250 / 50%); border-top: 1px solid oklch(0.34 0.011 75 / 55%);
}
.tusk-toolbar {
background: linear-gradient(
180deg,
oklch(0.222 0.008 75),
oklch(0.205 0.007 75)
);
border-bottom: 1px solid oklch(0.34 0.011 75 / 60%);
} }
/* ═══════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════
@@ -316,7 +367,6 @@ button, a, [role="tab"], [role="menuitem"], [data-slot="button"] {
.tusk-conn-strip { .tusk-conn-strip {
position: relative; position: relative;
} }
.tusk-conn-strip::before { .tusk-conn-strip::before {
content: ""; content: "";
position: absolute; position: absolute;
@@ -325,84 +375,57 @@ button, a, [role="tab"], [role="menuitem"], [data-slot="button"] {
bottom: 0; bottom: 0;
width: var(--strip-width, 3px); width: var(--strip-width, 3px);
background: var(--strip-color, transparent); background: var(--strip-color, transparent);
border-radius: 0 2px 2px 0; box-shadow: 0 0 8px var(--strip-color, transparent);
} }
/* ═══════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════
Toolbar Resizable handles
═══════════════════════════════════════════════════════ */
.tusk-toolbar {
background: oklch(0.23 0.012 250);
border-bottom: 1px solid oklch(0.34 0.015 250 / 60%);
}
/* ═══════════════════════════════════════════════════════
Resizable handle
═══════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════ */
[data-panel-group-direction="horizontal"] > [data-resize-handle] { [data-panel-group-direction="horizontal"] > [data-resize-handle] {
width: 1px !important; width: 1px !important;
background: oklch(0.34 0.015 250 / 50%); background: oklch(0.34 0.011 75 / 55%);
transition: background 200ms, width 200ms; transition: background 180ms, width 180ms;
} }
[data-panel-group-direction="horizontal"] > [data-resize-handle]:hover, [data-panel-group-direction="horizontal"] > [data-resize-handle]:hover,
[data-panel-group-direction="horizontal"] > [data-resize-handle][data-resize-handle-active] { [data-panel-group-direction="horizontal"] > [data-resize-handle][data-resize-handle-active] {
width: 3px !important; width: 3px !important;
background: oklch(0.72 0.14 170 / 60%); background: oklch(0.808 0.124 82 / 65%);
} }
[data-panel-group-direction="vertical"] > [data-resize-handle] { [data-panel-group-direction="vertical"] > [data-resize-handle] {
height: 1px !important; height: 1px !important;
background: oklch(0.34 0.015 250 / 50%); background: oklch(0.34 0.011 75 / 55%);
transition: background 200ms, height 200ms; transition: background 180ms, height 180ms;
} }
[data-panel-group-direction="vertical"] > [data-resize-handle]:hover, [data-panel-group-direction="vertical"] > [data-resize-handle]:hover,
[data-panel-group-direction="vertical"] > [data-resize-handle][data-resize-handle-active] { [data-panel-group-direction="vertical"] > [data-resize-handle][data-resize-handle-active] {
height: 3px !important; height: 3px !important;
background: oklch(0.72 0.14 170 / 60%); background: oklch(0.808 0.124 82 / 65%);
} }
/* ═══════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════
CodeMirror theme overrides CodeMirror — autocomplete + gutter polish
(base colors & syntax live in src/lib/editor-theme.ts)
═══════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════ */
.cm-editor { .cm-editor {
font-size: 13px; font-size: 13px;
line-height: 1.6; line-height: 1.65;
} }
.cm-editor .cm-gutters {
background: oklch(0.215 0.012 250);
border-right: 1px solid oklch(0.34 0.015 250 / 50%);
color: oklch(0.48 0.012 250);
}
.cm-editor .cm-activeLineGutter {
background: oklch(0.72 0.14 170 / 8%);
color: oklch(0.65 0.015 250);
}
.cm-editor .cm-activeLine {
background: oklch(0.72 0.14 170 / 4%);
}
.cm-editor .cm-cursor {
border-left-color: oklch(0.72 0.14 170);
}
.cm-editor .cm-selectionBackground {
background: oklch(0.72 0.14 170 / 15%) !important;
}
.cm-editor .cm-tooltip-autocomplete { .cm-editor .cm-tooltip-autocomplete {
backdrop-filter: blur(16px); backdrop-filter: blur(16px);
background: oklch(0.25 0.014 250 / 95%); background: oklch(0.232 0.008 75 / 96%);
border: 1px solid oklch(0.34 0.015 250 / 70%); border: 1px solid oklch(0.34 0.011 75 / 70%);
border-radius: 6px; border-radius: 8px;
box-shadow: 0 8px 32px oklch(0 0 0 / 30%); box-shadow: 0 10px 36px oklch(0 0 0 / 38%);
overflow: hidden;
}
.cm-editor .cm-tooltip-autocomplete > ul > li[aria-selected] {
background: oklch(0.808 0.124 82 / 16%);
color: oklch(0.93 0.012 80);
}
.cm-editor .cm-completionLabel {
font-family: var(--font-mono);
} }
/* ═══════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════
@@ -422,16 +445,13 @@ button, a, [role="tab"], [role="menuitem"], [data-slot="button"] {
from { opacity: 0; transform: translateY(4px); } from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
@keyframes tusk-pulse-glow { @keyframes tusk-pulse-glow {
0%, 100% { opacity: 0.6; } 0%, 100% { opacity: 0.55; }
50% { opacity: 1; } 50% { opacity: 1; }
} }
.tusk-fade-in { .tusk-fade-in {
animation: tusk-fade-in 200ms cubic-bezier(0.4, 0, 0.2, 1); animation: tusk-fade-in 200ms cubic-bezier(0.4, 0, 0.2, 1);
} }
.tusk-pulse-glow { .tusk-pulse-glow {
animation: tusk-pulse-glow 2s ease-in-out infinite; animation: tusk-pulse-glow 2s ease-in-out infinite;
} }
@@ -441,6 +461,6 @@ button, a, [role="tab"], [role="menuitem"], [data-slot="button"] {
═══════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════ */
::selection { ::selection {
background: oklch(0.72 0.14 170 / 25%); background: oklch(0.808 0.124 82 / 26%);
color: oklch(0.95 0.005 250); color: oklch(0.96 0.01 80);
} }