Files
petal/web/src/index.css
prosolis 82e2bcc777 Suggestions: right-margin comment rail + Mandarin explanations
Surface every outstanding suggestion as a card in the right-hand
whitespace, vertically aligned to the text it flags — so the writer sees
the whole queue at once instead of hovering each highlight. Cards stack
with collision avoidance, link both ways with their highlight (hover/click
↔ soft text wash, driven through the decoration plugin so it survives
edit repaints), and carry the same Accept / Dismiss / Ask Petal actions.
The rail is a progressive enhancement: it mounts only when there's room
beside the editor, otherwise the existing inline hover card is unchanged.
Stacked cards that reach the bottom-right corner tuck behind the
companion mascot (z-order).

When a card is expanded, the Ask Petal bubble now opens with the
Simplified-Chinese translation of the explanation (the English stays in
the card body) instead of repeating the same text twice — a new
POST /api/suggestions/{id}/translate one-shot LLM endpoint, loaded
lazily on open with an English fallback.

Verified live against the local LLM via the uitest harness: rail
stacking, hover↔text wash, expand/Ask Petal, accept-from-rail, narrow
fallback, and the Mandarin bubble.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-26 12:23:02 -07:00

606 lines
19 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.
@import "tailwindcss";
/* Petal design tokens — soft, warm, bubbly stationery aesthetic.
Defined in @theme so they generate Tailwind utilities (bg-bg, text-plum,
rounded-card, font-ui, etc.). See petal-spec.md → Design System. */
@theme {
/* Palette */
--color-bg: #FDF6F0; /* warm cream canvas */
--color-surface: #FFFFFF; /* card/panel surfaces */
--color-surface-alt: #FFF0F5; /* rose-tinted alt surface */
--color-border: #F0E0EB; /* soft pink-grey border */
--color-plum: #3D2E39; /* warm dark plum text (not harsh black) */
--color-muted: #9B8CA3; /* muted lavender-grey */
--color-accent: #E8A0BF; /* soft rose — primary interactive */
--color-accent-hover: #D98AAF;/* deeper rose on hover */
/* Suggestion type colors */
--color-mint: #A8D8C8; /* grammar */
--color-peach: #F4B8A0; /* phrasing */
--color-lavender: #C5B4E8; /* idiom */
--color-sky: #A8CCE8; /* clarity */
--color-honey: #CE9B4F; /* voice */
--color-success: #8FCFA8; /* saved / accepted */
/* Typography */
--font-ui: "Nunito", "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
--font-body: "Lora", Georgia, serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
/* Shape language */
--radius-card: 16px;
--radius-modal: 24px;
--radius-pill: 999px;
--radius-input: 12px;
/* Soft, warm, lifted shadow */
--shadow-soft: 0 4px 20px rgba(180, 130, 160, 0.12);
}
html, body, #root {
height: 100%;
}
body {
margin: 0;
background: var(--color-bg);
color: var(--color-plum);
font-family: var(--font-ui);
-webkit-font-smoothing: antialiased;
}
/* Gentle, consistent motion across interactive elements */
button, a, input {
transition: all 200ms ease;
}
/* Editor body — serif Lora for prose, warm plum ink, roomy line height. The
title and headings use the Nunito UI face for contrast. */
.petal-prose {
font-family: var(--font-body);
font-size: 1.125rem;
line-height: 1.75;
color: var(--color-plum);
}
.petal-prose > * + * {
margin-top: 0.9em;
}
.petal-prose h1,
.petal-prose h2,
.petal-prose h3 {
font-family: var(--font-ui);
font-weight: 800;
line-height: 1.3;
}
.petal-prose h1 { font-size: 1.6em; }
.petal-prose h2 { font-size: 1.3em; }
.petal-prose h3 { font-size: 1.1em; }
.petal-prose ul,
.petal-prose ol {
padding-left: 1.4em;
}
.petal-prose ul { list-style: disc; }
.petal-prose ol { list-style: decimal; }
.petal-prose a {
color: var(--color-accent-hover);
text-decoration: underline;
}
.petal-prose blockquote {
border-left: 3px solid var(--color-border);
padding-left: 1em;
color: var(--color-muted);
font-style: italic;
}
.petal-prose code {
font-family: var(--font-mono);
font-size: 0.9em;
background: var(--color-surface-alt);
padding: 0.1em 0.35em;
border-radius: 6px;
}
/* Top formatting bar: a single line at rest so it stays slim, expanding to
reveal every control on hover (or while a control inside it has focus, e.g.
the link input or an open popover). The clipped right edge fades out as a hint
that more is available. */
.petal-toolbar {
flex-wrap: nowrap;
overflow: hidden;
max-width: 100%;
-webkit-mask-image: linear-gradient(to right, #000 90%, transparent 100%);
mask-image: linear-gradient(to right, #000 90%, transparent 100%);
}
.petal-toolbar:hover,
.petal-toolbar:focus-within {
flex-wrap: wrap;
overflow: visible;
-webkit-mask-image: none;
mask-image: none;
}
/* Highlighter mark — inline background swatch behind the text. */
.petal-prose mark {
border-radius: 4px;
padding: 0.05em 0.15em;
/* color comes from the inline style the Highlight extension writes */
}
/* Inserted images: rounded, soft-shadowed, never wider than the column. The
selected-node ring matches the rest of the rose theme. Scoped to the editor's
own <img> (not a class) since Tiptap doesn't always carry the configured
class through to the rendered node. */
.petal-prose img {
max-width: 100%;
height: auto;
border-radius: var(--radius-card);
box-shadow: var(--shadow-soft);
}
.petal-prose img.ProseMirror-selectednode {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
/* Tables: soft pink grid, rounded outer corners, a tinted header row. Targeted
by element within the editor — Tiptap renders the <table> without our
configured class, so a `.petal-table` selector would never match. The wrapper
Tiptap adds (.tableWrapper) handles horizontal overflow scroll. */
.petal-prose .tableWrapper {
overflow-x: auto;
}
.petal-prose table {
border-collapse: collapse;
width: 100%;
margin: 0.4em 0;
overflow: hidden;
border-radius: var(--radius-input);
border: 1px solid var(--color-border);
table-layout: fixed;
}
.petal-prose td,
.petal-prose th {
border: 1px solid var(--color-border);
padding: 0.4em 0.6em;
vertical-align: top;
position: relative;
min-width: 3em;
}
.petal-prose th {
background: var(--color-surface-alt);
font-weight: 700;
text-align: left;
}
/* The cell being edited / the active selection inside a table. */
.petal-prose .selectedCell::after {
content: '';
position: absolute;
inset: 0;
background: var(--color-surface-alt);
opacity: 0.5;
pointer-events: none;
}
/* The column-resize handle Tiptap draws between columns. */
.petal-prose .column-resize-handle {
position: absolute;
right: -2px;
top: 0;
bottom: -2px;
width: 4px;
background: var(--color-accent);
pointer-events: none;
}
/* Placeholder shown on the empty first paragraph (Tiptap Placeholder ext). */
.petal-prose p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
color: var(--color-muted);
float: left;
height: 0;
pointer-events: none;
}
/* --- Suggestion decorations -------------------------------------------------
Inline highlights anchored by string at render time (not stored marks). Each
type gets a soft underline in its palette color; hovering opens its card. */
.petal-suggestion {
border-bottom: 2px solid transparent;
border-radius: 2px;
cursor: pointer;
transition: background 200ms ease;
/* gentle fade + slight upward float as decorations appear */
animation: petal-suggestion-in 260ms ease both;
}
.petal-suggestion:hover {
background: var(--color-surface-alt);
}
.petal-suggestion-grammar { border-bottom-color: var(--color-mint); }
.petal-suggestion-phrasing { border-bottom-color: var(--color-peach); }
.petal-suggestion-idiom { border-bottom-color: var(--color-lavender); }
.petal-suggestion-clarity { border-bottom-color: var(--color-sky); }
.petal-suggestion-voice { border-bottom-color: var(--color-honey); }
@keyframes petal-suggestion-in {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.petal-suggestion-card {
animation: petal-suggestion-in 200ms ease both;
}
/* Text emphasized from its margin card (or a hover) — a soft wash so the
card↔text link reads at a glance, both directions. */
.petal-suggestion-active {
background: var(--color-surface-alt);
box-shadow: 0 0 0 3px var(--color-surface-alt);
border-radius: 3px;
}
/* --- Suggestion margin rail -------------------------------------------------
The right-hand "comment column": every outstanding suggestion as a card,
vertically aligned to the text it flags, so the whole queue is visible at a
glance instead of one-highlight-at-a-time on hover. It floats in the whitespace
just right of the editor column and is only mounted when there's room for it
(EditorCore measures the gap). Cards are absolutely positioned by a resolved
top (anchor + collision avoidance), so the container only anchors the column. */
.petal-rail {
position: absolute;
top: 0;
left: 100%;
margin-left: 32px;
width: 300px;
/* Below the companion mascot (z-40): where a stacked card reaches the
bottom-right corner it simply tucks behind the sleeping cat. */
z-index: 10;
}
.petal-rail-card {
position: absolute;
left: 0;
right: 0;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-left: 3px solid var(--color-border); /* type color set inline */
border-radius: var(--radius-card);
box-shadow: var(--shadow-soft);
padding: 0.7rem 0.85rem;
/* `top` eases so re-stacking (accept/dismiss/edit) glides instead of jumping.
The entrance uses fill `backwards` so its translateY doesn't linger and fight
the active-state transform once it's done. */
transition: top 240ms cubic-bezier(0.2, 0.7, 0.3, 1), box-shadow 200ms ease,
transform 200ms ease, border-color 200ms ease;
animation: petal-suggestion-in 220ms ease backwards;
}
.petal-rail-card-active {
transform: translateX(-5px);
box-shadow: 0 8px 26px rgba(180, 130, 160, 0.24);
border-top-color: var(--color-accent);
border-right-color: var(--color-accent);
border-bottom-color: var(--color-accent);
}
.petal-rail-x {
color: var(--color-muted);
font-size: 0.72rem;
line-height: 1;
padding: 2px 5px;
border-radius: var(--radius-pill);
}
.petal-rail-x:hover {
background: var(--color-surface-alt);
color: var(--color-plum);
}
/* --- Find & Replace ---------------------------------------------------------
In-document search (Ctrl/Cmd+F). Every match gets a soft honey wash; the
current match is brighter with a rose ring so it stands out as you step
through. Decorations, like every other highlight layer here. */
.petal-find-match {
background: var(--color-highlight, #fff1a8);
border-radius: 3px;
box-shadow: 0 0 0 1px var(--color-border);
}
.petal-find-match-active {
background: var(--color-peach);
box-shadow: 0 0 0 2px var(--color-accent);
}
/* --- Spell check ------------------------------------------------------------
Browser-side nspell flags misspellings with a soft rose wavy underline (a
gentler take on the classic red squiggle, to fit the pastel palette). Like the
suggestion layer these are ProseMirror decorations, never stored marks.
Clicking a flagged word opens its MisspellCard with corrections. */
.petal-misspelling {
cursor: pointer;
text-decoration: underline wavy var(--color-accent);
text-decoration-skip-ink: none;
text-underline-offset: 2px;
}
.petal-misspelling:hover {
background: var(--color-surface-alt);
border-radius: 2px;
}
.petal-misspell-card {
animation: petal-suggestion-in 200ms ease both;
}
/* --- Word lookup popover ----------------------------------------------------
Right-click a word for its dictionary definition and synonyms. Same soft card
treatment as the spelling popover; synonym pills swap the word on click. */
.petal-word-card {
animation: petal-suggestion-in 200ms ease both;
}
/* --- ESL superpowers (Phase 9) ----------------------------------------------
Inline Chinese gloss on hover (a small dark tooltip under the word), and the
selection rewrite bubble + its preview card. All share the soft pop-in. The
gloss tip fades in a touch faster — it's a fleeting reading aid, not a panel. */
.petal-gloss-tip {
animation: petal-suggestion-in 140ms ease both;
}
.petal-selection-bubble,
.petal-rewrite-card {
animation: petal-suggestion-in 200ms ease both;
}
/* --- Accept confetti --------------------------------------------------------
A tiny CSS-only burst played where a suggestion is accepted: four colored
dots spray up-and-out, then fade. No JS animation — each dot reads its
direction from --dx/--dy custom properties set inline. (Spec → Signature.) */
.petal-confetti {
width: 0;
height: 0;
}
.petal-confetti-dot {
position: absolute;
left: 0;
top: 0;
width: 7px;
height: 7px;
border-radius: var(--radius-pill);
animation: petal-confetti 680ms cubic-bezier(0.2, 0.7, 0.3, 1) forwards;
}
@keyframes petal-confetti {
0% { transform: translate(0, 0) scale(0.3); opacity: 0; }
18% { opacity: 1; }
100% { transform: translate(var(--dx), var(--dy)) scale(1); opacity: 0; }
}
/* --- Distraction-free mode ---------------------------------------------------
The doc-list sidebar slides left and collapses to zero width; the editor
canvas (centered, max-width) re-centers into the full pane. Width + transform
animate together for a smooth slide. (Spec → Distraction-free mode.) */
.petal-sidebar {
width: 260px;
overflow: hidden;
transition: width 280ms ease, transform 280ms ease, opacity 200ms ease;
}
.petal-sidebar-hidden {
width: 0;
transform: translateX(-24px);
opacity: 0;
pointer-events: none;
}
/* --- Companion kitten -------------------------------------------------------
The cozy corner mascot. Gently bobs while awake, settles and sways slowly
while napping; its speech bubble pops in; little zzz drift up when asleep. */
.petal-companion {
/* Mascot size scales with the viewport width: ~original on a laptop, up to
~2× on a large desktop. Tune the middle (vw) term to taste. */
--petal-companion-size: clamp(10rem, 17vw, 20rem);
animation: petal-bob 3.2s ease-in-out infinite;
transition: transform 200ms ease;
}
/* The Lottie art sits inside the round badge with a little breathing room. */
.petal-companion-art {
width: 88%;
height: 88%;
}
.petal-companion:hover {
transform: translateY(-2px) scale(1.04);
}
.petal-companion-sleep {
animation: petal-bob-slow 5s ease-in-out infinite;
}
@keyframes petal-bob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
@keyframes petal-bob-slow {
0%, 100% { transform: translateY(0) rotate(-1deg); }
50% { transform: translateY(-2px) rotate(1deg); }
}
.petal-bubble {
animation: petal-bubble-in 240ms cubic-bezier(0.2, 0.8, 0.3, 1.2) both;
transform-origin: bottom right;
}
@keyframes petal-bubble-in {
from { opacity: 0; transform: translateY(6px) scale(0.92); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.petal-zzz {
top: calc(var(--petal-companion-size) * 0.04);
right: calc(var(--petal-companion-size) * 0.1);
font-weight: 800;
font-size: calc(var(--petal-companion-size) * 0.12);
animation: petal-zzz 2.4s ease-in-out infinite;
}
@keyframes petal-zzz {
0% { opacity: 0; transform: translateY(0) scale(0.8); }
30% { opacity: 0.9; }
100% { opacity: 0; transform: translateY(-14px) scale(1.1); }
}
/* "New version available" banner — drifts down from the top like a petal. */
.petal-update {
animation: petal-drop 360ms cubic-bezier(0.2, 0.8, 0.3, 1.2) both;
}
@keyframes petal-drop {
from { opacity: 0; transform: translateY(-14px) scale(0.96); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* Blinking caret in the Ask Petal bubble while awaiting the first token. */
.petal-chat-caret {
animation: petal-breathe 1s ease-in-out infinite;
color: var(--color-accent);
}
/* Breathing rose dot shown in the StatusBar while a checkpoint runs. */
.petal-checkpoint-dot {
animation: petal-breathe 2s ease-in-out infinite;
}
@keyframes petal-breathe {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
/* --- Organization & search (Phase 10) --------------------------------------
Tag chips, the search results list, and per-row affordances. The row actions
(tag / delete) fade in on hover for pointer users but are always visible on
touch (no hover) — see the coarse-pointer block below. */
.petal-tag-chip {
transition: background 160ms ease, color 160ms ease, transform 160ms ease;
}
.petal-search-results {
animation: petal-suggestion-in 160ms ease both;
}
.petal-tag-picker {
animation: petal-suggestion-in 160ms ease both;
}
.petal-row-action {
opacity: 0;
transition: opacity 160ms ease, background 160ms ease;
}
.group:hover .petal-row-action,
.petal-row-action:focus-visible {
opacity: 1;
}
.petal-row-action:hover {
background: var(--color-surface-alt);
}
/* Clamp a search snippet to two lines. */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* --- Touch & tablet polish --------------------------------------------------
On coarse pointers (tablets, phones) there's no hover, so make tap targets
comfortable and reveal affordances that would otherwise be hover-only. */
@media (pointer: coarse) {
.petal-tap {
min-height: 44px;
}
.petal-tap-sm {
min-height: 36px;
}
/* Row actions can't rely on hover — keep them visible and roomy. */
.petal-row-action {
opacity: 1;
height: 36px;
width: 36px;
}
/* Bigger, easier-to-hit suggestion/word/tag pills. */
.petal-tag-chip {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
}
/* --- Responsive sidebar (narrow screens) ------------------------------------
Below the tablet breakpoint the sidebar becomes an overlay drawer toggled by a
hamburger in the header, instead of a permanent column. A scrim sits behind it.
On wide screens the toggle + scrim are hidden and the sidebar is in-flow. */
.petal-sidebar-toggle {
display: none;
}
.petal-scrim {
display: none;
}
@media (max-width: 768px) {
.petal-sidebar-toggle {
display: inline-flex;
}
.petal-sidebar {
position: fixed;
top: 48px; /* below the 12-height header */
bottom: 0;
left: 0;
z-index: 30;
background: var(--color-bg);
box-shadow: var(--shadow-soft);
}
/* On mobile the sidebar is hidden by default; the drawer-open class slides it
in. (Distraction-free's petal-sidebar-hidden still wins to keep it closed.) */
.petal-sidebar:not(.petal-drawer-open) {
width: 0;
transform: translateX(-24px);
opacity: 0;
pointer-events: none;
}
.petal-scrim.petal-scrim-show {
display: block;
position: fixed;
inset: 48px 0 0 0;
z-index: 20;
background: rgba(61, 46, 57, 0.18);
}
}
/* Print / Save-as-PDF: strip every bit of app chrome and editing decoration so
only the title and the writing itself reach the page. This is Petal's PDF
path — it uses the browser's own fonts, so CJK renders correctly with no
server-side font embedding. */
/* --- Ambient falling petals -------------------------------------------------
The WebGL petal layer is a fixed, non-interactive overlay that drifts soft
blossoms over the page. Kept gentle (low opacity) so it never competes with
the writing. It hides itself in print and is skipped entirely for users who
prefer reduced motion (the component renders nothing in that case). */
.petal-fall {
opacity: 1;
}
/* Honor reduced-motion globally: still the looping ambient animations and drop
the petal layer. One-shot entrance/feedback animations are left intact. */
@media (prefers-reduced-motion: reduce) {
.petal-fall {
display: none !important;
}
.petal-bob,
.petal-bob-slow,
.petal-zzz,
.petal-checkpoint-dot {
animation: none !important;
}
}
@media print {
.petal-fall,
.petal-no-print,
.petal-sidebar,
.petal-suggestion-card,
.petal-misspell-card,
.petal-gloss-tip,
.petal-selection-bubble,
.petal-rewrite-card,
.petal-confetti,
.petal-companion {
display: none !important;
}
html, body, #root {
height: auto !important;
overflow: visible !important;
background: #fff !important;
}
/* Drop the in-editor highlight underlines/tints — they're guidance, not ink. */
.petal-suggestion,
.petal-misspelling {
background: none !important;
text-decoration: none !important;
border-bottom: none !important;
}
/* Let the writing use the full page width. */
.max-w-\[720px\] { max-width: none !important; }
}