Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

816 lines
20 KiB
CSS

@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
/* ────────────────────────────────────────────────────────────────────────
Theme palettes — four families:
• Default (.theme-snow) → pure-white neutral greys, blue accent
• Cream (:root) and Dark (.dark) → warm family, terracotta accent
• Glass (.theme-glass) → translucent purple on near-black
Tokens that every theme must publish:
--background, --foreground, --card / --popover (+ their -foreground),
--primary (+ -foreground), --secondary, --muted (+ -foreground),
--accent (+ -foreground), --destructive (+ -foreground),
--border, --input, --ring,
--overlay → scrim color for modal backdrops / picker shells
(replaces hardcoded bg-black/40 in components)
──────────────────────────────────────────────────────────────────────── */
/* Cream — near-white warm canvas. Reads as off-white at first glance;
the warm tint only surfaces on filled blocks (selected nav, panels)
and against the terracotta primary. Inspired by Claude's chrome:
keep bg essentially white, save the visible warmth for accents. */
:root {
color-scheme: light;
--background: #fdfcf9;
--foreground: #1c1816;
--card: #ffffff;
--card-foreground: #1c1816;
--popover: #ffffff;
--popover-foreground: #1c1816;
--primary: #b0501e;
--primary-foreground: #ffffff;
--secondary: #f5f2ea;
--secondary-foreground: #1c1816;
--muted: #f1ede2;
--muted-foreground: #6d645a;
--accent: #ece7d8;
--accent-foreground: #1c1816;
--destructive: #c53a2c;
--destructive-foreground: #ffffff;
--border: #e6decc;
--input: #e6decc;
--ring: #b0501e;
--overlay: rgba(28, 24, 22, 0.42);
}
/* Dark — warm near-black, same terracotta family as Cream. */
.dark {
color-scheme: dark;
--background: #1a1918;
--foreground: #e8e4de;
--card: #242220;
--card-foreground: #e8e4de;
--popover: #242220;
--popover-foreground: #e8e4de;
--primary: #d4734b;
--primary-foreground: #1a1918;
--secondary: #2a2725;
--secondary-foreground: #e8e4de;
--muted: #2a2725;
--muted-foreground: #9b9590;
--accent: #302d2a;
--accent-foreground: #e8e4de;
--destructive: #d44a3c;
--destructive-foreground: #ffffff;
--border: #3a3634;
--input: #3a3634;
--ring: #d4734b;
--overlay: rgba(0, 0, 0, 0.6);
}
/* Default — pure-white neutral canvas (theme id "snow" kept for stored
preferences). No tint anywhere: flat #fff bg, plain grey surfaces and
borders. The only chroma is the blue primary on interactive accents,
in the spirit of Codex/ChatGPT chrome. */
.theme-snow {
color-scheme: light;
--background: #ffffff;
--foreground: #0d0d0d;
--card: #ffffff;
--card-foreground: #0d0d0d;
--popover: #ffffff;
--popover-foreground: #0d0d0d;
--primary: #2563eb;
--primary-foreground: #ffffff;
--secondary: #f7f7f7;
--secondary-foreground: #0d0d0d;
--muted: #f2f2f2;
--muted-foreground: #6e6e6e;
--accent: #ececec;
--accent-foreground: #0d0d0d;
--destructive: #d92d20;
--destructive-foreground: #ffffff;
--border: #e5e5e5;
--input: #e5e5e5;
--ring: #2563eb;
--overlay: rgba(0, 0, 0, 0.4);
}
/* Glass — translucent purple panels on near-black. Slightly brighter
primary so the accent reads against the radial gradient. */
.theme-glass {
color-scheme: dark;
--background: #0e0d1a;
--foreground: #ffffff;
--card: rgba(255, 255, 255, 0.06);
--card-foreground: #ffffff;
--popover: rgba(24, 22, 36, 0.92);
--popover-foreground: #ffffff;
--primary: #a855f7;
--primary-foreground: #ffffff;
--secondary: rgba(255, 255, 255, 0.04);
--secondary-foreground: #eeeeee;
--muted: rgba(255, 255, 255, 0.06);
--muted-foreground: #b5b5c7;
--accent: rgba(255, 255, 255, 0.12);
--accent-foreground: #ffffff;
--border: rgba(255, 255, 255, 0.12);
--input: rgba(255, 255, 255, 0.06);
--ring: #a855f7;
--overlay: rgba(0, 0, 0, 0.55);
}
.theme-glass body {
background: radial-gradient(circle at 50% 50%, #1a1a2e 0%, #0d0d1a 100%);
background-attachment: fixed;
}
.theme-glass .surface-card {
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}
@media (min-width: 768px) {
.theme-glass .surface-card {
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
}
.theme-glass .btn-primary {
background: linear-gradient(45deg, #9d50bb, #6e48aa);
box-shadow: 0 0 15px rgba(157, 80, 187, 0.4);
border: none;
transition: all 0.3s ease;
}
.theme-glass .btn-primary:hover {
box-shadow: 0 0 25px rgba(157, 80, 187, 0.6);
transform: translateY(-1px);
}
* {
@apply border-[color:var(--border)];
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
html,
body {
height: 100%;
overflow: hidden;
}
html {
-webkit-font-smoothing: antialiased;
}
html[data-scroll-behavior="smooth"] {
scroll-behavior: smooth;
}
body {
@apply antialiased;
background: var(--background);
color: var(--foreground);
}
button,
input,
select,
textarea {
font: inherit;
}
summary::-webkit-details-marker {
display: none;
}
}
@layer components {
.surface-card {
@apply rounded-2xl border bg-[var(--card)] shadow-sm;
}
.muted-chip {
@apply rounded-full bg-[var(--muted)] px-3 py-1 text-xs text-[var(--muted-foreground)];
}
}
@layer utilities {
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
}
/* ─── Scrollbar ─── */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--muted-foreground);
}
/* ─── Streaming surfaces ───
Every scroll container that wraps an LLM-streaming surface (chat,
trace card, quiz follow-up, book chat, …) opts into this contract
by carrying ``data-chat-scroll-root="true"``.
``overflow-anchor: none``
Disables the browser's automatic scroll anchoring. Anchoring tries
to keep an in-viewport element fixed in screen space when content
above it grows; for a chat surface this fights ``useChatAutoScroll``
every time a code block / KaTeX block / dynamic viewer mounts and
reflows the message list. Without this rule the page visibly jitters
during fast streams.
``scroll-behavior: auto``
The hook intentionally never animates ``scrollTop``. If a global
``scroll-behavior: smooth`` ever lands (theme, base reset, browser
experiment), a smooth animation racing against the next-frame pin
reintroduces the exact stutter this rule is here to prevent. */
[data-chat-scroll-root="true"] {
overflow-anchor: none;
scroll-behavior: auto;
}
/* ─── Prose: base ─── */
.prose {
@apply font-serif text-[16px];
}
.prose pre {
@apply rounded-lg p-4 overflow-x-auto my-4;
background: #292524;
color: #d6d3d1;
}
.prose code:not(.md-code-block__code):not(.md-inline-code) {
@apply px-1.5 py-0.5 rounded text-sm font-mono;
background: var(--muted);
color: var(--foreground);
}
.prose pre code {
@apply bg-transparent text-inherit p-0;
}
.md-renderer .md-inline-code {
@apply px-1.5 py-0.5 rounded text-sm font-mono;
background: var(--muted);
color: var(--foreground);
}
.md-renderer .md-code-block {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
/* Force light text on dark code blocks regardless of theme. The wrapper
.md-code-block always uses a fixed dark background (#1f2937 / #292524),
so we need to override .prose pre (color: #D6D3D1) and any inherited
--foreground from the prose wrapper that would otherwise render as
near-black in the light theme. Specificity (0,2,1) beats .prose pre. */
.md-renderer .md-code-block,
.md-renderer .md-code-block pre,
.md-renderer .md-code-block code {
color: #e5e7eb;
}
.md-renderer .md-code-block pre,
.md-renderer .md-code-block code {
background: transparent !important;
}
.md-renderer .md-code-block pre {
@apply overflow-x-auto;
margin: 0 !important;
border-radius: 0 !important;
padding: 1rem !important;
}
.md-renderer .md-code-block__code {
padding: 0 !important;
border-radius: 0 !important;
font-size: 0.875rem !important;
line-height: 1.7 !important;
color: inherit !important;
}
.md-renderer .md-code-block__code span {
background: transparent !important;
border: 0 !important;
box-shadow: none !important;
}
/* ─── Headings ─── */
.prose h1,
.prose h2,
.prose h3,
.prose h4,
.prose h5,
.prose h6 {
@apply font-sans;
}
.prose h1 {
@apply text-2xl font-bold tracking-tight mt-8 mb-4;
color: var(--foreground);
}
.prose h2 {
@apply text-xl font-semibold tracking-tight mt-7 mb-3;
color: var(--foreground);
}
.prose h3 {
@apply text-lg font-semibold tracking-tight mt-6 mb-2.5;
color: var(--foreground);
}
.prose h4 {
@apply text-base font-semibold tracking-tight mt-5 mb-2;
color: var(--foreground);
}
.prose h5 {
@apply text-sm font-semibold tracking-tight mt-4 mb-2;
color: var(--foreground);
}
.prose h6 {
@apply text-sm font-semibold tracking-tight mt-4 mb-2;
color: var(--muted-foreground);
}
/* ─── Body text ─── */
.prose p {
@apply mb-4;
line-height: 1.75;
color: var(--foreground);
}
.prose strong {
@apply font-semibold;
color: var(--foreground);
}
.prose em {
@apply italic;
}
/* ─── Strikethrough ─── */
.prose del,
.prose s {
@apply line-through;
color: var(--muted-foreground);
}
/* ─── Highlight / Mark ─── */
.prose mark {
@apply rounded px-1 py-0.5;
background: #fef08a;
color: #422006;
}
.dark .prose mark {
background: #854d0e;
color: #fef9c3;
}
/* ─── Subscript / Superscript ─── */
.prose sub {
@apply text-[0.75em];
vertical-align: sub;
}
.prose sup {
@apply text-[0.75em];
vertical-align: super;
}
/* ─── Abbreviation ─── */
.prose abbr[title] {
@apply cursor-help;
text-decoration: underline dotted;
text-underline-offset: 3px;
}
/* ─── Keyboard ─── */
.prose kbd {
@apply inline-block rounded border px-1.5 py-0.5 font-mono text-[0.8em] font-medium leading-none;
background: var(--muted);
border-color: var(--border);
color: var(--foreground);
box-shadow: 0 1px 0 1px var(--border);
}
/* ─── Links ─── */
.prose a {
@apply underline underline-offset-2;
color: var(--primary);
}
/* ─── Lists ─── */
.prose ul {
@apply list-disc list-outside ml-6 mb-4;
color: var(--foreground);
}
.prose ol {
@apply list-decimal list-outside ml-6 mb-4;
color: var(--foreground);
}
.prose li {
@apply mb-1;
}
.prose li > ul,
.prose li > ol {
@apply mt-1 mb-0;
}
/* ─── Task list (GFM) ─── */
.prose ul:has(> li > input[type="checkbox"]) {
@apply list-none ml-0 pl-0;
}
.prose li:has(> input[type="checkbox"]) {
@apply flex items-start gap-0;
}
.prose li > input[type="checkbox"] {
@apply mt-1.5 shrink-0;
}
/* ─── Blockquote ─── */
.prose blockquote {
@apply border-l-[3px] pl-4 italic my-4;
border-color: var(--muted-foreground);
color: var(--muted-foreground);
}
.prose blockquote blockquote {
@apply mt-2;
border-color: var(--border);
}
/* ─── Horizontal rule ─── */
.prose hr {
@apply my-6 border-none;
height: 1px;
background: var(--border);
}
/* ─── Tables ─── */
.prose table {
@apply border-collapse;
}
.prose th {
@apply font-semibold px-4 py-2 border text-left;
background: var(--muted);
color: var(--foreground);
border-color: var(--border);
}
.prose td {
@apply px-4 py-2 border;
color: var(--foreground);
border-color: var(--border);
}
/* ─── Images ─── */
.prose img {
@apply my-4 max-w-full h-auto rounded-lg;
}
.prose figure {
@apply my-4;
}
.prose figcaption {
@apply mt-2 text-center text-sm;
color: var(--muted-foreground);
}
/* ─── Details / Summary ─── */
.prose details {
@apply my-4 rounded-lg border px-4 py-2;
background: var(--card);
border-color: var(--border);
}
.prose details:empty,
.prose details:not(:has(> :not(summary))),
.md-renderer details:empty,
.md-renderer details:not(:has(> :not(summary))) {
display: none;
}
.prose > :empty:not(hr):not(br):not(img) {
display: none;
}
.prose summary {
@apply cursor-pointer font-medium select-none;
color: var(--foreground);
}
.prose details[open] > summary {
@apply mb-2;
}
/* ─── Definition list ─── */
.prose dl {
@apply my-4;
}
.prose dt {
@apply font-semibold mt-3;
color: var(--foreground);
}
.prose dd {
@apply ml-6 mt-1;
color: var(--muted-foreground);
}
/* ─── Footnotes (remark-gfm) ─── */
.prose .footnotes {
@apply mt-8 border-t pt-4 text-sm;
border-color: var(--border);
color: var(--muted-foreground);
}
.prose .footnotes ol {
@apply ml-4;
}
/* ─── md-renderer: scope overrides only inside our component ─── */
.md-renderer .contains-task-list {
@apply list-none ml-0 pl-0;
}
/* Animations */
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fade-in 0.35s ease-out forwards;
}
/* Picker chrome: a soft scrim fade + a small spring-in for the card, so
modal pickers feel placed rather than popped. */
@keyframes dt-overlay-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.animate-overlay-in {
animation: dt-overlay-in 160ms ease-out;
}
@keyframes dt-pop-in {
from {
opacity: 0;
transform: translateY(6px) scale(0.985);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.animate-pop-in {
animation: dt-pop-in 200ms cubic-bezier(0.22, 1, 0.36, 1);
}
/* Streaming "breathing" label — used above the trace panel to surface that
the assistant is still working. Cycles between full and dim opacity with
a slow ease, mirroring Claude desktop's reasoning indicator. */
@keyframes dt-breathing {
0%,
100% {
opacity: 0.45;
}
50% {
opacity: 1;
}
}
.dt-breathing-text {
animation: dt-breathing 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* Icon-only stretch/contract on top of the opacity breathing. Slightly
different period (2.4s vs 1.8s) so the scale and opacity cycles drift
in and out of phase, giving the mark a hand-drawn, "alive" feel rather
than a single mechanical pulse. */
@keyframes dt-mark-pulse {
0%,
100% {
transform: scale(0.9);
}
50% {
transform: scale(1.08);
}
}
.dt-mark-pulse {
animation: dt-mark-pulse 2.4s cubic-bezier(0.4, 0, 0.6, 1) infinite;
transform-origin: center;
transform-box: fill-box;
will-change: transform;
}
/* While a fullscreen picker/modal is open, freeze ambient background
animations (sidebar "running session" pulse, breathing titles, pulsing
marks). They live behind the modal's frosted `backdrop-filter`; their
per-frame repaints were being re-sampled by the blur, reading as a constant
shimmer behind the scrim. `paused` holds them at their current frame (no
jump) and they resume on close. */
body[data-picker-open] .dt-breathing-text,
body[data-picker-open] .session-pulse,
body[data-picker-open] .dt-session-icon-running,
body[data-picker-open] .dt-mark-pulse {
animation-play-state: paused !important;
}
/* Capability menu pop-up — the dropdown blooms out of the chip itself
rather than fading in from above the composer. Origin at bottom-left so
the menu visually "unfurls" from the chevron corner of the chip. */
@keyframes dt-popup-up {
from {
opacity: 0;
transform: translateY(6px) scale(0.96);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.dt-popup-up {
animation: dt-popup-up 180ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
transform-origin: bottom left;
will-change: opacity, transform;
}
/* Session status pulse */
@keyframes session-pulse {
0%,
100% {
opacity: 0;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(2.2);
}
}
.session-pulse {
animation: session-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* Session avatar — gentle breathing wiggle when a session is running */
@keyframes dt-session-icon-running {
0%,
100% {
transform: translateY(0) rotate(0deg) scale(1);
}
25% {
transform: translateY(-0.6px) rotate(-6deg) scale(1.05);
}
50% {
transform: translateY(0) rotate(0deg) scale(1.02);
}
75% {
transform: translateY(0.6px) rotate(6deg) scale(1.05);
}
}
.dt-session-icon-running {
animation: dt-session-icon-running 1.6s cubic-bezier(0.45, 0.05, 0.55, 0.95)
infinite;
transform-origin: center;
}
/* ----- Chat shell ↔ file preview drawer ---------------------------------
Squeezes the chat column when the drawer is open. The transition
timing here MUST match FilePreviewDrawer's transform transition so the
slide-in and the squeeze animate as one motion. Stays a no-op below
the lg breakpoint, where the drawer overlays instead. */
.chat-preview-shell {
transition: padding-right 220ms ease-out;
padding-right: 0;
}
/* While the viewer is being drag-resized, kill the squeeze transition so the
chat column tracks the panel edge frame-for-frame instead of lagging. */
:root[data-viewer-resizing="true"] .chat-preview-shell {
transition: none;
}
@media (min-width: 1024px) {
/* The wider composer-side preview drawer (pre-send attachment peek)
gets the largest squeeze. */
.chat-preview-shell[data-preview-open="true"] {
padding-right: min(560px, 92vw);
}
/* Viewer panel (tabbed file/web preview, opened from message attachments
or assistant links) is next-widest. Wins over the activity panel when
both are open. Width is user-resizable, so the squeeze tracks the
``--viewer-width`` var the panel writes (falls back to 620px). */
.chat-preview-shell[data-preview-open="false"][data-viewer-open="true"] {
padding-right: min(var(--viewer-width, 620px), 92vw);
}
}
/* Sidebar tooltip */
.dt-tooltip-wrapper {
@apply relative inline-flex;
}
.dt-tooltip-content {
@apply absolute z-50 rounded-lg bg-[var(--popover)] px-3 py-1.5 text-xs shadow-md text-[var(--popover-foreground)] pointer-events-none;
animation: dt-tooltip-fade-in 150ms ease-out;
}
.dt-tooltip-label {
@apply font-medium truncate;
}
.dt-tooltip-desc {
@apply mt-0.5 text-[11px] text-[var(--muted-foreground)] truncate;
}
.dt-tooltip-content[data-side="right"] {
@apply left-full top-1/2 -translate-y-1/2 ml-2 max-w-[200px];
}
.dt-tooltip-content[data-side="bottom"] {
@apply bottom-[-40px] left-1/2 -translate-x-1/2 max-w-[180px];
}
@keyframes dt-tooltip-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* ─── Reduced motion ──────────────────────────────────────────────────────
Honor the OS-level prefers-reduced-motion setting. We don't strip motion
entirely — interaction-critical transitions (drawer slide, menu reveal)
stay visible but are shortened to a near-imperceptible duration so the
UI still communicates state changes without vestibular jitter. Loop
animations (breathing, pulse, spin) are removed outright. */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
.dt-breathing-text,
.dt-mark-pulse,
.session-pulse,
.animate-spin {
animation: none !important;
}
}