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>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<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>
<body>
<div id="root"></div>

View File

@@ -111,13 +111,13 @@ function UsageBadge({ usage }: { usage: ContextUsage | undefined }) {
let toneClass = "text-muted-foreground/70";
if (ratio >= 0.85) toneClass = "text-destructive";
else if (ratio >= 0.6) toneClass = "text-amber-500";
else if (ratio >= 0.3) toneClass = "text-emerald-500/80";
else if (ratio >= 0.6) toneClass = "text-warning";
else if (ratio >= 0.3) toneClass = "text-success/80";
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";
else if (ratio >= 0.6) fillClass = "bg-amber-500";
else if (ratio >= 0.6) fillClass = "bg-warning";
return (
<Tooltip>

View File

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

View File

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

View File

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

View File

@@ -66,6 +66,15 @@ export function Toolbar() {
"--strip-color": activeColor ?? "transparent",
} 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
variant="ghost"
size="xs"

View File

@@ -131,7 +131,7 @@ export function MemoryPanel() {
)}
{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
</div>
)}

View File

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

View File

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

View File

@@ -190,7 +190,9 @@ export function ResultsTable({
return (
<div
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={{
top: `${virtualRow.start}px`,
height: `${virtualRow.size}px`,
@@ -201,7 +203,7 @@ export function ResultsTable({
return (
<div
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 }}
>
{flexRender(

View File

@@ -148,7 +148,7 @@ function QueryRow({
onDoubleClick={onOpen}
>
<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">
{query.name}
</span>

View File

@@ -281,7 +281,7 @@ function SchemaNode({
)}
</span>
{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" />
)}
@@ -328,10 +328,10 @@ function SchemaNode({
}
const categoryIcons = {
tables: <Table2 className="h-3.5 w-3.5 text-sky-400/80" />,
views: <Eye className="h-3.5 w-3.5 text-emerald-400/80" />,
functions: <FunctionSquare className="h-3.5 w-3.5 text-violet-400/80" />,
sequences: <Hash className="h-3.5 w-3.5 text-amber-400/80" />,
tables: <Table2 className="h-3.5 w-3.5 text-data-table" />,
views: <Eye className="h-3.5 w-3.5 text-data-view" />,
functions: <FunctionSquare className="h-3.5 w-3.5 text-data-function" />,
sequences: <Hash className="h-3.5 w-3.5 text-data-sequence" />,
};
function CategoryNode({

View File

@@ -206,14 +206,14 @@ export function TableDataView({ connectionId, schema, table }: Props) {
}}
>
{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" />
Read-Only
</span>
)}
{!isReadOnly && usesCtid && (
<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."
>
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 *));
/* ═══════════════════════════════════════════════════════
TUSK — "Twilight" Design System
Soft dark with blue undertones and teal accents
TUSK — "Graphite & Honey" Design System
Warm graphite workstation · ivory text · honey accent
Monospace-first, tuned for long data + query sessions.
═══════════════════════════════════════════════════════ */
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 3px);
--radius-md: calc(var(--radius) - 1px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
@@ -49,72 +49,87 @@
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
/* Custom semantic tokens */
--color-tusk-teal: var(--tusk-teal);
--color-tusk-purple: var(--tusk-purple);
--color-tusk-amber: var(--tusk-amber);
--color-tusk-rose: var(--tusk-rose);
--color-tusk-surface: var(--tusk-surface);
/* Brand + semantic status tokens */
--color-honey: var(--honey);
--color-success: var(--success);
--color-warning: var(--warning);
--color-info: var(--info);
--color-violet: var(--violet);
/* Font families */
--font-sans: "Outfit", system-ui, -apple-system, sans-serif;
--font-mono: "JetBrains Mono", "Fira Code", "Cascadia Code", ui-monospace, monospace;
/* Database object categories (schema tree, syntax, etc.) */
--color-data-table: var(--data-table);
--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 {
--radius: 0.5rem;
/* Soft twilight palette — comfortable, not eye-straining */
--background: oklch(0.2 0.012 250);
--foreground: oklch(0.9 0.005 250);
--card: oklch(0.23 0.012 250);
--card-foreground: oklch(0.9 0.005 250);
--popover: oklch(0.25 0.014 250);
--popover-foreground: oklch(0.9 0.005 250);
/* ── Warm graphite surfaces (hue ~75, very low chroma) ─────────
Layered from deepest (app) to highest (popover). Never pure
black — warm charcoal reduces halation over long sessions. */
--background: oklch(0.176 0.006 75);
--foreground: oklch(0.912 0.013 80);
--card: oklch(0.205 0.007 75);
--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 */
--primary: oklch(0.72 0.14 170);
--primary-foreground: oklch(0.18 0.015 250);
/* ── Honey primary — warm, inviting, easy on the eyes ───────── */
--primary: oklch(0.808 0.124 82);
--primary-foreground: oklch(0.21 0.03 80);
/* Surfaces — gentle stepping */
--secondary: oklch(0.27 0.012 250);
--secondary-foreground: oklch(0.85 0.008 250);
--muted: oklch(0.27 0.012 250);
--muted-foreground: oklch(0.62 0.015 250);
--accent: oklch(0.28 0.014 250);
--accent-foreground: oklch(0.9 0.005 250);
/* ── Subtle fills + hover surfaces ──────────────────────────── */
--secondary: oklch(0.255 0.008 75);
--secondary-foreground: oklch(0.88 0.012 80);
--muted: oklch(0.255 0.008 75);
--muted-foreground: oklch(0.638 0.013 78);
--accent: oklch(0.285 0.01 75);
--accent-foreground: oklch(0.93 0.012 80);
/* Status */
--destructive: oklch(0.65 0.2 15);
--destructive: oklch(0.655 0.196 24);
/* Borders & inputs — more visible, less transparent */
--border: oklch(0.34 0.015 250 / 70%);
--input: oklch(0.36 0.015 250 / 60%);
--ring: oklch(0.72 0.14 170 / 40%);
/* ── Borders + inputs — visible but quiet ───────────────────── */
--border: oklch(0.34 0.011 75 / 65%);
--input: oklch(0.36 0.011 75 / 55%);
--ring: oklch(0.808 0.124 82 / 45%);
/* Chart palette */
--chart-1: oklch(0.72 0.14 170);
--chart-2: oklch(0.68 0.14 200);
--chart-3: oklch(0.78 0.14 85);
--chart-4: oklch(0.62 0.18 290);
--chart-5: oklch(0.68 0.16 30);
/* ── Charts — honey-led harmonious set ──────────────────────── */
--chart-1: oklch(0.808 0.124 82);
--chart-2: oklch(0.7 0.12 220);
--chart-3: oklch(0.74 0.13 152);
--chart-4: oklch(0.7 0.15 305);
--chart-5: oklch(0.7 0.16 28);
/* Sidebar <EFBFBD><EFBFBD> same family, slightly offset */
--sidebar: oklch(0.215 0.012 250);
--sidebar-foreground: oklch(0.9 0.005 250);
--sidebar-primary: oklch(0.72 0.14 170);
--sidebar-primary-foreground: oklch(0.9 0.005 250);
--sidebar-accent: oklch(0.28 0.014 250);
--sidebar-accent-foreground: oklch(0.9 0.005 250);
--sidebar-border: oklch(0.34 0.015 250 / 70%);
--sidebar-ring: oklch(0.72 0.14 170 / 40%);
/* ── Sidebar — one notch deeper than the main canvas ────────── */
--sidebar: oklch(0.192 0.006 75);
--sidebar-foreground: oklch(0.9 0.012 80);
--sidebar-primary: oklch(0.808 0.124 82);
--sidebar-primary-foreground: oklch(0.21 0.03 80);
--sidebar-accent: oklch(0.285 0.01 75);
--sidebar-accent-foreground: oklch(0.93 0.012 80);
--sidebar-border: oklch(0.34 0.011 75 / 60%);
--sidebar-ring: oklch(0.808 0.124 82 / 45%);
/* Tusk semantic tokens */
--tusk-teal: oklch(0.72 0.14 170);
--tusk-purple: oklch(0.62 0.2 290);
--tusk-amber: oklch(0.78 0.14 85);
--tusk-rose: oklch(0.65 0.2 15);
--tusk-surface: oklch(0.26 0.012 250);
/* ── Brand + semantic status ────────────────────────────────── */
--honey: oklch(0.808 0.124 82);
--success: oklch(0.74 0.14 152);
--warning: oklch(0.78 0.135 65);
--info: oklch(0.72 0.12 222);
--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 {
@apply bg-background text-foreground;
font-family: var(--font-sans);
font-feature-settings: "ss02" 1, "zero" 1; /* slashed zero, alt glyphs */
letter-spacing: -0.006em;
overflow: hidden;
user-select: none;
-webkit-font-smoothing: antialiased;
@@ -136,39 +153,53 @@
text-rendering: optimizeLegibility;
}
/* Monospace for code and data */
code, pre, .font-mono,
[data-slot="sql-editor"],
.cm-editor {
font-family: var(--font-mono);
/* Tabular figures everywhere data is shown — columns line up */
code, pre, .font-mono, table, input, [data-slot="sql-editor"], .cm-editor {
font-variant-numeric: tabular-nums;
}
/* Smoother scrollbars */
/* Smoother, quieter scrollbars */
::-webkit-scrollbar {
width: 6px;
height: 6px;
width: 7px;
height: 7px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: oklch(0.42 0.015 250 / 45%);
border-radius: 3px;
background: oklch(0.42 0.01 75 / 45%);
border-radius: 4px;
border: 1px solid transparent;
background-clip: padding-box;
}
::-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 {
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 {
@@ -177,38 +208,42 @@
inset: 0;
z-index: 9999;
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-repeat: repeat;
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 {
box-shadow: 0 0 10px oklch(0.72 0.14 170 / 12%),
0 0 3px oklch(0.72 0.14 170 / 8%);
.tusk-glow-honey {
box-shadow: 0 0 12px oklch(0.808 0.124 82 / 14%),
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%),
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
/* ═══════════════════════════════════════════════════════
Active tab indicator — top honey bar
═══════════════════════════════════════════════════════ */
.tusk-tab-active {
position: relative;
}
.tusk-tab-active::after {
content: "";
position: absolute;
@@ -216,35 +251,34 @@
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, oklch(0.72 0.14 170), oklch(0.68 0.14 200));
border-radius: 0 0 2px 2px;
background: linear-gradient(90deg, oklch(0.808 0.124 82), oklch(0.79 0.13 70));
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 {
background: linear-gradient(
135deg,
oklch(0.62 0.2 290 / 5%) 0%,
oklch(0.62 0.2 290 / 2%) 50%,
oklch(0.72 0.14 170 / 3%) 100%
oklch(0.72 0.15 305 / 6%) 0%,
oklch(0.72 0.15 305 / 2%) 50%,
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 {
color: oklch(0.68 0.18 290);
filter: drop-shadow(0 0 3px oklch(0.62 0.2 290 / 30%));
color: oklch(0.74 0.15 305);
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"] {
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="menu"],
[data-state="open"][data-side] {
backdrop-filter: blur(16px) saturate(1.2);
-webkit-backdrop-filter: blur(16px) saturate(1.2);
backdrop-filter: blur(16px) saturate(1.15);
-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 {
position: relative;
}
.tusk-sidebar-tab-active::after {
content: "";
position: absolute;
bottom: 0;
left: 25%;
right: 25%;
left: 22%;
right: 22%;
height: 2px;
background: oklch(0.72 0.14 170);
background: oklch(0.808 0.124 82);
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 {
background: oklch(0.23 0.012 250);
border-bottom: 1px solid oklch(0.34 0.015 250 / 80%);
background: oklch(0.232 0.008 75);
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 {
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 {
color: oklch(0.5 0.015 250);
color: oklch(0.52 0.012 75);
font-style: italic;
}
.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 {
background: oklch(0.215 0.012 250);
border-top: 1px solid oklch(0.34 0.015 250 / 50%);
background: oklch(0.192 0.006 75);
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 {
position: relative;
}
.tusk-conn-strip::before {
content: "";
position: absolute;
@@ -325,84 +375,57 @@ button, a, [role="tab"], [role="menuitem"], [data-slot="button"] {
bottom: 0;
width: var(--strip-width, 3px);
background: var(--strip-color, transparent);
border-radius: 0 2px 2px 0;
box-shadow: 0 0 8px var(--strip-color, transparent);
}
/* ═══════════════════════════════════════════════════════
Toolbar
═══════════════════════════════════════════════════════ */
.tusk-toolbar {
background: oklch(0.23 0.012 250);
border-bottom: 1px solid oklch(0.34 0.015 250 / 60%);
}
/* ═══════════════════════════════════════════════════════
Resizable handle
Resizable handles
═══════════════════════════════════════════════════════ */
[data-panel-group-direction="horizontal"] > [data-resize-handle] {
width: 1px !important;
background: oklch(0.34 0.015 250 / 50%);
transition: background 200ms, width 200ms;
background: oklch(0.34 0.011 75 / 55%);
transition: background 180ms, width 180ms;
}
[data-panel-group-direction="horizontal"] > [data-resize-handle]:hover,
[data-panel-group-direction="horizontal"] > [data-resize-handle][data-resize-handle-active] {
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] {
height: 1px !important;
background: oklch(0.34 0.015 250 / 50%);
transition: background 200ms, height 200ms;
background: oklch(0.34 0.011 75 / 55%);
transition: background 180ms, height 180ms;
}
[data-panel-group-direction="vertical"] > [data-resize-handle]:hover,
[data-panel-group-direction="vertical"] > [data-resize-handle][data-resize-handle-active] {
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 {
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 {
backdrop-filter: blur(16px);
background: oklch(0.25 0.014 250 / 95%);
border: 1px solid oklch(0.34 0.015 250 / 70%);
border-radius: 6px;
box-shadow: 0 8px 32px oklch(0 0 0 / 30%);
background: oklch(0.232 0.008 75 / 96%);
border: 1px solid oklch(0.34 0.011 75 / 70%);
border-radius: 8px;
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); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes tusk-pulse-glow {
0%, 100% { opacity: 0.6; }
0%, 100% { opacity: 0.55; }
50% { opacity: 1; }
}
.tusk-fade-in {
animation: tusk-fade-in 200ms cubic-bezier(0.4, 0, 0.2, 1);
}
.tusk-pulse-glow {
animation: tusk-pulse-glow 2s ease-in-out infinite;
}
@@ -441,6 +461,6 @@ button, a, [role="tab"], [role="menuitem"], [data-slot="button"] {
═══════════════════════════════════════════════════════ */
::selection {
background: oklch(0.72 0.14 170 / 25%);
color: oklch(0.95 0.005 250);
background: oklch(0.808 0.124 82 / 26%);
color: oklch(0.96 0.01 80);
}