Files
Netcatty/index.css
T
陈大猫andClaude Opus 4.7 c941038e68 [codex] Bundle Symbols Nerd Font Mono for terminal icon fallback (#846)
* Bundle Symbols Nerd Font Mono as terminal icon fallback

PR #845 added "Symbols Nerd Font Mono" to the terminal fontFamily
fallback chain so PUA glyphs (powerline / devicons / etc.) resolve
even when the user's primary font lacks them. That only worked if the
user had separately installed the symbol font; ship it ourselves so
icons render out of the box regardless of the chosen base font.

- Drop SymbolsNerdFontMono-Regular.ttf into public/fonts (~2.5 MB);
  Vite copies it to dist/fonts and the existing app:// protocol
  handler already knows the font/ttf MIME type.
- Register an @font-face in index.css pointing at the bundled file.
  font-display: block prevents tofu while the (instantly-available
  bundled) face loads, only affecting PUA glyphs since the base font
  is listed earlier in the fallback chain.
- Include the upstream LICENSE next to the font.

Source: ryanoasis/nerd-fonts NerdFontsSymbolsOnly v3.4.0 (MIT).

Refs #843

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Reference bundled font by absolute path so prod build resolves

Address Codex P2 on PR #846: the relative `./fonts/...` URL was emitted
verbatim into dist/assets/index-*.css, where the browser resolved it
against the CSS file's location and 404'd on
dist/assets/fonts/SymbolsNerdFontMono-Regular.ttf — the actual file
lives in dist/fonts/, so the icon fallback never loaded in packaged
builds and Nerd Font glyphs still rendered as tofu.

Switch the @font-face url() to `/fonts/...`. Vite's `base: "./"`
config rewrites that to the correct dist-relative form during build
(`../fonts/SymbolsNerdFontMono-Regular.ttf` from dist/assets/), and in
dev the same path is served by the Vite dev server out of public/.
Verified by re-running `vite build` and grepping the produced CSS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 08:39:01 +08:00

517 lines
11 KiB
CSS

@import "tailwindcss";
/* Bundled icon-only fallback so terminals show Nerd Font glyphs (powerline,
devicons, etc.) regardless of which base font the user picks. The font is
referenced last in the fontFamily fallback chain (see withCjkFallback in
infrastructure/config/fonts.ts) — base text comes from the user's chosen
font, missing PUA glyphs fall through to this face.
Source: https://github.com/ryanoasis/nerd-fonts (NerdFontsSymbolsOnly,
v3.4.0). License: MIT — see public/fonts/SymbolsNerdFont-LICENSE.txt. */
@font-face {
font-family: "Symbols Nerd Font Mono";
/* Absolute path resolves against the document origin in both dev
(http://localhost:5173) and packaged (app://netcatty), regardless of
where the bundled CSS file ends up. A relative ./fonts/... would be
resolved against dist/assets/index-*.css in production and 404. */
src: url("/fonts/SymbolsNerdFontMono-Regular.ttf") format("truetype");
font-weight: normal;
font-style: normal;
font-display: block;
}
/* ============================================
Tailwind CSS v4 Theme Configuration
============================================ */
@theme {
/* Default border color - ensures 'border' class uses theme color instead of currentColor */
--default-border-color: var(--color-border);
/* Colors - mapped from CSS variables */
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
/* Border Radius */
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
/* Fonts */
--font-sans: "Space Grotesk", system-ui, sans-serif;
--font-mono: "JetBrains Mono", monospace;
/* Animations */
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-fade-in: fade-in 0.2s ease-out;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-collapsible-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-collapsible-content-height);
}
to {
height: 0;
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
}
@keyframes pop-in {
0% {
opacity: 0;
transform: scale(0.82) translateY(6px);
}
45% {
opacity: 1;
transform: scale(1.06) translateY(-2px);
}
72% {
transform: scale(0.97) translateY(1px);
}
88% {
transform: scale(1.01);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes ripple {
0% {
transform: scale(0);
opacity: 0.35;
}
100% {
transform: scale(1);
opacity: 0;
}
}
@keyframes split-panel-enter {
0% {
width: 0;
min-width: 0;
opacity: 0;
transform: translateX(22px);
}
55% {
opacity: 0.88;
}
100% {
width: var(--aside-inline-width);
min-width: var(--aside-inline-width);
opacity: 1;
transform: translateX(0);
}
}
.split-panel-enter {
animation: split-panel-enter 220ms cubic-bezier(0.24, 0.84, 0.32, 1) both;
will-change: width, opacity, transform;
}
:root {
color-scheme: light;
}
/* Global default border color - ensures 'border' class always uses theme color */
*,
*::before,
*::after {
border-color: hsl(var(--border));
}
body {
min-height: 100vh;
font-family: var(--font-sans);
background-color: hsl(var(--background));
color: hsl(var(--foreground));
}
[data-instant-theme-switch="true"],
[data-instant-theme-switch="true"] *,
[data-instant-theme-switch="true"] *::before,
[data-instant-theme-switch="true"] *::after {
transition: none !important;
}
.dark {
color-scheme: dark;
}
.dark body {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
}
/* Immersive mode: fade-out overlay for smooth theme restoration */
.immersive-fade-overlay {
position: fixed;
inset: 0;
z-index: 9999;
pointer-events: none;
opacity: 1;
transition: opacity 200ms ease-out;
}
.immersive-fade-overlay.fade-out {
opacity: 0;
}
/* Immersive mode: override agent icon badge colors to follow the theme */
.immersive-transition [data-agent-badge] {
border-color: hsl(var(--primary) / 0.2) !important;
background-color: hsl(var(--primary) / 0.1) !important;
}
.netcatty-shell {
position: relative;
background: linear-gradient(180deg, hsl(var(--background)) 0%, hsl(var(--background)) 60%, hsl(var(--background) / 0.9) 100%);
}
/* Slim down xterm 6.0 VS Code scrollbar — wide hit area, thin visual slider */
.xterm .xterm-scrollable-element > .scrollbar.vertical {
width: 12px !important;
}
.xterm .xterm-scrollable-element > .scrollbar.vertical > .slider {
width: 6px !important;
border-radius: 3px;
left: 3px !important;
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.thinking-shimmer {
background: linear-gradient(
90deg,
currentColor 0%,
currentColor 40%,
rgba(255, 255, 255, 0.6) 50%,
currentColor 60%,
currentColor 100%
);
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: thinking-shimmer 1.5s ease-in-out infinite;
}
@keyframes thinking-shimmer {
0% {
background-position: 100% center;
}
100% {
background-position: -100% center;
}
}
@keyframes progress-shimmer {
0% {
transform: translateX(-200%);
}
100% {
transform: translateX(200%);
}
}
.glass-panel {
background: hsl(var(--secondary) / 0.95);
border: 1px solid hsl(var(--border) / 0.8);
box-shadow: 0 14px 40px hsl(var(--foreground) / 0.12);
}
.soft-card {
background: hsl(var(--card));
border: 1px solid hsl(var(--border) / 0.8);
box-shadow: 0 4px 12px hsl(var(--foreground) / 0.06);
}
@keyframes session-activity-breathe {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.18;
}
}
.session-activity-dot {
animation: session-activity-breathe 1.4s ease-in-out infinite;
}
.card-highlight {
position: relative;
overflow: hidden;
}
.card-highlight::before {
content: "";
position: absolute;
inset: -40%;
background: radial-gradient(500px circle at 20% 20%, hsl(var(--primary) / 0.18), transparent 55%);
opacity: 0;
transition: opacity 180ms ease;
}
.card-highlight:hover::before {
opacity: 1;
}
.elevate {
transition: transform 140ms ease, border-color 140ms ease;
}
.elevate:hover {
transform: translateY(-2px);
}
::selection {
background: hsl(var(--primary) / 0.22);
color: hsl(var(--foreground));
}
/* Port Forwarding Traffic Animation */
@keyframes traffic-flow {
0% {
stroke-dashoffset: 24;
}
100% {
stroke-dashoffset: 0;
}
}
@keyframes traffic-flow-reverse {
0% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: 24;
}
}
@keyframes pulse-glow {
0%,
100% {
opacity: 0.6;
}
50% {
opacity: 1;
}
}
.traffic-line {
stroke-dasharray: 8 4;
animation: traffic-flow 0.8s linear infinite;
}
.traffic-line-reverse {
stroke-dasharray: 8 4;
animation: traffic-flow-reverse 0.8s linear infinite;
}
.traffic-glow {
animation: pulse-glow 1.5s ease-in-out infinite;
}
/* Custom titlebar drag regions for Electron */
.app-drag {
-webkit-app-region: drag;
app-region: drag;
user-select: none;
-webkit-user-select: none;
}
.app-no-drag {
-webkit-app-region: no-drag;
app-region: no-drag;
user-select: auto;
-webkit-user-select: auto;
}
.app-no-drag * {
-webkit-app-region: no-drag;
app-region: no-drag;
user-select: auto;
-webkit-user-select: auto;
}
.no-scrollbar {
scrollbar-width: none;
-ms-overflow-style: none;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.workspace-pane {
outline: none;
overflow: visible;
}
/* Dim terminal text in unfocused workspace panes (default) */
.workspace-pane:not(:focus-within) .xterm-screen {
opacity: 0.82;
}
/* Keep current pane fully visible while its context menu / popover is open;
focus moves to the menu portal and would otherwise drop :focus-within. */
.workspace-pane[data-menu-open] .xterm-screen {
opacity: 1;
}
/* Border-style focus indicator (opt-in via data attribute) */
[data-workspace-focus="border"] .workspace-pane:not(:focus-within) .xterm-screen {
opacity: 1;
}
[data-workspace-focus="border"] .workspace-pane::after {
content: "";
position: absolute;
inset: 0;
border: 2px solid transparent;
pointer-events: none;
transition: border-color 120ms ease;
z-index: 40;
}
[data-workspace-focus="border"] .workspace-pane:focus-within::after {
border-color: hsl(var(--primary));
}
/* ── Streamdown code block overrides ── */
[data-streamdown="code-block"] {
position: relative !important;
border-radius: 10px !important;
background: hsl(var(--muted) / 0.5) !important;
overflow: hidden !important;
margin: 6px 0 !important;
padding: 0 !important;
border: none !important;
gap: 0 !important;
}
[data-streamdown="code-block-header"] {
height: auto !important;
padding: 4px 12px 0 !important;
font-size: 11px !important;
}
[data-streamdown="code-block-header"] span {
margin-left: 0 !important;
}
[data-streamdown="code-block-actions"] {
position: absolute !important;
top: 4px !important;
right: 4px !important;
border: none !important;
background: none !important;
backdrop-filter: none !important;
padding: 0 !important;
gap: 2px !important;
opacity: 0;
transition: opacity 0.15s;
}
[data-streamdown="code-block"]:hover [data-streamdown="code-block-actions"] {
opacity: 1;
}
[data-streamdown="code-block-actions"] button {
padding: 4px !important;
border-radius: 4px;
}
[data-streamdown="code-block-body"] {
border: none !important;
border-radius: 0 !important;
background: transparent !important;
padding: 0 !important;
margin: 0 !important;
overflow-x: auto !important;
overflow-y: hidden !important;
overscroll-behavior-x: contain;
-webkit-overflow-scrolling: touch;
font-size: 0 !important; /* collapse whitespace text nodes */
}
[data-streamdown="code-block-body"] pre {
font-size: 12px !important; /* restore in pre */
}
[data-streamdown="code-block"] pre {
display: block !important;
margin: 0 !important;
background: transparent !important;
border: none !important;
border-radius: 0 !important;
padding: 0 12px 10px !important;
width: max-content !important;
min-width: 100% !important;
font-size: 12px !important;
line-height: 1.5 !important;
white-space: pre !important;
}