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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
}`}
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
102
src/lib/editor-theme.ts
Normal 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),
|
||||
];
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user