Files
learnharness/app/globals.css
T
amosblomqvist 81be55f35c Empty-graph chrome: Import Notes stays, search + settings wait for nodes
Owner ruling on the empty graph (honesty principle — dead controls are
lies): the top-chrome actions row now gates per control.

- Import Notes renders ALWAYS (graph enabled) — the one action that
  works on an empty graph, the entry point itself. It used to hide with
  everything else, leaving the empty state a dead end with no entry.
- Search + settings render only when the snapshot has nodes, and their
  appear-once mount carries a deliberate 200ms fade (graph-chrome-fade-in,
  globals.css — the settings-pop register, reduced-motion appears
  instantly). Computed animationName verified live.
- The row's body is now the exported pure GraphChromeRow (the
  DeleteNodeBody pattern) — the no-match note's truth (visibleIds +
  snapshot) stays upstream, computed by the wrapper.
- The empty-state placeholder copy is LEFT AS-IS (SCOPE rule: it already
  has copy — icon, title, and the session-mapping paragraph).

Pins (tests/render-graph-chrome.test.tsx): empty → Import Notes present,
search/settings absent, no fade carriers; populated → all three, exactly
two fade carriers; the no-match note follows its upstream truth. SSR of
GraphChromeRow both ways.

Live-verified on :3001 with scratch data: empty graph → Import Notes
only + placeholder intact; a real import through the button (file input
via DataTransfer) → nodes land, search + settings fade in (computed
chrome-fade-in on both carriers); wipe → reload → back to Import Notes
only (the reverse path). Scratch graph restored byte-identical.
932/932, tsc/lint/build green.
2026-09-25 01:26:42 +03:00

503 lines
18 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
/*
* THE SKIN — wave 5. Design tokens, the single place to restyle the app.
*
* Mathematical soundness, per the owner's brief:
* - Tailwind's built-in 0.25rem (4px) spacing scale IS the rhythm — every
* padding/margin/gap in the app uses whole units of a 4px base.
* - The type scale below is a declared modular ladder (each --text-* token
* carries its own line-height); components never invent pixel sizes.
* - Radii ladder is 4 / 8 / 12 / 16px multiples of the 4px base.
* - Emphasis colors live in tokens (--lh-ink-strong / --lh-ink-italic) so a
* future settings UI reassigns them (configurable per the owner).
*/
/* Palette (owner's hexes): main #141414, raised #1C1C1C, edges #434343.
Mono chrome = JetBrains Mono; prose = Source Sans 3; titles = CMU Serif. */
:root {
color-scheme: dark;
--radius: 8px;
--background: #141414;
--foreground: #e6e6e6;
--card: #1c1c1c;
--card-foreground: #e6e6e6;
--popover: #1c1c1c;
--popover-foreground: #e6e6e6;
--primary: #e6e6e6;
--primary-foreground: #141414;
--secondary: #1c1c1c;
--secondary-foreground: #e6e6e6;
--muted: #1c1c1c;
--muted-foreground: #8d8d8d;
--accent: #2a2a2a;
--accent-foreground: #e6e6e6;
--destructive: oklch(0.704 0.191 22.216);
--border: #434343;
--input: #434343;
--ring: #8d8d8d;
/* Emphasis coloring in teaching prose — ready for the settings UI. */
--lh-ink-strong: #ee928c;
--lh-ink-italic: #6fa9d6;
/* THE READING SIZE = the ladder's 16px rung (--text-lg). Can't be
var()-linked: @theme inline inlines literals into utilities without
emitting runtime vars — so this is the one documented duplication:
change one, change both. (owner, typography round 4: snapped from
the reskin's hand-tuned 16.5 to the ladder's 16.) */
--lh-prose-size: 16px;
--lh-prose-leading: 1.5;
/* Markdown heading weight (appearance settings; 900 = the Black cut). */
--lh-heading-weight: 900;
}
/* The app is always dark (<html class="dark">); mirror the same values so
the block stays coherent under Tailwind's variant. */
.dark {
--background: #141414;
--foreground: #e6e6e6;
--card: #1c1c1c;
--card-foreground: #e6e6e6;
--popover: #1c1c1c;
--popover-foreground: #e6e6e6;
--primary: #e6e6e6;
--primary-foreground: #141414;
--secondary: #1c1c1c;
--secondary-foreground: #e6e6e6;
--muted: #1c1c1c;
--muted-foreground: #8d8d8d;
--accent: #2a2a2a;
--accent-foreground: #e6e6e6;
--destructive: oklch(0.704 0.191 22.216);
--border: #434343;
--input: #434343;
--ring: #8d8d8d;
--lh-ink-strong: #ee928c;
--lh-ink-italic: #6fa9d6;
/* THE READING SIZE = the ladder's 16px rung (--text-lg). Can't be
var()-linked: @theme inline inlines literals into utilities without
emitting runtime vars — so this is the one documented duplication:
change one, change both. (owner, typography round 4: snapped from
the reskin's hand-tuned 16.5 to the ladder's 16.) */
--lh-prose-size: 16px;
--lh-prose-leading: 1.5;
/* Markdown heading weight (appearance settings; 900 = the Black cut). */
--lh-heading-weight: 900;
}
/* CMU Serif bold (titles) — bundled local file, no CDN dependency. */
@font-face {
font-family: "lh-cmu";
src: url("/fonts/cmunbx.ttf") format("truetype");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@theme inline {
/* Fonts: chrome = JetBrains Mono, prose = Source Sans 3, titles = CMU
Serif bold. The JetBrains/SourceSans variables attach in app/layout.tsx.
(owner, typography round) The Tailwind defaults are ALIASED to the app
fonts: `font-mono`/`font-sans` utilities would otherwise fall to the
SYSTEM stacks — a fourth font the app never chose — and streamdown's
code-block chrome + mermaid-error + demo-card all use them. */
--font-ui: var(--font-jetbrains), ui-monospace, SFMono-Regular, monospace;
--font-prose: var(--font-sans3), ui-sans-serif, system-ui, sans-serif;
--font-mono: var(--font-jetbrains), ui-monospace, SFMono-Regular, monospace;
--font-sans: var(--font-sans3), ui-sans-serif, system-ui, sans-serif;
/* The modular type ladder — each size carries its own line-height.
Component sizes must pull from this ladder. (owner, typography
round 4: the 10px rung is merged into 11 — one pixel apart was a
distinction with no visible difference, and 10px sits at the mono
legibility floor; quiet labels get their quiet from color +
tracking, not a notch of size. Reading prose = the 16px rung via
--lh-prose-size.) */
/* --font-title lives in the NON-inline @theme below: inline tokens get
their literal values baked into the utilities (.font-title{font-family:
lh-cmu,serif}), which made the appearance "session names in serif"
toggle write a runtime --font-title no consumer ever read (owner, fix
wave: toggle looked dead). Non-inline emits :root { --font-title: … }
and the utility reads var(--font-title) — appearance overrides win. */
--text-xs: 11px;
--text-xs--line-height: 1.4;
--text-sm: 12.5px;
--text-sm--line-height: 1.4;
--text-base: 14px;
--text-base--line-height: 1.45;
--text-lg: 16px;
--text-lg--line-height: 1.45;
--text-xl: 20px;
--text-xl--line-height: 1.4;
--text-2xl: 25px;
--text-2xl--line-height: 1.3;
--text-3xl: 32px;
--text-3xl--line-height: 1.25;
/* Radii ladder — 4/8/12/16 multiples of the 4px base. */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius: 8px;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
* {
border-color: var(--color-border);
}
body {
background: var(--color-background);
color: var(--color-foreground);
font-family: var(--font-ui);
}
/* Teaching prose (the markdown renderer's container gets .lh-prose):
Source Sans 3 with the size/leading tokens; emphasis color bound to
the settings-ready tokens. */
.lh-prose {
font-family: var(--font-prose);
font-size: var(--lh-prose-size);
line-height: var(--lh-prose-leading);
}
/* Session-name weight (owner, fix wave round 3): with the serif toggle
OFF, --font-title resolves to the prose font — Source Sans 3 is loaded
as a VARIABLE font, and without an explicit weight the titles render at
400, a whisper next to CMU's bold-only face. Request 900: the variable
font takes it, and CMU (a single 700 face) snaps to its nearest
available weight — the same rendering it already had. */
.font-title {
font-weight: 900;
}
.lh-prose :where(strong, b) {
color: var(--lh-ink-strong);
font-weight: 700;
}
.lh-prose :where(em, i) {
color: var(--lh-ink-italic);
}
/* Markdown lists indent from the prose edge (owner ask, 2026-09-24):
streamdown ships list-inside with no container padding (top-level
markers sit ON the text edge — nested lists get their pl-6 only via
[li_&]). 24px — the same measure as that nested-case pl-6 — pulls
the whole list right as a block. Unlayered CSS beats the utility
layer; :where keeps specificity zero. */
.lh-prose :where(ul, ol) {
padding-left: 1.5rem;
}
/* Markdown headings render at the font's BLACK weight (owner ask,
2026-09-19) — 900, not streamdown's font-semibold 600. The variable
Source Sans 3 axis spans 200–900 (next/font loads the full range), so
900 draws the real Black cut. Unlayered CSS beats the utility layer;
:where keeps specificity zero. Chrome titles (CMU Serif) don't live
under .lh-prose and stay untouched. */
.lh-prose :where(h1, h2, h3, h4, h5, h6) {
font-weight: var(--lh-heading-weight, 900);
}
/* lh-input (owner, typography round 2): lh-prose's 1.5 READING leading
makes typing carets tower over the glyphs — the caret fills the line
box (24px at 16px text, ~2× the glyph height). Inputs are not
paragraphs: a tighter 1.25 leading brings the caret to the font-metric
floor (~20px, as close to the glyphs as Chromium will draw it) while
wrapped lines in growing textareas still read fine. Rendered prose keeps
1.5 — the two share family and size, they differ only in leading. */
.lh-input {
font-family: var(--font-prose);
font-size: var(--lh-prose-size);
line-height: 1.25;
}
/* Thin dark native scrollbar (app-wide): dark thumb, transparent track. */
* {
scrollbar-width: thin;
scrollbar-color: var(--color-border) transparent;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 8px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-muted-foreground);
}
/* Dock variant swap — quick fade/rise on content change; the shell's HEIGHT
morph is separate (WAAPI in DockOverlay). Re-applied via class toggling, no
remount, so collapsed-question state survives. */
@keyframes dock-variant-in {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.dock-variant {
animation: dock-variant-in 160ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
/* ============================================================
* W3 — presentation fixes (appended 2026-08-22)
* ============================================================ */
/* Streamdown's internal UI chrome (code blocks, tables, mermaid containers,
copy/download buttons) is styled with Tailwind utilities living in its dist
* bundle. Tailwind v4 never scans node_modules, so without this line those
* classes are dead — code blocks and mermaid containers rendered unstyled.
* (Streamdown README, "Tailwind CSS setup".) */
@source "../node_modules/streamdown/dist/*.js";
/* Token the streamdown utilities reference (code-block/mermaid chrome uses
* bg-sidebar / border-sidebar): map it onto the app's card surface. */
@theme inline {
--color-sidebar: var(--card);
}
/* Mermaid diagrams: keep wide charts inside the content column. Mermaid
* inlines a px max-width on the svg, so !important is required to win. */
[data-streamdown="mermaid"] svg {
max-width: 100% !important;
height: auto !important;
}
/* Display math: long equations scroll instead of overflowing the column. */
.katex-display {
overflow-x: auto;
overflow-y: hidden;
}
/* ============================================================
* Chrome melt (owner wave, 2026-09-19) — tables and code blocks stop
* being boxes: blocks read as part of the message. These rules override
* streamdown's Tailwind utilities (utilities live in a cascade LAYER;
* unlayered author CSS wins regardless of order).
* ============================================================ */
/* Code blocks: no header bar — one quiet rounded tint so code reads as a
unit. The tint is DARKER than the page (muted #1c1c1c was LIGHTER than the
background #141414 and read as raised — owner note, chrome-melt tweaks
2026-09-19) plus a subtle hairline outline. (md-blocks renders its own
hover copy corner.) */
[data-streamdown="code-block"] {
border: 1px solid var(--border);
background: color-mix(in oklab, var(--background) 86%, black);
border-radius: 0.75rem;
padding: 0.375rem;
}
[data-streamdown="code-block-header"] {
display: none;
}
[data-streamdown="code-block-body"] {
border: 0;
background: transparent;
padding: 0.75rem 1rem;
}
/* Tables: hairline row rules + header underline, and a full outer border
— actual RECTANGLES outlined on all four sides (owner note, chrome-melt
tweaks 2026-09-19: the melt left the shape missing its left/right/top
edges). border-collapse merges the outer border with the row rules, so
no edge doubles. (md-blocks supplies the wrapper + the copy corner.) */
[data-streamdown="table"] {
border: 1px solid var(--border);
/* Tables run at the SESSION prose size (owner note, 2026-09-19: they
visibly lagged the 16px chat text at streamdown's 12.5px text-sm).
Unlayered CSS beats the cells' own utility-layer text-sm, and the
:where keeps specificity zero — the override wins on layering alone. */
font-size: var(--lh-prose-size);
}
[data-streamdown="table"] :where(th, td) {
font-size: inherit;
}
/* Column separators: a hairline left rule on every cell but the first in
its row (owner ask, 2026-09-19) — border-collapse merges it with the row
rules and the outer border, no doubled lines anywhere. */
[data-streamdown="table"] :where(th, td):not(:first-child) {
border-left: 1px solid var(--border);
}
[data-streamdown="table-header"] {
background: transparent;
border-bottom: 1px solid var(--border);
}
[data-streamdown="table-body"] [data-streamdown="table-row"] {
border-bottom: 1px solid var(--border);
}
/* The mermaid focus overlay's chart wrapper: the svg fills the wrapper
(which is sized to the chart's viewBox — pan-zoom.ts math centers on it),
overriding mermaid's inline max-width cap. Later + same-specificity
than the column rule above, so the !important tie breaks our way; the
overlay's svg never sits under [data-streamdown="mermaid"] anyway. */
[data-lh-overlay-chart] svg {
max-width: none !important;
width: 100% !important;
height: auto !important;
}
/* ============================================================
* Session status — the braille spinner (the one working indicator)
* ============================================================ */
/* (owner, minimal-color round) Phase color-coding is RETIRED: phases only
advance (every session ends in Teaching), so phase-as-signal was
decoration; the --phase-* tokens and every phase-dot state are gone.
What state deserved — "a turn is in flight" — is carried by MOTION
alone: the terminal braille spinner (components/braille-spinner.tsx),
monochrome, existing only while true. */
/* One keyframes, all ten frames: each frame <g> holds opacity 1 for its
10% slice of the 800ms cycle and 0 elsewhere; frames stagger via
positive animation-delay (frame k starts at k×80ms). Static contexts
(SSR, reduced motion) show frame 0 — see the base rules below. */
@keyframes braille-frame {
0%,
10% {
opacity: 1;
}
10.001%,
100% {
opacity: 0;
}
}
.braille-frame {
animation: braille-frame 800ms linear infinite;
opacity: 0;
}
/* Static fallback: frame 0 (⠋) is the glyph a non-animated context shows. */
.braille-frame[data-frame="0"] {
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
.braille-frame {
animation: none;
}
}
/* Settings dialog pop (settings-app wave, 2026-09-20): a short fade +
2% settle-scale — understated by design, the app's motion taste
(200ms sidebar, 150ms here); honors reduced-motion by appearing. */
@keyframes settings-pop {
from {
opacity: 0;
scale: 0.98;
}
to {
opacity: 1;
scale: 1;
}
}
.settings-dialog-pop {
animation: settings-pop 150ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
.settings-dialog-pop {
animation: none;
}
}
/* Graph chrome fade-in (empty-graph chrome wave, owner 2026-09-25): the
search + settings controls appear ONCE, as the first nodes land —
Import Notes always renders (the empty graph's entry point), the other
two mount only when the snapshot has nodes. Same understated register
as settings-pop (a plain fade); reduced-motion appears instantly. */
@keyframes chrome-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.graph-chrome-fade-in {
animation: chrome-fade-in 200ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
.graph-chrome-fade-in {
animation: none;
}
}
/* ─────────────────────────────────────────────────────────────────────────
Electron merged chrome (owner, 2026-09-17). The system title bar is
hidden (titleBarStyle: "hiddenInset" in electron/main.cjs); the app's
own 60px header bands are the drag surface. Everything here is inert
in the plain browser — it keys off <html class="lh-electron">, which
SidebarProvider sets after mount when it detects the Electron shell.
- .lh-drag window-drag region — on a dedicated mask div in
home.tsx placed FIRST in the page DOM (the mask
resolves by DOM order, not z-index: no-drag subtracts
only regions added earlier in the tree)
- .lh-no-drag carve-out for interactive elements inside a drag band
- .lh-lights-gutter the sidebar LH band's traffic-light gutter: lights
end at x≈68, the wordmark starts at 80px (5rem)
- .lh-closed-toggle same gutter for the sidebar toggle when the sidebar
is closed — the lights are static, the toggle moves
───────────────────────────────────────────────────────────────────────── */
.lh-electron .lh-drag {
-webkit-app-region: drag;
}
.lh-electron .lh-no-drag {
-webkit-app-region: no-drag;
}
/* 92px = measured lights' right edge (~75pt) + the lights' own 16px
inset from the window edge — comfortable, symmetric air (owner). */
.lh-electron .lh-lights-gutter {
padding-left: 92px;
}
.lh-electron .lh-closed-toggle {
margin-left: 92px;
}
/* --font-title deliberately NOT @theme inline (see comment above): the
session-name-serif toggle overrides this var at runtime, so the
.font-title utility must read it through var(), not carry a baked copy. */
@theme {
--font-title: "lh-cmu", serif;
}