Files
stack/apps/web/src/app/globals.css
Jason Woltje f4b4ba4c54
Some checks failed
ci/woodpecker/push/web Pipeline failed
feat(web): implement SSE chat streaming with real-time token rendering
- Implement streamChatMessage() using fetch ReadableStream for SSE parsing
- Update useChat hook with streaming state, abort support, and fallback to non-streaming
- Add streaming indicator (blinking cursor) in MessageList during token streaming
- Update ChatInput with Stop button during streaming, disable input while streaming
- Add CSS animations for streaming cursor
- Fix message ID uniqueness to prevent collisions in rapid sends
- Update tests for streaming path with makeStreamSucceed/makeStreamFail helpers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 20:38:49 -06:00

897 lines
21 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.
@tailwind base;
@tailwind components;
@tailwind utilities;
/* =============================================================================
MOSAIC DESIGN SYSTEM — Reference token system from dashboard design
============================================================================= */
/* -----------------------------------------------------------------------------
Primitive Tokens (Dark-first — dark is the default theme)
----------------------------------------------------------------------------- */
:root {
/* Mosaic design tokens — dark palette (default) */
--ms-bg-950: #080b12;
--ms-bg-900: #0f141d;
--ms-bg-850: #151b26;
--ms-surface-800: #1b2331;
--ms-surface-750: #232d3f;
--ms-border-700: #2f3b52;
--ms-text-100: #eef3ff;
--ms-text-300: #c5d0e6;
--ms-text-500: #8f9db7;
--ms-blue-500: #2f80ff;
--ms-blue-400: #56a0ff;
--ms-red-500: #e5484d;
--ms-red-400: #f06a6f;
--ms-purple-500: #8b5cf6;
--ms-purple-400: #a78bfa;
--ms-teal-500: #14b8a6;
--ms-teal-400: #2dd4bf;
--ms-amber-500: #f59e0b;
--ms-amber-400: #fbbf24;
--ms-pink-500: #ec4899;
--ms-emerald-500: #10b981;
--ms-orange-500: #f97316;
--ms-cyan-500: #06b6d4;
--ms-indigo-500: #6366f1;
/* Semantic aliases — dark theme is default */
--bg: var(--ms-bg-900);
--bg-deep: var(--ms-bg-950);
--bg-mid: var(--ms-bg-850);
--surface: var(--ms-surface-800);
--surface-2: var(--ms-surface-750);
--border: var(--ms-border-700);
--text: var(--ms-text-100);
--text-2: var(--ms-text-300);
--muted: var(--ms-text-500);
--primary: var(--ms-blue-500);
--primary-l: var(--ms-blue-400);
--danger: var(--ms-red-500);
--success: var(--ms-teal-500);
--warn: var(--ms-amber-500);
--purple: var(--ms-purple-500);
/* Typography */
--font: var(--font-outfit, 'Outfit'), system-ui, sans-serif;
--mono: var(--font-fira-code, 'Fira Code'), 'Cascadia Code', monospace;
/* Radius scale */
--r: 8px;
--r-sm: 5px;
--r-lg: 12px;
--r-xl: 16px;
/* Layout dimensions */
--sidebar-w: 260px;
--topbar-h: 56px;
--terminal-h: 220px;
/* Easing */
--ease: cubic-bezier(0.16, 1, 0.3, 1);
/* Legacy shadow tokens (retained for component compat) */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.5), 0 4px 6px -4px rgb(0 0 0 / 0.4);
}
/* -----------------------------------------------------------------------------
Light Theme Override — applied via data-theme attribute on <html>
----------------------------------------------------------------------------- */
[data-theme="light"] {
--ms-bg-950: #f8faff;
--ms-bg-900: #f0f4fc;
--ms-bg-850: #e8edf8;
--ms-surface-800: #dde4f2;
--ms-surface-750: #d0d9ec;
--ms-border-700: #b8c4de;
--ms-text-100: #0f141d;
--ms-text-300: #2f3b52;
--ms-text-500: #5a6a87;
/* Re-alias semantics for light — identical structure, primitive tokens differ */
--bg: var(--ms-bg-900);
--bg-deep: var(--ms-bg-950);
--bg-mid: var(--ms-bg-850);
--surface: var(--ms-surface-800);
--surface-2: var(--ms-surface-750);
--border: var(--ms-border-700);
--text: var(--ms-text-100);
--text-2: var(--ms-text-300);
--muted: var(--ms-text-500);
/* Lighter shadows for light mode */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05), 0 1px 3px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.08), 0 2px 4px -2px rgb(0 0 0 / 0.06);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.08);
}
/* -----------------------------------------------------------------------------
Base Styles
----------------------------------------------------------------------------- */
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-size: 15px;
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: var(--font);
background: var(--bg);
color: var(--text);
line-height: 1.5;
}
a {
color: inherit;
text-decoration: none;
}
button {
font-family: inherit;
cursor: pointer;
border: none;
background: none;
color: inherit;
}
input,
select,
textarea {
font-family: inherit;
}
ul {
list-style: none;
}
/* Subtle grain/noise overlay for texture */
body::before {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
z-index: 9999;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='1'/%3E%3C/svg%3E");
opacity: 0.025;
}
/* -----------------------------------------------------------------------------
Focus States - Accessible & Visible
----------------------------------------------------------------------------- */
@layer base {
:focus-visible {
outline: 2px solid var(--ms-blue-400);
outline-offset: 2px;
}
:focus:not(:focus-visible) {
outline: none;
}
}
/* -----------------------------------------------------------------------------
Scrollbar Styling - Minimal & Professional
----------------------------------------------------------------------------- */
::-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);
}
/* Firefox */
* {
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
/* -----------------------------------------------------------------------------
App Shell Grid Layout
----------------------------------------------------------------------------- */
.app-shell {
display: grid;
grid-template-columns: var(--sidebar-w) 1fr;
grid-template-rows: var(--topbar-h) 1fr;
height: 100vh;
overflow: hidden;
}
.app-header {
grid-column: 1 / -1;
grid-row: 1;
background: var(--bg-deep);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 20px;
gap: 12px;
z-index: 100;
}
.app-sidebar {
grid-column: 1;
grid-row: 2;
background: var(--bg-deep);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
}
.app-main {
grid-column: 2;
grid-row: 2;
background: var(--bg);
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
/* -----------------------------------------------------------------------------
Responsive App Shell — Mobile (< 768px): single-column, sidebar as overlay
----------------------------------------------------------------------------- */
@media (max-width: 767px) {
.app-shell {
grid-template-columns: 1fr;
}
.app-sidebar {
position: fixed;
left: 0;
top: var(--topbar-h);
bottom: 0;
width: 240px;
z-index: 150;
transform: translateX(-100%);
transition: transform 0.2s ease;
}
.app-sidebar[data-mobile-open="true"] {
transform: translateX(0);
}
.app-main {
grid-column: 1;
}
.app-header {
grid-column: 1;
}
}
/* -----------------------------------------------------------------------------
Responsive App Shell — Tablet (768px1023px): sidebar toggleable, pushes content
----------------------------------------------------------------------------- */
@media (min-width: 768px) and (max-width: 1023px) {
.app-shell[data-sidebar-hidden="true"] {
grid-template-columns: 1fr;
}
.app-shell[data-sidebar-hidden="true"] .app-sidebar {
display: none;
}
.app-shell[data-sidebar-hidden="true"] .app-main {
grid-column: 1;
}
.app-shell[data-sidebar-hidden="true"] .app-header {
grid-column: 1;
}
}
/* -----------------------------------------------------------------------------
Typography Utilities
----------------------------------------------------------------------------- */
@layer utilities {
.text-display {
font-size: 1.875rem;
line-height: 2.25rem;
font-weight: 600;
letter-spacing: -0.025em;
}
.text-heading-1 {
font-size: 1.5rem;
line-height: 2rem;
font-weight: 600;
letter-spacing: -0.025em;
}
.text-heading-2 {
font-size: 1.125rem;
line-height: 1.5rem;
font-weight: 600;
letter-spacing: -0.01em;
}
.text-body {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-caption {
font-size: 0.75rem;
line-height: 1rem;
}
.text-mono {
font-family: var(--mono);
font-size: 0.8125rem;
line-height: 1.25rem;
}
}
/* -----------------------------------------------------------------------------
Button Component Styles
----------------------------------------------------------------------------- */
@layer components {
.btn {
@apply inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-all duration-150;
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
@apply disabled:opacity-50 disabled:cursor-not-allowed;
}
.btn-primary {
@apply btn px-4 py-2;
background: linear-gradient(135deg, var(--ms-blue-500), var(--ms-purple-500));
color: white;
border-radius: var(--r);
}
.btn-primary:hover:not(:disabled) {
box-shadow: 0 8px 28px rgba(47, 128, 255, 0.38);
transform: translateY(-1px);
}
.btn-secondary {
@apply btn px-4 py-2;
background-color: var(--surface);
color: var(--text-2);
border: 1px solid var(--border);
border-radius: var(--r);
}
.btn-secondary:hover:not(:disabled) {
background-color: var(--surface-2);
color: var(--text);
}
.btn-ghost {
@apply btn px-3 py-2;
background-color: transparent;
color: var(--muted);
border-radius: var(--r);
}
.btn-ghost:hover:not(:disabled) {
background-color: var(--surface);
color: var(--text);
}
.btn-danger {
@apply btn px-4 py-2;
background-color: var(--danger);
color: white;
border-radius: var(--r);
}
.btn-danger:hover:not(:disabled) {
filter: brightness(0.9);
}
.btn-sm {
@apply px-3 py-1.5 text-xs;
}
.btn-lg {
@apply px-6 py-3 text-base;
}
}
/* -----------------------------------------------------------------------------
Input Component Styles
----------------------------------------------------------------------------- */
@layer components {
.input {
@apply w-full text-sm transition-all duration-150;
@apply focus:outline-none;
background-color: var(--bg);
border: 1px solid var(--border);
border-radius: var(--r);
color: var(--text);
padding: 11px 14px;
}
.input::placeholder {
color: var(--muted);
}
.input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(47, 128, 255, 0.12);
}
.input:disabled {
@apply opacity-50 cursor-not-allowed;
background-color: var(--surface);
}
.input-error {
border-color: var(--danger);
}
.input-error:focus {
border-color: var(--danger);
box-shadow: 0 0 0 3px rgba(229, 72, 77, 0.12);
}
}
/* -----------------------------------------------------------------------------
Card Component Styles
----------------------------------------------------------------------------- */
@layer components {
.card {
@apply rounded-lg p-4;
background-color: var(--surface);
border: 1px solid var(--border);
box-shadow: var(--shadow-sm);
}
.card-elevated {
@apply card;
box-shadow: var(--shadow-md);
}
.card-interactive {
@apply card transition-all duration-150;
}
.card-interactive:hover {
border-color: var(--muted);
box-shadow: var(--shadow-md);
}
}
/* -----------------------------------------------------------------------------
Badge Component Styles
----------------------------------------------------------------------------- */
@layer components {
.badge {
@apply inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium;
}
.badge-success {
background-color: rgba(20, 184, 166, 0.15);
color: var(--ms-teal-400);
}
.badge-warning {
background-color: rgba(245, 158, 11, 0.15);
color: var(--ms-amber-400);
}
.badge-error {
background-color: rgba(229, 72, 77, 0.15);
color: var(--ms-red-400);
}
.badge-info {
background-color: rgba(47, 128, 255, 0.15);
color: var(--ms-blue-400);
}
.badge-neutral {
background-color: var(--surface-2);
color: var(--text-2);
}
.badge-primary {
background-color: rgba(47, 128, 255, 0.15);
color: var(--primary-l);
}
}
/* -----------------------------------------------------------------------------
Status Indicator Styles
----------------------------------------------------------------------------- */
@layer components {
.status-dot {
@apply inline-block h-2 w-2 rounded-full;
}
.status-dot-success {
background-color: var(--success);
box-shadow: 0 0 5px var(--success);
}
.status-dot-warning {
background-color: var(--warn);
box-shadow: 0 0 5px var(--warn);
}
.status-dot-error {
background-color: var(--danger);
box-shadow: 0 0 5px var(--danger);
}
.status-dot-info {
background-color: var(--primary);
box-shadow: 0 0 5px var(--primary);
}
.status-dot-neutral {
background-color: var(--muted);
}
.status-dot-pulse {
@apply relative;
}
.status-dot-pulse::before {
content: "";
@apply absolute inset-0 rounded-full animate-ping;
background-color: inherit;
opacity: 0.5;
}
}
/* -----------------------------------------------------------------------------
Keyboard Shortcut Styling
----------------------------------------------------------------------------- */
@layer components {
.kbd {
@apply inline-flex items-center justify-center rounded px-1.5 py-0.5 text-xs font-medium;
background-color: var(--surface-2);
border: 1px solid var(--border);
color: var(--muted);
font-family: var(--mono);
min-width: 1.5rem;
box-shadow: 0 1px 0 var(--border);
}
.kbd-group {
@apply inline-flex items-center gap-1;
}
}
/* -----------------------------------------------------------------------------
Table Styles - Dense & Professional
----------------------------------------------------------------------------- */
@layer components {
.table-pro {
@apply w-full text-sm;
}
.table-pro thead {
@apply sticky top-0;
background-color: var(--surface);
border-bottom: 1px solid var(--border);
}
.table-pro th {
@apply px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider;
color: var(--muted);
}
.table-pro th.sortable {
@apply cursor-pointer select-none;
}
.table-pro th.sortable:hover {
color: var(--text);
}
.table-pro tbody tr {
border-bottom: 1px solid var(--border);
transition: background-color 0.1s ease;
}
.table-pro tbody tr:hover {
background-color: var(--surface);
}
.table-pro td {
@apply px-4 py-3;
}
.table-pro-dense td {
@apply py-2;
}
}
/* -----------------------------------------------------------------------------
Skeleton Loading Styles
----------------------------------------------------------------------------- */
@layer components {
.skeleton {
@apply animate-pulse rounded;
background: linear-gradient(
90deg,
var(--surface) 0%,
var(--surface-2) 50%,
var(--surface) 100%
);
background-size: 200% 100%;
}
.skeleton-text {
@apply skeleton h-4 w-full;
}
.skeleton-text-sm {
@apply skeleton h-3 w-3/4;
}
.skeleton-avatar {
@apply skeleton h-10 w-10 rounded-full;
}
.skeleton-card {
@apply skeleton h-32 w-full;
}
}
/* -----------------------------------------------------------------------------
Modal & Dialog Styles
----------------------------------------------------------------------------- */
@layer components {
.modal-backdrop {
@apply fixed inset-0 z-50 flex items-center justify-center p-4;
background-color: rgb(0 0 0 / 0.5);
backdrop-filter: blur(2px);
}
.modal-content {
@apply relative max-h-[90vh] w-full max-w-lg overflow-y-auto;
background-color: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r-lg);
box-shadow: var(--shadow-lg);
}
.modal-header {
@apply flex items-center justify-between p-4 border-b;
border-color: var(--border);
}
.modal-body {
@apply p-4;
}
.modal-footer {
@apply flex items-center justify-end gap-3 p-4 border-t;
border-color: var(--border);
}
}
/* -----------------------------------------------------------------------------
Tooltip Styles
----------------------------------------------------------------------------- */
@layer components {
.tooltip {
@apply absolute z-50 rounded px-2 py-1 text-xs font-medium;
background-color: var(--text);
color: var(--bg);
box-shadow: var(--shadow-md);
border-radius: var(--r-sm);
}
.tooltip::before {
content: "";
@apply absolute;
border: 4px solid transparent;
}
.tooltip-top::before {
@apply left-1/2 top-full -translate-x-1/2;
border-top-color: var(--text);
}
}
/* -----------------------------------------------------------------------------
Animations - Functional Only
----------------------------------------------------------------------------- */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.98);
}
to {
opacity: 1;
transform: scale(1);
}
}
.animate-fade-in {
animation: fadeIn 0.15s ease-out;
}
.animate-slide-in {
animation: slideIn 0.15s ease-out;
}
.animate-scale-in {
animation: scaleIn 0.15s ease-out;
}
.message-animate {
animation: slideIn 0.2s ease-out;
}
.animate-menu-enter {
animation: scaleIn 0.1s ease-out;
}
/* Streaming cursor for real-time token rendering */
@keyframes streaming-cursor-blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.streaming-cursor {
display: inline-block;
width: 2px;
height: 1em;
background-color: rgb(var(--accent-primary));
border-radius: 1px;
animation: streaming-cursor-blink 1s step-end infinite;
vertical-align: text-bottom;
margin-left: 1px;
}
/* -----------------------------------------------------------------------------
Dashboard Layout — Responsive Grids
----------------------------------------------------------------------------- */
.metrics-strip {
display: grid;
grid-template-columns: repeat(var(--ms-cols, 6), 1fr);
gap: 0;
border-radius: var(--r-lg);
overflow: hidden;
border: 1px solid var(--border);
}
.metric-cell {
border-left: 1px solid var(--border);
}
.metric-cell:first-child {
border-left: none;
}
@media (max-width: 900px) {
.metrics-strip {
grid-template-columns: repeat(3, 1fr);
}
.metric-cell:nth-child(3n + 1) {
border-left: none;
}
}
@media (max-width: 640px) {
.metrics-strip {
grid-template-columns: repeat(2, 1fr);
}
.metric-cell:nth-child(3n + 1) {
border-left: 1px solid var(--border);
}
.metric-cell:nth-child(2n + 1) {
border-left: none;
}
}
.dash-grid {
display: grid;
grid-template-columns: 1fr 320px;
gap: 16px;
}
@media (max-width: 900px) {
.dash-grid {
grid-template-columns: 1fr;
}
}
/* -----------------------------------------------------------------------------
Responsive Typography Adjustments
----------------------------------------------------------------------------- */
@media (max-width: 640px) {
.text-display {
font-size: 1.5rem;
line-height: 2rem;
}
.text-heading-1 {
font-size: 1.25rem;
line-height: 1.75rem;
}
}
/* -----------------------------------------------------------------------------
High Contrast Mode Support
----------------------------------------------------------------------------- */
@media (prefers-contrast: high) {
:root {
--border: #4a5a78;
--muted: #a0b0cc;
}
}
/* -----------------------------------------------------------------------------
Reduced Motion Support
----------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* -----------------------------------------------------------------------------
Print Styles
----------------------------------------------------------------------------- */
@media print {
body {
background: white;
color: black;
}
.no-print {
display: none !important;
}
}