Files
new-api/web/src/styles/index.css
T
RedwindA 137d1171f2 feat(web): fade in streamed response words and harden playground editor (#6895)
* feat(web): fade in newly streamed response words

Animate only new word-level deltas while markdown is still streaming, and
cache markdown-it instances per parser id so concurrent Response trees do
not rebuild or reparse on every render.

* fix(web): keep CodeMirror editor alive across keystroke re-renders

Deliver onKeyDown through a ref instead of the extensions memo so a new
handler identity no longer tears down the EditorView, which reset the
cursor to the document start and made typing appear right-to-left.

* feat(web): add unsaved changes confirmation dialog in PlaygroundMessageEditor

Implement a confirmation dialog to warn users about unsaved changes when attempting to leave the editor. This includes handling the beforeunload event to prevent accidental navigation away from the editor. Additionally, add tests to verify the dialog's behavior under various scenarios.

* test(web): cover beforeunload guard and fade hydration suppression

Address review feedback: add regression tests for the unsaved-changes
beforeunload guard and the first-render fade suppression of hydrated
content, and annotate getCachedMarkdown's return type.
2026-08-18 18:20:53 +08:00

721 lines
17 KiB
CSS

/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
@import 'tailwindcss';
@import 'tw-animate-css';
@import 'shadcn/tailwind.css';
@import '@fontsource-variable/public-sans';
/* Editorial serif (Lora) backing the `serif` font axis and the Anthropic
* preset's default typography. See `--font-serif` in theme.css for the
* full Latin + CJK fallback stack and `theme-presets.css` for the cascade
* that activates it. Loaded globally so font-switching is instantaneous
* with no FOUT once the variable is fetched. */
@import '@fontsource-variable/lora';
@import './theme.css';
@import './theme-presets.css';
/* Shiki dual themes: token colors follow dark theme (pre background stays `bg-background` on the block) */
@layer components {
.shiki span {
color: var(--shiki-light) !important;
font-style: var(--shiki-light-font-style) !important;
font-weight: var(--shiki-light-font-weight) !important;
text-decoration: var(--shiki-light-text-decoration) !important;
}
.dark .shiki span {
color: var(--shiki-dark) !important;
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
.json-code-editor-textarea {
overflow: auto !important;
scrollbar-gutter: stable;
}
.json-code-editor-textarea::placeholder {
color: var(--muted-foreground);
opacity: 1;
-webkit-text-fill-color: var(--muted-foreground);
}
.json-code-editor-textarea,
.json-code-editor-highlight,
.json-code-editor-lines {
overflow-wrap: normal !important;
white-space: pre !important;
}
.json-code-editor-highlight .yace-tok--str {
color: var(--chart-5);
}
.json-code-editor-highlight .yace-tok--num {
color: var(--chart-4);
}
.json-code-editor-highlight .yace-tok--kw {
color: var(--chart-1);
}
.json-code-editor-highlight .yace-tok--punc {
color: var(--muted-foreground);
}
.json-code-editor-highlight .yace-tok--com {
color: var(--muted-foreground);
}
}
@layer base {
* {
@apply border-border outline-ring/50;
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
html {
@apply overflow-x-hidden font-sans;
}
body {
@apply bg-background text-foreground has-[div[data-variant='inset']]:bg-sidebar min-h-svh w-full;
/* Font is driven by the theme's font axis via `--font-body`
* (defined in theme.css, swapped by `[data-theme-font='...']` blocks
* in theme-presets.css). Defaults to the project's humanist sans. */
font-family: var(--font-body);
}
/* Keep sticky headers stable while primitives lock body scrolling. */
body[data-scroll-locked] {
overflow: unset !important;
}
/* Cursor pointer for buttons */
button:not(:disabled),
[role='button']:not(:disabled) {
cursor: pointer;
}
/* Prevent focus zoom on mobile devices */
@media screen and (max-width: 767px) {
input,
select,
textarea {
font-size: 16px !important;
}
}
}
/* Vercel Geist-style skeleton shimmer */
.skeleton-shimmer {
background: linear-gradient(
90deg,
var(--skeleton-base) 0%,
var(--skeleton-base) 33%,
var(--skeleton-highlight) 50%,
var(--skeleton-base) 67%,
var(--skeleton-base) 100%
);
background-size: 300% 100%;
animation: skeleton-shimmer 1.8s ease-in-out infinite;
}
@keyframes skeleton-shimmer {
0% {
background-position: 100% 50%;
}
100% {
background-position: -100% 50%;
}
}
/* Override auto-skeleton-react inline styles with Vercel shimmer */
.auto-skeleton-fade [class^='skeleton-'] {
background: linear-gradient(
90deg,
var(--skeleton-base) 0%,
var(--skeleton-base) 33%,
var(--skeleton-highlight) 50%,
var(--skeleton-base) 67%,
var(--skeleton-base) 100%
) !important;
background-size: 300% 100% !important;
animation: skeleton-shimmer 1.8s ease-in-out infinite !important;
}
@media (prefers-reduced-motion: reduce) {
.skeleton-shimmer,
.auto-skeleton-fade [class^='skeleton-'] {
animation: none !important;
background: var(--skeleton-base) !important;
}
}
@utility container {
margin-inline: auto;
padding-inline: 2rem;
}
@utility max-w-container {
max-width: 1280px;
}
@utility max-w-container-lg {
max-width: 1536px;
}
@utility no-scrollbar {
/* Hide scrollbar for Chrome, Safari and Opera */
&::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
/* Tooltip content can still scroll, but should not show a distracting axis. */
[data-slot='tooltip-content'],
[data-slot='tooltip-content'] * {
-ms-overflow-style: none;
scrollbar-width: none;
}
[data-slot='tooltip-content']::-webkit-scrollbar,
[data-slot='tooltip-content'] *::-webkit-scrollbar {
display: none;
}
@utility hover-scrollbar {
/* Hide scrollbar by default */
scrollbar-width: thin;
scrollbar-color: transparent transparent;
/* Show scrollbar on hover */
&:hover {
scrollbar-color: var(--border) transparent;
}
/* Webkit browsers (Chrome, Safari, Edge) */
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: 4px;
}
&:hover::-webkit-scrollbar-thumb {
background-color: var(--border);
}
&:hover::-webkit-scrollbar-thumb:hover {
background-color: color-mix(in oklch, var(--border) 60%, transparent);
}
}
@utility faded-bottom {
@apply after:pointer-events-none after:absolute after:start-0 after:bottom-0 after:hidden after:h-32 after:w-full after:rounded-b-2xl after:bg-[linear-gradient(180deg,_transparent_10%,_var(--background)_70%)] md:after:block;
}
.CollapsibleContent {
overflow: hidden;
}
@media (prefers-reduced-motion: no-preference) {
.CollapsibleContent[data-open] {
animation: slideDown 300ms ease-out;
}
.CollapsibleContent:not([data-open]) {
animation: slideUp 300ms ease-out;
}
}
@keyframes slideDown {
from {
height: 0;
}
to {
height: var(--collapsible-panel-height);
}
}
@keyframes slideUp {
from {
height: var(--collapsible-panel-height);
}
to {
height: 0;
}
}
/* Launch UI Animations and Effects - Matching Template Exactly */
@layer utilities {
/* Gradient utilities */
.bg-radial {
background-image: radial-gradient(var(--tw-gradient-stops));
}
/* Glass morphism effects - matching Launch UI template */
.glass-1 {
@apply border-border from-card/80 to-card/40 dark:border-border/10 dark:border-b-border/5 dark:border-t-border/20 dark:from-card/5 dark:to-card/0 border bg-linear-to-b;
}
.glass-2 {
@apply border-border from-card/100 to-card/80 dark:border-border/10 dark:border-b-border/5 dark:border-t-border/20 dark:from-card/10 dark:to-card/5 border bg-linear-to-b;
}
.glass-3 {
@apply border-border from-card/30 to-card/20 dark:border-border/10 dark:border-t-border/20 dark:border-b-border/5 dark:from-primary/5 dark:to-primary/2 border bg-linear-to-b;
}
.glass-4 {
@apply border-border border-b-input/90 from-card/60 to-card/20 dark:border-border/10 dark:border-t-border/30 dark:from-primary/10 dark:to-primary/5 border bg-linear-to-b dark:border-b-0;
}
.glass-5 {
@apply border-border border-b-input from-card/100 to-card/20 dark:border-border/10 dark:border-t-border/30 dark:from-primary/15 dark:to-primary/5 border bg-linear-to-b dark:border-b-0;
}
/* Fade effects - matching Launch UI template */
.fade-x {
mask-image: linear-gradient(
to right,
transparent 0%,
black 25%,
black 75%,
transparent 100%
);
}
.fade-y {
mask-image: linear-gradient(
to top,
transparent 0%,
black 25%,
black 75%,
transparent 100%
);
}
.fade-top {
mask-image: linear-gradient(to bottom, transparent 0%, black 35%);
}
.fade-bottom {
mask-image: linear-gradient(to top, transparent 0%, black 35%);
}
.fade-top-lg {
mask-image: linear-gradient(to bottom, transparent 15%, black 100%);
}
.fade-bottom-lg {
mask-image: linear-gradient(to top, transparent 15%, black 100%);
}
.fade-left {
mask-image: linear-gradient(to right, transparent 0%, black 35%);
}
.fade-right {
mask-image: linear-gradient(to left, transparent 0%, black 35%);
}
.fade-left-lg {
mask-image: linear-gradient(to right, transparent 15%, black 100%);
}
.fade-right-lg {
mask-image: linear-gradient(to left, transparent 15%, black 100%);
}
/* Animations */
.animate-appear {
animation: appear 0.6s forwards ease-out;
}
.animate-appear-zoom {
animation: appear-zoom 0.6s forwards ease-out;
}
.animation-delay-100 {
animation-delay: 100ms;
}
.animation-delay-300 {
animation-delay: 300ms;
}
.animation-delay-700 {
animation-delay: 700ms;
}
.animation-delay-1000 {
animation-delay: 1000ms;
}
@keyframes appear {
0% {
opacity: 0;
transform: translateY(1rem);
filter: blur(0.5rem);
}
50% {
filter: blur(0);
}
100% {
opacity: 1;
transform: translateY(0);
filter: blur(0);
}
}
@keyframes appear-zoom {
0% {
opacity: 0;
transform: scale(0.5);
}
100% {
opacity: 1;
transform: scale(1);
}
}
/* Vertical scroll animation for icon lists */
@keyframes scroll-up {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-50%);
}
}
.animate-scroll-up {
animation: scroll-up 20s linear infinite;
will-change: transform;
}
.animate-scroll-down {
animation: scroll-up 20s linear infinite reverse;
will-change: transform;
}
/* Pause animation on hover */
.scroll-container:hover .animate-scroll-up,
.scroll-container:hover .animate-scroll-down {
animation-play-state: paused;
}
}
@media (prefers-reduced-motion: reduce) {
.animate-appear,
.animate-appear-zoom,
.animate-scroll-up,
.animate-scroll-down,
[data-slot='table'] tbody tr {
animation: none !important;
opacity: 1;
transform: none;
}
}
/* Smooth streaming: one-shot fade-in on newly streamed words.
* Duration must match FADE_DURATION_MS in response-fade.ts */
@keyframes stream-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
[data-stream-fade] {
animation: stream-fade-in 250ms ease-out both;
animation-delay: var(--stream-fade-delay, 0ms);
}
@media (prefers-reduced-motion: reduce) {
[data-stream-fade] {
animation: none;
}
}
/* ── Landing page scroll-triggered animations ── */
@keyframes landing-fade-up {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes landing-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes landing-scale-in {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes landing-fade-left {
from {
opacity: 0;
transform: translateX(24px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes landing-fade-right {
from {
opacity: 0;
transform: translateX(-24px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.landing-animate-fade-up {
animation: landing-fade-up 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.landing-animate-fade-in {
animation: landing-fade-in 0.6s ease-out both;
}
.landing-animate-scale-in {
animation: landing-scale-in 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.landing-animate-fade-left {
animation: landing-fade-left 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.landing-animate-fade-right {
animation: landing-fade-right 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
}
@media (prefers-reduced-motion: reduce) {
.landing-animate-fade-up,
.landing-animate-fade-in,
.landing-animate-scale-in,
.landing-animate-fade-left,
.landing-animate-fade-right {
animation: none !important;
opacity: 1 !important;
transform: none !important;
}
}
/* Micro-interactions — Vercel-style subtle hover/active feedback */
@media (prefers-reduced-motion: no-preference) {
[data-slot='card']:not([data-card-hover='false']) {
transition:
transform 150ms ease,
box-shadow 150ms ease;
}
@media (min-width: 641px) {
[data-slot='card']:not([data-card-hover='false']):hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgb(0 0 0 / 0.06);
}
.dark [data-slot='card']:not([data-card-hover='false']):hover {
box-shadow: 0 4px 12px rgb(0 0 0 / 0.3);
}
}
button:not(:disabled):active,
[role='button']:not(:disabled):active {
transform: scale(0.98);
}
[data-slot='table'] tr {
transition: background-color 120ms ease;
}
/* Table row stagger-in animation */
[data-slot='table'] tbody tr {
animation: tableRowEnter 0.2s cubic-bezier(0.33, 1, 0.68, 1) both;
}
[data-slot='table'] tbody tr:nth-child(2) {
animation-delay: 25ms;
}
[data-slot='table'] tbody tr:nth-child(3) {
animation-delay: 50ms;
}
[data-slot='table'] tbody tr:nth-child(4) {
animation-delay: 75ms;
}
[data-slot='table'] tbody tr:nth-child(5) {
animation-delay: 100ms;
}
[data-slot='table'] tbody tr:nth-child(6) {
animation-delay: 125ms;
}
[data-slot='table'] tbody tr:nth-child(7) {
animation-delay: 150ms;
}
[data-slot='table'] tbody tr:nth-child(8) {
animation-delay: 175ms;
}
[data-slot='table'] tbody tr:nth-child(9) {
animation-delay: 200ms;
}
[data-slot='table'] tbody tr:nth-child(10) {
animation-delay: 225ms;
}
[data-slot='table'] tbody tr:nth-child(n + 11) {
animation-delay: 250ms;
}
}
@keyframes tableRowEnter {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Tabular numbers for data-heavy contexts */
[data-slot='table'] td,
.tabular-nums {
font-variant-numeric: tabular-nums;
}
/* Long list rendering optimization */
.content-auto {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}
/* ── Hero terminal demo animations ── */
@keyframes terminal-demo-blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.terminal-demo-blink {
animation: terminal-demo-blink 1s step-end infinite;
}
@keyframes terminal-demo-spin {
to {
transform: rotate(360deg);
}
}
.terminal-demo-spin {
animation: terminal-demo-spin 0.8s linear infinite;
}
@keyframes terminal-demo-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.3;
}
}
.terminal-demo-pulse {
animation: terminal-demo-pulse 1.5s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.terminal-demo-blink,
.terminal-demo-spin,
.terminal-demo-pulse {
animation: none !important;
}
}
/* ── Auto group flowing border ── */
@property --auto-group-flow-angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
@keyframes auto-group-flow-border-travel {
to {
--auto-group-flow-angle: 360deg;
}
}
/* Border-only effect: the conic gradient covers the whole layer, but the
* two-layer mask (content-box XOR full box) keeps only a `padding`-wide
* ring visible, so the highlight hugs the rounded perimeter. Animating
* the gradient's start angle makes the bright segment travel around all
* four edges and corners without touching the interior. */
.auto-group-flow-border {
padding: 1.5px;
border-radius: inherit;
background: conic-gradient(
from var(--auto-group-flow-angle),
transparent 0deg,
transparent 240deg,
color-mix(in oklch, var(--primary) 45%, transparent) 300deg,
var(--primary) 342deg,
transparent 360deg
);
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
animation: auto-group-flow-border-travel 3.2s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.auto-group-flow-border {
display: none;
}
}