d3ro-voice/site/src/index.css
Yun Chan 7b1210b5d1 feat(desktop): honest, unified dictation popups with Esc to cancel
Wave 3 of the 2026-09-28 design overhaul.

- Esc (no modifiers) cancels an in-progress dictation, recording or
  processing: VoiceModeService.requestEscape() queues the dead 'escape'
  action. Clicking the capsule cancels only while recording, so a stray
  click during processing no longer throws the text away.
- Errors show in the user's language: bootstrap now passes error.code to
  the tip (the translated branch never ran), and one core classifier
  (voice-error-message) feeds both the tip and the main-window alert.
- Capsule: bars stay flat without input (no fake "alive" wobble before the
  mic opens), an indeterminate bar replaces the made-up percentage, the
  partial transcript stays visible while processing, an Esc hint, and a
  brief "Inserted" state so success is visible.
- All six popups share popups/_shared (Pretendard, one panel radius,
  selection, kbd hints, reduced motion). History/command popups get
  titles, listbox roles and window sizes from popup-list-geometry (empty
  states were clipped); the command popup's "0" key is registered;
  result popup explains why it appeared and can be closed; captions keep
  a theme-independent dark backing on purpose.
- Popup themes follow the OS when set to system and the document lang
  follows the app language.
- Site hero demo mirrors the new capsule (done state, indeterminate bar,
  Esc hint; 10 locales).
2026-09-28 20:46:24 +09:00

295 lines
7.5 KiB
CSS

@tailwind base;
@tailwind components;
@tailwind utilities;
/* ── Base ─────────────────────────────────────────────── */
@layer base {
html {
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
@apply bg-surface-950 font-sans text-neutral-100;
}
body {
overflow-x: hidden;
}
::selection {
@apply bg-brand-blue/35 text-white;
}
:focus-visible {
@apply outline outline-2 outline-offset-2 outline-brand-blue-light;
}
/* 고정 헤더(64px)가 앵커 대상의 머리를 가리지 않게 한다. */
[id] {
scroll-margin-top: 5rem;
}
/* 의미적 강조는 세미볼드까지 — 볼드(700) 금지 (design.md 타이포 원칙 1) */
strong, b { font-weight: 600; }
[hidden] { display: none !important; }
}
/* ── Components ───────────────────────────────────────── */
@layer components {
.section {
@apply py-20 md:py-28;
}
/* 버튼: 최소 44px 터치 타깃. 흰 글자 대비 4.5:1 이상을 위해 채움색은 blue-dark 계열. */
.btn {
@apply inline-flex min-h-[44px] flex-wrap items-center justify-center gap-x-2 gap-y-1 rounded-xl px-5 py-2 text-center text-base font-medium leading-snug transition-colors duration-150;
}
.btn-sm {
@apply min-h-[40px] px-4 text-sm;
}
.btn-lg {
@apply min-h-[52px] px-7;
}
.btn-primary {
@apply bg-brand-blue-dark text-white hover:bg-brand-blue-deep;
}
.btn-secondary {
@apply border border-white/15 bg-surface-800 text-neutral-100 hover:border-white/30 hover:bg-surface-700;
}
.link {
@apply text-brand-blue-light underline decoration-brand-blue-light/40 underline-offset-4 transition-colors hover:decoration-brand-blue-light;
}
.kbd {
@apply inline-flex min-h-[32px] items-center rounded-md border border-white/15 border-b-white/25 bg-surface-700 px-2.5 font-sans text-sm text-neutral-100;
}
.kbd-accent {
@apply border-brand-blue/50 border-b-brand-blue bg-brand-blue/15 text-brand-blue-light;
}
.skip-link {
@apply fixed left-4 top-3 z-[60] -translate-y-24 rounded-lg bg-surface-800 px-4 py-3 text-sm text-white shadow-chassis focus:translate-y-0;
}
}
/* ── 히어로 예시: 입력 대상 창 ─────────────────────────── */
.demo-window {
position: relative;
overflow: hidden;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: #0f1116;
box-shadow: 0 24px 48px -12px rgba(0, 0, 0, 0.6);
}
.demo-titlebar {
display: flex;
align-items: center;
gap: 8px;
height: 36px;
padding: 0 14px;
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
background: #151821;
font-size: 12px;
color: #a1a1aa;
}
.demo-app-icon {
width: 12px;
height: 12px;
border-radius: 3px;
background: #3b82f6;
}
.demo-win-glyph {
font-size: 11px;
line-height: 1;
}
.demo-doc {
min-height: 236px;
padding: 22px 22px 108px;
font-size: 15px;
line-height: 1.7;
}
.demo-typed {
border-radius: 3px;
animation: demo-paste 1.6s ease-out;
}
@keyframes demo-paste {
0% { background: rgba(59, 130, 246, 0.35); }
100% { background: transparent; }
}
.demo-caret {
display: inline-block;
width: 2px;
height: 1.15em;
margin-left: 1px;
vertical-align: text-bottom;
background: #f4f4f5;
animation: demo-caret 1.06s steps(1) infinite;
}
@keyframes demo-caret {
50% { opacity: 0; }
}
.demo-capsule-slot {
position: absolute;
left: 0;
right: 0;
bottom: 20px;
display: flex;
justify-content: center;
opacity: 0;
transform: translateY(6px) scale(0.98);
transition: opacity 180ms cubic-bezier(0.16, 1, 0.3, 1), transform 180ms cubic-bezier(0.16, 1, 0.3, 1);
pointer-events: none;
}
.demo-capsule-slot.is-visible {
opacity: 1;
transform: none;
}
/* ── 녹음 캡슐 — 앱 popups/recording-tip/style.css(v4, 2026-09-28)와 같은 값 ───
* 판 모양(radius 10)·단색 막대·입력 없으면 막대 정지·처리 중 무한 막대·완료 "입력함".
* 색은 앱 다크 테마 원시값(packages/ui/src/theme-raw.ts)과 같다 — 값이 바뀌면 함께 고친다. */
.capsule {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 168px;
max-width: 300px;
padding: 8px 12px;
border-radius: 10px;
border: 1px solid rgba(148, 180, 255, 0.12);
background: #111a30;
box-shadow: 0 12px 32px rgba(3, 7, 18, 0.55);
}
.capsule.is-listening {
border-color: rgba(148, 180, 255, 0.2);
}
.capsule-row {
display: flex;
align-items: center;
gap: 10px;
}
.capsule-bars {
display: flex;
align-items: center;
gap: 3px;
height: 22px;
}
.capsule-bar {
display: block;
width: 3px;
min-height: 2px;
height: 2px;
border-radius: 2px;
background: #3b82f6;
transition: height 80ms ease-out;
}
.capsule.is-thinking .capsule-bar {
background: #7e8eb0;
}
.capsule-duration {
min-width: 32px;
font-size: 13px;
font-weight: 500;
font-variant-numeric: tabular-nums;
color: #eef2fb;
}
.capsule-thinking {
flex: 1;
font-size: 12px;
color: #93a4c8;
white-space: nowrap;
}
.capsule-cancel {
margin-left: auto;
display: inline-flex;
align-items: center;
height: 18px;
padding: 0 5px;
border: 1px solid rgba(148, 180, 255, 0.2);
border-radius: 4px;
font-size: 11px;
line-height: 1;
color: #7e8eb0;
white-space: nowrap;
}
.capsule-progress {
position: relative;
display: block;
height: 2px;
overflow: hidden;
border-radius: 1px;
background: rgba(148, 180, 255, 0.12);
}
.capsule-progress-bar {
position: absolute;
top: 0;
bottom: 0;
width: 36%;
border-radius: 1px;
background: #3b82f6;
animation: capsule-indeterminate 1.1s cubic-bezier(0.22, 1, 0.36, 1) infinite;
}
@keyframes capsule-indeterminate {
from { left: -36%; }
to { left: 100%; }
}
.capsule-partial {
display: flex;
flex-direction: column;
justify-content: flex-end;
max-height: 38px;
overflow: hidden;
border-top: 1px solid rgba(148, 180, 255, 0.07);
padding-top: 6px;
font-size: 12px;
line-height: 1.55;
color: #eef2fb;
}
.capsule.is-thinking .capsule-partial {
color: #7e8eb0;
}
.capsule-done {
font-size: 12px;
color: #eef2fb;
}
.capsule-done-icon {
color: #34d399;
}
/* ── Grid Background Pattern (히어로) ─────────────────── */
.bg-grid {
background-image:
linear-gradient(rgba(59, 130, 246, 0.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(59, 130, 246, 0.035) 1px, transparent 1px);
background-size: 64px 64px;
}
/* ── Utilities ─────────────────────────────────────────── */
@layer utilities {
.text-balance {
text-wrap: balance;
}
/* 한글 어절 단위 줄바꿈. 중국어·일본어에 keep-all을 걸면 줄바꿈 기회가 사라지므로 한국어에만 적용한다. */
.keep-ko {
overflow-wrap: break-word;
}
.keep-ko:lang(ko) {
word-break: keep-all;
}
}
/* ── Reduced motion ───────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}