Files
opsxshell/src/index.css
T
Logan.Li b0d0e3a5f3 feat(i18n): add multi-language support (ja/ko/vi/ru/zh-TW) with system auto-detection
- Extend AppLanguage to 7 locales: zh-CN, zh-TW, en, ja, ko, vi, ru
- Auto-detect system language via navigator.languages; fallback to English
- Add BCP47 locale mapping for date formatting and sorting
- Create 50 translation JSON files (5 languages × 10 namespaces, 1137 keys each)
- Update language selector with native language names
- Extend locale key parity test to cover all 7 languages (70 assertions)
- Add VNC/RDP desktop, serial terminal, AI agent, port forwarding, and SFTP features
- Add credential encryption, host metrics, session backup crypto, and zmodem bridge
- Update Rust backend: desktop services, host metrics, SSH/SFTP enhancements
2026-08-12 00:07:48 +08:00

623 lines
15 KiB
CSS

@tailwind base;
@tailwind components;
@tailwind utilities;
/* ── Bundled Nerd Font (Powerline / Oh-My-Zsh / p10k glyphs) ── */
@font-face {
font-family: 'MesloLGS NF';
src: url('/fonts/MesloLGS NF Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'MesloLGS NF';
src: url('/fonts/MesloLGS NF Bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'MesloLGS NF';
src: url('/fonts/MesloLGS NF Italic.ttf') format('truetype');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'MesloLGS NF';
src: url('/fonts/MesloLGS NF Bold Italic.ttf') format('truetype');
font-weight: 700;
font-style: italic;
font-display: swap;
}
/* ===================================================================
* OpsXShell v1.4.0 — Professional Theme System
*
* Default (no data-theme) is dark mode for correct first paint.
* useTheme.ts immediately sets data-theme + overrides vars on mount.
*
* Design: Warp-inspired warm dark + Linear luminance stacking
* =================================================================== */
:root {
/* ---- Dark (default, matches warm slate-navy palette) ---- */
--color-dark-900: #0f172a;
--color-dark-800: #1e293b;
--color-dark-700: #334155;
--color-dark-600: #475569;
/* Semantic surface tokens */
--color-surface-base: #0f172a;
--color-surface-raised: #1e293b;
--color-surface-overlay: #334155;
--color-surface-inset: #475569;
--color-surface-hover: #374151;
--color-surface-active: #4b5563;
/* Text */
--color-text-primary: #f1f5f9;
--color-text-secondary: #cbd5e1;
--color-text-muted: #94a3b8;
--color-text-faint: #64748b;
--color-text-dim: #475569;
/* Content */
--color-content-primary: #f1f5f9;
--color-content-secondary: #cbd5e1;
--color-content-tertiary: #94a3b8;
--color-content-inverse: #0f172a;
/* Border — soft hairlines (not bright slate blocks) */
--color-border: rgba(148, 163, 184, 0.08);
--color-border-subtle: rgba(148, 163, 184, 0.06);
--color-border-default: rgba(148, 163, 184, 0.10);
--color-border-strong: rgba(148, 163, 184, 0.16);
--color-border-focus: #3b82f6;
/* Legacy border-dark-* remaps (components still use dark-600/700 borders) */
--hairline-soft: rgba(148, 163, 184, 0.06);
--hairline: rgba(148, 163, 184, 0.10);
--hairline-strong: rgba(148, 163, 184, 0.16);
/* Accent */
--color-accent-blue: #3b82f6;
--color-accent-blue-hover: #60a5fa;
--color-accent-green: #10b981;
--color-accent-purple: #8b5cf6;
--color-accent-orange: #f97316;
--color-accent-red: #ef4444;
/* Feedback */
--color-feedback-success: #10b981;
--color-feedback-success-bg: #064e3b;
--color-feedback-warning: #f59e0b;
--color-feedback-warning-bg: #78350f;
--color-feedback-error: #ef4444;
--color-feedback-error-bg: #7f1d1d;
--color-feedback-info: #3b82f6;
--color-feedback-info-bg: #1e3a5f;
/* Placeholder */
--color-placeholder: #64748b;
--color-placeholder-disabled: #475569;
/* UI chrome */
--color-scrollbar-track: #1e293b;
--color-scrollbar-thumb: #475569;
--color-scrollbar-thumb-h: #64748b;
--color-shadow-color: rgba(0, 0, 0, 0.4);
/* Terminal */
--color-terminal-bg: #0f172a;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Light theme hairlines (useTheme also sets these via tokens) */
[data-theme='light'] {
--color-border: rgba(15, 23, 42, 0.06);
--color-border-subtle: rgba(15, 23, 42, 0.04);
--color-border-default: rgba(15, 23, 42, 0.08);
--color-border-strong: rgba(15, 23, 42, 0.12);
--hairline-soft: rgba(15, 23, 42, 0.05);
--hairline: rgba(15, 23, 42, 0.08);
--hairline-strong: rgba(15, 23, 42, 0.12);
}
/*
* Soften legacy Tailwind border-dark-* / divide-dark-* classes.
* Surface colors (bg-dark-*) stay unchanged — only border-color is remapped.
*/
@layer utilities {
.border-dark-900,
.border-dark-800 {
border-color: var(--hairline-soft) !important;
}
.border-dark-700 {
border-color: var(--hairline) !important;
}
.border-dark-600 {
border-color: var(--hairline-strong) !important;
}
.border-dark-700\/20,
.border-dark-700\/30,
.border-dark-600\/30,
.border-dark-800\/40,
.border-dark-800\/50 {
border-color: var(--hairline-soft) !important;
}
.border-dark-700\/35,
.border-dark-700\/40,
.border-dark-700\/45,
.border-dark-700\/50,
.border-dark-700\/60,
.border-dark-700\/70,
.border-dark-600\/40,
.border-dark-600\/50,
.border-dark-600\/60 {
border-color: var(--hairline) !important;
}
.border-dark-600\/70,
.border-dark-600\/80,
.border-dark-500 {
border-color: var(--hairline-strong) !important;
}
/* SFTP / list row separators — even softer */
.border-b.border-dark-700\/20,
.border-t.border-dark-700\/20 {
border-color: var(--hairline-soft) !important;
}
.divide-dark-700\/20 > :not([hidden]) ~ :not([hidden]),
.divide-dark-700\/30 > :not([hidden]) ~ :not([hidden]),
.divide-dark-700\/35 > :not([hidden]) ~ :not([hidden]),
.divide-dark-700\/40 > :not([hidden]) ~ :not([hidden]),
.divide-dark-700\/50 > :not([hidden]) ~ :not([hidden]),
.divide-dark-600 > :not([hidden]) ~ :not([hidden]),
.divide-dark-700 > :not([hidden]) ~ :not([hidden]) {
border-color: var(--hairline-soft) !important;
}
.ring-dark-700,
.ring-dark-600 {
--tw-ring-color: var(--hairline) !important;
}
}
html, body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--color-surface-base) !important;
color: var(--color-text-primary);
overflow: hidden;
}
#root {
width: 100vw;
height: 100vh;
}
/* Window chrome utilities
* Tauri: drag region must be large enough to move the window.
* Interactive controls inside drag areas MUST set no-drag.
*/
.drag,
.titlebar-drag {
-webkit-app-region: drag;
app-region: drag;
}
.no-drag,
.titlebar-no-drag {
-webkit-app-region: no-drag;
app-region: no-drag;
}
/* Buttons/inputs inside drag regions stay clickable */
.titlebar-drag button,
.titlebar-drag a,
.titlebar-drag input,
.titlebar-drag select,
.titlebar-drag textarea,
.titlebar-drag [role='button'],
.drag button,
.drag a,
.drag input {
-webkit-app-region: no-drag;
app-region: no-drag;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-scrollbar-track);
}
::-webkit-scrollbar-thumb {
background: var(--color-scrollbar-thumb);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-scrollbar-thumb-h);
}
/* Terminal styles */
.terminal-container {
background-color: var(--color-terminal-bg);
padding: 8px;
border-radius: 4px;
height: 100%;
overflow: auto;
}
/* XTerm.js styles */
.xterm {
padding: 4px;
}
.xterm .xterm-viewport {
background-color: transparent !important;
}
.xterm .xterm-screen {
background-color: transparent !important;
}
/* Animation */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.animate-fade-in {
animation: fadeIn 0.2s ease-in-out;
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.6);
}
to {
opacity: 1;
transform: scale(1);
}
}
.animate-scale-in {
animation: scaleIn 0.15s ease-out;
}
/* Slide animations */
@keyframes slideInRight {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOutRight {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
@keyframes slideInUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes scaleIn {
from { transform: scale(0.95); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
@keyframes pulse-subtle {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
@keyframes pfDashFlow {
to {
stroke-dashoffset: -16;
}
}
@keyframes pfTunnelPulse {
0%,
100% {
opacity: 0.22;
stroke-dashoffset: 0;
}
50% {
opacity: 1;
stroke-dashoffset: -14;
}
}
.pf-tunnel-dash {
animation: pfTunnelPulse 1.35s ease-in-out infinite;
}
.animate-slide-in-right { animation: slideInRight 0.3s cubic-bezier(0.16, 1, 0.3, 1); }
.animate-slide-out-right { animation: slideOutRight 0.2s ease-in forwards; }
.animate-slide-in-up { animation: slideInUp 0.25s cubic-bezier(0.16, 1, 0.3, 1); }
.animate-scale-in { animation: scaleIn 0.2s cubic-bezier(0.16, 1, 0.3, 1); }
.animate-pulse-subtle { animation: pulse-subtle 2s ease-in-out infinite; }
/* Focus ring */
button:focus-visible,
a:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: 2px solid var(--color-accent-blue);
outline-offset: 2px;
}
button:focus:not(:focus-visible),
a:focus:not(:focus-visible) {
outline: none;
}
/* Button press feedback */
button:active:not(:disabled) {
transform: scale(0.98);
}
button {
transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease, transform 0.1s ease;
}
/* ── Form controls (use plain CSS: CSS-var colors don't support /opacity in @apply) ── */
.ui-label {
display: block;
font-size: 0.75rem;
font-weight: 500;
color: var(--color-text-muted);
margin-bottom: 0.375rem;
letter-spacing: 0.025em;
}
.ui-input,
.ui-input-sm,
.ui-select,
.ui-textarea,
.ui-select-pill {
width: 100%;
border-radius: 0.5rem;
background-color: var(--color-surface-base);
border: 1px solid var(--color-border-default);
color: var(--color-content-primary);
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.ui-input,
.ui-select,
.ui-textarea {
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
}
.ui-input-sm {
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
}
.ui-input::placeholder,
.ui-input-sm::placeholder,
.ui-textarea::placeholder {
color: var(--color-placeholder);
}
.ui-input:focus,
.ui-input-sm:focus,
.ui-select:focus,
.ui-textarea:focus,
.ui-select-pill:focus {
outline: none;
border-color: var(--color-accent-blue);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.18);
}
.ui-input:disabled,
.ui-input-sm:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.ui-select {
appearance: none;
cursor: pointer;
padding-right: 2rem;
}
.ui-textarea {
resize: vertical;
min-height: 80px;
font-family: ui-monospace, Menlo, Monaco, Consolas, monospace;
line-height: 1.625;
}
.ui-section {
border-radius: 0.75rem;
border: 1px solid var(--color-border-default);
background-color: var(--color-surface-raised);
padding: 1rem;
}
.ui-section-title {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-faint);
}
.ui-divider {
border-top: 1px solid var(--color-border-default);
}
.ui-icon-box {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background-color: rgba(59, 130, 246, 0.12);
border: 1px solid rgba(59, 130, 246, 0.2);
color: var(--color-accent-blue);
}
.ui-chip {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.5rem;
border-radius: 0.375rem;
font-size: 11px;
font-weight: 500;
background-color: var(--color-surface-overlay);
color: var(--color-text-muted);
border: 1px solid var(--color-border-default);
}
.ui-nav-item {
width: 100%;
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.625rem 0.75rem;
border-radius: 0.75rem;
text-align: left;
transition: background-color 0.15s ease, color 0.15s ease;
color: var(--color-text-muted);
border: 1px solid transparent;
}
.ui-nav-item:hover {
background-color: var(--color-surface-overlay);
color: var(--color-content-primary);
}
.ui-nav-item-active {
background-color: rgba(59, 130, 246, 0.12);
color: var(--color-content-primary);
border-color: rgba(59, 130, 246, 0.25);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}
.launcher-card {
display: flex;
align-items: center;
gap: 0.625rem;
border-radius: 0.75rem;
border: 1px solid var(--color-border-default);
background: var(--color-surface-raised);
padding: 0.75rem;
text-align: left;
color: var(--color-content-primary);
}
.launcher-card:hover,
.launcher-row:hover {
border-color: rgba(59, 130, 246, 0.45);
background: var(--color-surface-overlay);
}
.launcher-card span,
.launcher-row span {
display: flex;
min-width: 0;
flex-direction: column;
}
.launcher-card strong,
.launcher-row strong {
font-size: 0.75rem;
font-weight: 600;
color: var(--color-content-primary);
}
.launcher-card small,
.launcher-row small {
margin-top: 0.125rem;
font-size: 0.625rem;
color: var(--color-text-faint);
}
.launcher-row {
display: flex;
width: 100%;
align-items: center;
gap: 0.625rem;
border-radius: 0.625rem;
border: 1px solid transparent;
padding: 0.625rem 0.75rem;
color: var(--color-content-primary);
}
.launcher-badge {
display: inline-flex !important;
flex-direction: row !important;
flex-shrink: 0;
border-radius: 0.375rem;
background: var(--color-surface-overlay);
padding: 0.125rem 0.375rem;
font-size: 0.625rem;
color: var(--color-text-muted);
}
/* Pill select — compact control on the right */
.ui-select-pill {
width: auto;
max-width: 200px;
appearance: none;
cursor: pointer;
padding: 0.375rem 2rem 0.375rem 0.75rem;
font-size: 0.75rem;
border-radius: 0.75rem;
background-color: var(--color-surface-base);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.6rem center;
}
/* Drawer enter from right */
@keyframes slideInRightDrawer {
from { transform: translateX(100%); opacity: 0.6; }
to { transform: translateX(0); opacity: 1; }
}
.animate-slide-in-right {
animation: slideInRightDrawer 0.22s cubic-bezier(0.16, 1, 0.3, 1);
}
/* Skeleton shimmer */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.animate-shimmer {
background: linear-gradient(90deg, var(--color-surface-raised) 25%, var(--color-surface-overlay) 50%, var(--color-surface-raised) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
}