coding_agent_search/html_export/
styles.rs1use super::template::ExportOptions;
6use tracing::debug;
7
8pub struct StyleBundle {
10 pub critical_css: String,
12
13 pub print_css: String,
15}
16
17pub fn generate_styles(options: &ExportOptions) -> StyleBundle {
19 let critical_css = generate_critical_css(options);
20 let print_css = generate_print_css();
21 debug!(
22 component = "styles",
23 operation = "generate",
24 critical_bytes = critical_css.len(),
25 print_bytes = print_css.len(),
26 "Generated CSS styles"
27 );
28 StyleBundle {
29 critical_css,
30 print_css,
31 }
32}
33
34fn generate_critical_css(options: &ExportOptions) -> String {
35 let search_styles = if options.include_search {
36 SEARCH_STYLES
37 } else {
38 ""
39 };
40
41 let encryption_styles = if options.encrypt {
42 ENCRYPTION_STYLES
43 } else {
44 ""
45 };
46
47 format!(
48 "{}\n{}\n{}\n{}",
49 CORE_STYLES, COMPONENT_STYLES, search_styles, encryption_styles
50 )
51}
52
53const CORE_STYLES: &str = r#"
55/* ============================================
56 Agent Flywheel Design System - Terminal Noir
57 Exact match to globals.css reference
58 ============================================ */
59
60@font-face {
61 font-family: 'Space Grotesk';
62 src: local('Space Grotesk'), local('SpaceGrotesk');
63 font-weight: 400 700;
64 font-display: swap;
65}
66
67@font-face {
68 font-family: 'IBM Plex Sans';
69 src: local('IBM Plex Sans'), local('IBMPlexSans');
70 font-weight: 400 700;
71 font-display: swap;
72}
73
74@font-face {
75 font-family: 'JetBrains Mono';
76 src: local('JetBrains Mono'), local('JetBrainsMono');
77 font-weight: 400 700;
78 font-display: swap;
79}
80
81:root {
82 --radius: 0.75rem;
83
84 /* Deep space palette - from reference */
85 --background: oklch(0.11 0.015 260);
86 --foreground: oklch(0.95 0.01 260);
87
88 /* Cards with subtle elevation */
89 --card: oklch(0.14 0.02 260);
90 --card-foreground: oklch(0.95 0.01 260);
91
92 --popover: oklch(0.13 0.02 260);
93 --popover-foreground: oklch(0.95 0.01 260);
94
95 /* Electric cyan primary */
96 --primary: oklch(0.75 0.18 195);
97 --primary-foreground: oklch(0.13 0.02 260);
98
99 /* Muted backgrounds */
100 --secondary: oklch(0.18 0.02 260);
101 --secondary-foreground: oklch(0.85 0.01 260);
102
103 --muted: oklch(0.16 0.015 260);
104 --muted-foreground: oklch(0.6 0.02 260);
105
106 /* Warm amber accent */
107 --accent: oklch(0.78 0.16 75);
108 --accent-foreground: oklch(0.13 0.02 260);
109
110 /* Destructive red */
111 --destructive: oklch(0.65 0.22 25);
112
113 /* Borders and inputs */
114 --border: oklch(0.25 0.02 260);
115 --input: oklch(0.2 0.02 260);
116 --ring: oklch(0.75 0.18 195);
117
118 /* Custom accent colors */
119 --cyan: oklch(0.75 0.18 195);
120 --amber: oklch(0.78 0.16 75);
121 --magenta: oklch(0.7 0.2 330);
122 --green: oklch(0.72 0.19 145);
123 --purple: oklch(0.65 0.18 290);
124 --red: oklch(0.65 0.22 25);
125
126 /* Typography Scale - Fluid */
127 --text-xs: clamp(0.6875rem, 0.65rem + 0.15vw, 0.75rem);
128 --text-sm: clamp(0.8125rem, 0.775rem + 0.2vw, 0.875rem);
129 --text-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
130 --text-lg: clamp(1.125rem, 1.05rem + 0.4vw, 1.375rem);
131 --text-xl: clamp(1.375rem, 1.25rem + 0.65vw, 1.75rem);
132 --text-2xl: clamp(1.625rem, 1.45rem + 0.9vw, 2.25rem);
133
134 /* Spacing System */
135 --space-1: 0.25rem;
136 --space-2: 0.5rem;
137 --space-3: 0.75rem;
138 --space-4: 1rem;
139 --space-5: 1.25rem;
140 --space-6: 1.5rem;
141 --space-8: 2rem;
142 --space-10: 2.5rem;
143 --space-12: 3rem;
144 --space-16: 4rem;
145
146 /* Enhanced Shadow System - from reference */
147 --shadow-xs: 0 1px 2px oklch(0 0 0 / 0.08);
148 --shadow-sm: 0 2px 4px oklch(0 0 0 / 0.08), 0 1px 2px oklch(0 0 0 / 0.06);
149 --shadow-md: 0 4px 8px oklch(0 0 0 / 0.1), 0 2px 4px oklch(0 0 0 / 0.06);
150 --shadow-lg: 0 8px 24px oklch(0 0 0 / 0.12), 0 4px 8px oklch(0 0 0 / 0.06);
151 --shadow-xl: 0 16px 48px oklch(0 0 0 / 0.16), 0 8px 16px oklch(0 0 0 / 0.08);
152
153 /* Colored glow shadows - from reference */
154 --shadow-glow-sm: 0 0 12px oklch(0.75 0.18 195 / 0.2);
155 --shadow-glow: 0 0 24px oklch(0.75 0.18 195 / 0.25), 0 0 48px oklch(0.75 0.18 195 / 0.1);
156 --shadow-glow-primary: 0 4px 20px oklch(0.75 0.18 195 / 0.35), 0 0 0 1px oklch(0.75 0.18 195 / 0.15);
157 --shadow-glow-amber: 0 4px 20px oklch(0.78 0.16 75 / 0.3), 0 0 0 1px oklch(0.78 0.16 75 / 0.15);
158
159 /* Radius system */
160 --radius-sm: calc(var(--radius) - 4px);
161 --radius-md: calc(var(--radius) - 2px);
162 --radius-lg: var(--radius);
163 --radius-xl: calc(var(--radius) + 4px);
164
165 /* Transitions */
166 --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
167 --transition-normal: 250ms cubic-bezier(0.4, 0, 0.2, 1);
168
169 /* Touch targets */
170 --touch-min: 44px;
171}
172
173/* Light mode - from reference */
174[data-theme="light"] {
175 --background: oklch(0.98 0.005 260);
176 --foreground: oklch(0.15 0.02 260);
177 --card: oklch(1 0 0);
178 --card-foreground: oklch(0.15 0.02 260);
179 --popover: oklch(1 0 0);
180 --popover-foreground: oklch(0.15 0.02 260);
181 --primary: oklch(0.55 0.2 195);
182 --primary-foreground: oklch(1 0 0);
183 --secondary: oklch(0.94 0.01 260);
184 --secondary-foreground: oklch(0.2 0.02 260);
185 --muted: oklch(0.94 0.01 260);
186 --muted-foreground: oklch(0.45 0.02 260);
187 --accent: oklch(0.65 0.18 75);
188 --accent-foreground: oklch(0.15 0.02 260);
189 --destructive: oklch(0.55 0.25 25);
190 --border: oklch(0.9 0.01 260);
191 --input: oklch(0.92 0.01 260);
192 --ring: oklch(0.55 0.2 195);
193
194 --cyan: oklch(0.55 0.2 195);
195 --green: oklch(0.5 0.18 145);
196 --amber: oklch(0.6 0.18 75);
197}
198
199/* Base reset */
200*, *::before, *::after {
201 box-sizing: border-box;
202 margin: 0;
203 padding: 0;
204}
205
206html {
207 overflow-x: hidden;
208 scroll-behavior: smooth;
209 -webkit-font-smoothing: antialiased;
210 -moz-osx-font-smoothing: grayscale;
211}
212
213body {
214 font-family: 'Space Grotesk', 'IBM Plex Sans', 'Manrope', sans-serif;
215 font-size: var(--text-base);
216 line-height: 1.65;
217 color: #e8e9ed;
218 color: var(--foreground);
219 /* Solid dark background - hex fallback first, then oklch if supported */
220 background-color: #16161f;
221 min-height: 100vh;
222 min-height: 100dvh;
223 overflow-x: hidden;
224 max-width: 100vw;
225}
226
227/* Override background with oklch for modern browsers */
228@supports (background: oklch(0.11 0.015 260)) {
229 body {
230 background-color: oklch(0.11 0.015 260);
231 }
232}
233
234/* Hero background overlay - subtle ambient glow */
235body::before {
236 content: '';
237 position: fixed;
238 inset: 0;
239 pointer-events: none;
240 z-index: -1;
241 background:
242 radial-gradient(ellipse at 30% 20%, rgba(70, 180, 220, 0.12) 0%, transparent 40%),
243 radial-gradient(ellipse at 70% 80%, rgba(200, 100, 180, 0.08) 0%, transparent 40%),
244 radial-gradient(ellipse at 90% 30%, rgba(220, 180, 80, 0.06) 0%, transparent 30%);
245}
246
247/* Custom scrollbar - from reference */
248::-webkit-scrollbar {
249 width: 8px;
250 height: 8px;
251}
252::-webkit-scrollbar-track {
253 background: oklch(0.14 0.02 260);
254}
255::-webkit-scrollbar-thumb {
256 background: oklch(0.3 0.02 260);
257 border-radius: 4px;
258}
259::-webkit-scrollbar-thumb:hover {
260 background: oklch(0.4 0.02 260);
261}
262
263/* Firefox scrollbar */
264* {
265 scrollbar-width: thin;
266 scrollbar-color: oklch(0.3 0.02 260) oklch(0.14 0.02 260);
267}
268
269/* ============================================
270 Layout - Full Width Utilization
271 ============================================ */
272
273.app-container {
274 width: 100%;
275 max-width: 100%;
276 margin: 0 auto;
277 padding: var(--space-4);
278 padding-bottom: calc(var(--space-8) + env(safe-area-inset-bottom, 0px));
279}
280
281@media (min-width: 768px) {
282 .app-container {
283 padding: var(--space-6) var(--space-8);
284 }
285}
286
287@media (min-width: 1024px) {
288 .app-container {
289 padding: var(--space-8) var(--space-12);
290 max-width: calc(100% - 80px);
291 }
292}
293
294@media (min-width: 1280px) {
295 .app-container {
296 max-width: calc(100% - 160px);
297 padding: var(--space-8) var(--space-16);
298 }
299}
300
301@media (min-width: 1536px) {
302 .app-container {
303 max-width: 1400px;
304 }
305}
306
307/* ============================================
308 Glass morphism - exact match to reference
309 ============================================ */
310
311.glass {
312 background: oklch(0.14 0.02 260 / 0.8);
313 backdrop-filter: blur(12px);
314 -webkit-backdrop-filter: blur(12px);
315 border: 1px solid oklch(0.3 0.02 260 / 0.3);
316}
317
318.glass-subtle {
319 background: oklch(0.14 0.02 260 / 0.6);
320 backdrop-filter: blur(8px);
321 -webkit-backdrop-filter: blur(8px);
322}
323
324/* ============================================
325 Typography
326 ============================================ */
327
328h1, h2, h3, h4, h5, h6 {
329 font-weight: 600;
330 line-height: 1.3;
331 color: var(--foreground);
332 letter-spacing: -0.02em;
333}
334
335h1 { font-size: var(--text-2xl); }
336h2 { font-size: var(--text-xl); }
337h3 { font-size: var(--text-lg); }
338
339p {
340 margin-bottom: 1em;
341}
342p:last-child { margin-bottom: 0; }
343
344a {
345 color: var(--primary);
346 text-decoration: none;
347 transition: color var(--transition-fast);
348}
349
350a:hover {
351 color: oklch(0.85 0.18 195);
352 text-decoration: underline;
353}
354
355/* Inline code */
356code:not(pre code) {
357 font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', ui-monospace, monospace;
358 font-size: 0.875em;
359 padding: 0.125rem 0.375rem;
360 background: var(--secondary);
361 border: 1px solid var(--border);
362 border-radius: var(--radius-sm);
363 color: var(--primary);
364 overflow-wrap: break-word;
365 word-break: break-word;
366}
367
368/* Code blocks */
369pre {
370 font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', ui-monospace, monospace;
371 font-size: 0.8125rem;
372 line-height: 1.7;
373 background: oklch(0.08 0.015 260);
374 border: 1px solid var(--border);
375 border-radius: var(--radius-lg);
376 padding: var(--space-4);
377 overflow-x: auto;
378 margin: var(--space-4) 0;
379 max-width: 100%;
380}
381
382pre code {
383 padding: 0;
384 background: transparent;
385 border: none;
386 color: var(--foreground);
387 font-size: inherit;
388}
389
390/* Lists */
391ul, ol {
392 margin: var(--space-2) 0;
393 padding-left: 1.5em;
394}
395li {
396 margin-bottom: 0.25em;
397}
398li::marker { color: var(--muted-foreground); }
399
400/* Blockquotes */
401blockquote {
402 border-left: 3px solid var(--primary);
403 padding: var(--space-2) var(--space-4);
404 margin: var(--space-4) 0;
405 background: linear-gradient(90deg, oklch(0.75 0.18 195 / 0.05) 0%, transparent 100%);
406 border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
407 color: var(--secondary-foreground);
408}
409
410/* Tables */
411table {
412 width: 100%;
413 border-collapse: collapse;
414 margin: var(--space-4) 0;
415 font-size: 0.875rem;
416}
417th, td {
418 padding: var(--space-2) var(--space-3);
419 border: 1px solid var(--border);
420 text-align: left;
421}
422th {
423 background: var(--secondary);
424 font-weight: 600;
425 font-size: 0.75rem;
426 text-transform: uppercase;
427 letter-spacing: 0.5px;
428 color: var(--muted-foreground);
429}
430tr:hover td {
431 background: var(--muted);
432}
433"#;
434
435const COMPONENT_STYLES: &str = r#"
436/* ============================================
437 Header - Terminal Style
438 ============================================ */
439
440.header {
441 margin-bottom: var(--space-6);
442 padding: var(--space-4) var(--space-5);
443 background: var(--card);
444 border: 1px solid var(--border);
445 border-radius: var(--radius-xl);
446 position: relative;
447}
448
449/* Terminal traffic lights */
450.header::before {
451 content: '';
452 position: absolute;
453 top: var(--space-4);
454 left: var(--space-5);
455 width: 12px;
456 height: 12px;
457 border-radius: 50%;
458 background: oklch(0.65 0.22 25);
459 box-shadow:
460 20px 0 0 oklch(0.78 0.16 75),
461 40px 0 0 oklch(0.72 0.19 145);
462}
463
464.header-content {
465 padding-left: 72px;
466}
467
468.header-title {
469 font-size: var(--text-lg);
470 font-weight: 600;
471 color: var(--foreground);
472 margin-bottom: var(--space-2);
473 line-height: 1.4;
474 font-family: 'Space Grotesk', 'IBM Plex Sans', sans-serif;
475}
476
477.header-meta {
478 display: flex;
479 flex-wrap: wrap;
480 align-items: center;
481 gap: var(--space-2) var(--space-4);
482 font-size: var(--text-sm);
483 color: var(--muted-foreground);
484}
485
486.header-meta span {
487 display: inline-flex;
488 align-items: center;
489 gap: 6px;
490}
491
492.header-agent {
493 color: var(--primary);
494 font-weight: 500;
495}
496
497.header-project {
498 font-family: 'JetBrains Mono', ui-monospace, monospace;
499 font-size: var(--text-xs);
500 padding: 0.25rem 0.625rem;
501 background: var(--secondary);
502 border: 1px solid var(--border);
503 border-radius: var(--radius-sm);
504 color: var(--muted-foreground);
505}
506
507/* ============================================
508 Toolbar - Glassmorphic
509 ============================================ */
510
511.toolbar {
512 position: sticky;
513 top: var(--space-4);
514 z-index: 50;
515 display: flex;
516 align-items: center;
517 gap: var(--space-2);
518 padding: var(--space-3) var(--space-4);
519 margin-bottom: var(--space-6);
520 background: oklch(0.14 0.02 260 / 0.8);
521 backdrop-filter: blur(12px);
522 -webkit-backdrop-filter: blur(12px);
523 border: 1px solid oklch(0.3 0.02 260 / 0.3);
524 border-radius: var(--radius-xl);
525 box-shadow: var(--shadow-lg);
526 transition: all var(--transition-normal);
527}
528
529.toolbar:hover {
530 box-shadow: var(--shadow-xl), var(--shadow-glow-sm);
531}
532
533[data-theme="light"] .toolbar {
534 background: oklch(1 0 0 / 0.85);
535 border-color: var(--border);
536}
537
538.search-wrapper {
539 flex: 1;
540 position: relative;
541 min-width: 0;
542}
543
544.search-input {
545 width: 100%;
546 padding: 0.625rem 0.875rem;
547 padding-right: 3rem;
548 font-size: var(--text-sm);
549 color: var(--foreground);
550 background: var(--input);
551 border: 1px solid var(--border);
552 border-radius: var(--radius-md);
553 outline: none;
554 transition: all var(--transition-fast);
555}
556
557.search-input::placeholder {
558 color: var(--muted-foreground);
559}
560
561.search-input:hover {
562 border-color: oklch(0.35 0.02 260);
563}
564
565.search-input:focus {
566 border-color: var(--primary);
567 box-shadow: 0 0 0 3px oklch(0.75 0.18 195 / 0.15), var(--shadow-glow-sm);
568}
569
570.search-count {
571 position: absolute;
572 right: 0.875rem;
573 top: 50%;
574 transform: translateY(-50%);
575 font-size: var(--text-xs);
576 font-weight: 500;
577 color: var(--muted-foreground);
578 background: var(--secondary);
579 padding: 0.125rem 0.375rem;
580 border-radius: var(--radius-sm);
581}
582
583.toolbar-btn {
584 display: flex;
585 align-items: center;
586 justify-content: center;
587 width: var(--touch-min);
588 height: var(--touch-min);
589 min-width: var(--touch-min);
590 background: transparent;
591 border: 1px solid transparent;
592 border-radius: var(--radius-md);
593 color: var(--muted-foreground);
594 cursor: pointer;
595 transition: all var(--transition-fast);
596 position: relative;
597}
598
599.toolbar-btn:hover {
600 background: var(--secondary);
601 border-color: var(--border);
602 color: var(--foreground);
603}
604
605.toolbar-btn:active {
606 transform: scale(0.95);
607}
608
609.toolbar-btn svg {
610 width: 20px;
611 height: 20px;
612 transition: transform var(--transition-fast);
613}
614
615.toolbar-btn:hover svg {
616 transform: scale(1.1);
617}
618
619/* Theme toggle icon states */
620.icon-sun, .icon-moon {
621 transition: opacity var(--transition-fast), transform var(--transition-fast);
622}
623[data-theme="dark"] .icon-sun { opacity: 0; position: absolute; transform: rotate(90deg) scale(0.8); }
624[data-theme="dark"] .icon-moon { opacity: 1; }
625[data-theme="light"] .icon-sun { opacity: 1; }
626[data-theme="light"] .icon-moon { opacity: 0; position: absolute; transform: rotate(-90deg) scale(0.8); }
627
628/* ============================================
629 Messages - Card Based
630 ============================================ */
631
632.conversation {
633 display: flex;
634 flex-direction: column;
635 gap: var(--space-4);
636 position: relative;
637 z-index: 1;
638}
639
640/* Message wrapper - inherits conversation layout */
641.conversation-messages {
642 display: flex;
643 flex-direction: column;
644 gap: var(--space-4);
645}
646
647.message {
648 position: relative;
649 padding: var(--space-4) var(--space-5);
650 background: #1e1e28;
651 background: var(--card);
652 border: 1px solid #2d2d3a;
653 border: 1px solid var(--border);
654 border-radius: var(--radius-xl);
655 border-left: 4px solid #2d2d3a;
656 border-left: 4px solid var(--border);
657 transition: all var(--transition-fast);
658}
659
660.message:hover {
661 border-color: oklch(0.35 0.02 260);
662 box-shadow: var(--shadow-md);
663}
664
665.message.search-hit {
666 border-color: var(--primary);
667 box-shadow: var(--shadow-md), var(--shadow-glow-sm);
668}
669
670/* Role-specific styling */
671.message-user {
672 border-left-color: var(--green);
673}
674.message-user:hover {
675 box-shadow: var(--shadow-md), 0 0 30px -10px var(--green);
676}
677
678.message-assistant, .message-agent {
679 border-left-color: var(--primary);
680}
681.message-assistant:hover, .message-agent:hover {
682 box-shadow: var(--shadow-md), 0 0 30px -10px var(--primary);
683}
684
685.message-tool {
686 border-left-color: var(--amber);
687}
688.message-tool:hover {
689 box-shadow: var(--shadow-md), 0 0 30px -10px var(--amber);
690}
691
692.message-system {
693 border-left-color: var(--purple);
694 background: linear-gradient(135deg, var(--card) 0%, oklch(0.65 0.18 290 / 0.03) 100%);
695}
696
697.message-header {
698 display: flex;
699 align-items: center;
700 justify-content: space-between;
701 gap: var(--space-3);
702 margin-bottom: var(--space-3);
703 padding-bottom: var(--space-2);
704 border-bottom: 1px solid oklch(0.25 0.02 260 / 0.5);
705}
706
707.message-header-left {
708 display: flex;
709 align-items: center;
710 gap: var(--space-2);
711 min-width: 0;
712}
713
714.message-header-right {
715 display: flex;
716 flex-wrap: wrap;
717 align-items: center;
718 gap: var(--space-1);
719 flex-shrink: 0;
720}
721
722/* Lucide SVG icon styling */
723.lucide-icon {
724 display: inline-block;
725 vertical-align: middle;
726 flex-shrink: 0;
727}
728
729.lucide-spin {
730 animation: lucide-spin 1s linear infinite;
731}
732
733@keyframes lucide-spin {
734 from { transform: rotate(0deg); }
735 to { transform: rotate(360deg); }
736}
737
738.message-icon {
739 display: flex;
740 align-items: center;
741 justify-content: center;
742 width: 24px;
743 height: 24px;
744 line-height: 1;
745}
746
747.message-icon .lucide-icon {
748 width: 16px;
749 height: 16px;
750}
751
752.message-author {
753 font-weight: 600;
754 font-size: var(--text-sm);
755 letter-spacing: -0.01em;
756}
757
758.message-user .message-author { color: var(--green); }
759.message-assistant .message-author, .message-agent .message-author { color: var(--primary); }
760.message-tool .message-author { color: var(--amber); }
761.message-system .message-author { color: var(--purple); }
762
763.message-time {
764 margin-left: auto;
765 font-size: var(--text-xs);
766 font-weight: 500;
767 color: var(--muted-foreground);
768 font-variant-numeric: tabular-nums;
769}
770
771.message-content {
772 font-size: var(--text-base);
773 line-height: 1.7;
774 color: var(--secondary-foreground);
775}
776
777.message-content > *:first-child { margin-top: 0; }
778.message-content > *:last-child { margin-bottom: 0; }
779
780/* Message content typography */
781.message-content p { margin-bottom: 0.85em; }
782.message-content h1, .message-content h2, .message-content h3 {
783 margin-top: 1.25em;
784 margin-bottom: 0.5em;
785 font-weight: 600;
786 color: var(--foreground);
787}
788.message-content h1 { font-size: 1.25rem; }
789.message-content h2 { font-size: 1.125rem; }
790.message-content h3 { font-size: 1rem; }
791.message-content ul, .message-content ol {
792 margin: 0.5em 0;
793 padding-left: 1.25em;
794}
795.message-content li { margin-bottom: 0.25em; }
796.message-content li::marker { color: var(--muted-foreground); }
797.message-content strong { color: var(--foreground); font-weight: 600; }
798
799/* Message link button */
800.message-link {
801 position: absolute;
802 top: var(--space-4);
803 right: var(--space-4);
804 opacity: 0;
805 padding: 6px;
806 background: var(--secondary);
807 border: 1px solid var(--border);
808 border-radius: var(--radius-sm);
809 color: var(--muted-foreground);
810 cursor: pointer;
811 transition: all var(--transition-fast);
812}
813
814.message:hover .message-link { opacity: 1; }
815.message-link:hover {
816 color: var(--primary);
817 border-color: var(--primary);
818 box-shadow: var(--shadow-glow-sm);
819}
820.message-link.copied {
821 color: var(--green);
822 border-color: var(--green);
823}
824
825/* ============================================
826 Tool Calls - Collapsible
827 ============================================ */
828
829/* Tool Badge - Compact inline badges with hover popovers */
830.tool-badges {
831 display: flex;
832 flex-wrap: wrap;
833 align-items: center;
834 gap: 4px;
835}
836
837.tool-badge {
838 position: relative;
839 display: inline-flex;
840 align-items: center;
841 justify-content: center;
842 min-width: 24px;
843 height: 24px;
844 padding: 0 4px;
845 font-size: 0.6875rem;
846 font-family: 'JetBrains Mono', ui-monospace, monospace;
847 background: transparent;
848 appearance: none;
849 -webkit-appearance: none;
850 border: 1px solid oklch(0.3 0.02 260 / 0.5);
851 border-radius: 6px;
852 cursor: pointer;
853 transition: all var(--transition-fast);
854 white-space: nowrap;
855 color: var(--amber);
856}
857
858.tool-badge:hover,
859.tool-badge:focus {
860 background: oklch(0.78 0.16 75 / 0.15);
861 border-color: var(--amber);
862 transform: scale(1.1);
863 outline: none;
864 box-shadow: var(--shadow-glow-amber);
865}
866
867.tool-badge:focus-visible {
868 box-shadow: 0 0 0 2px var(--primary);
869}
870
871.tool-badge-icon {
872 display: flex;
873 align-items: center;
874 justify-content: center;
875}
876
877.tool-badge-icon .lucide-icon {
878 width: 14px;
879 height: 14px;
880 stroke-width: 2;
881}
882
883.tool-badge-status {
884 display: inline-flex;
885 align-items: center;
886 justify-content: center;
887 position: absolute;
888 top: 2px;
889 right: 2px;
890 width: 6px;
891 height: 6px;
892 border-radius: 50%;
893 padding: 0;
894}
895
896.tool-badge-status .lucide-icon {
897 display: none;
898}
899
900/* Status-based badge styling with subtle left accent */
901.tool-badge.tool-status-success { border-color: var(--green); }
902.tool-badge.tool-status-error { border-color: var(--red); }
903.tool-badge.tool-status-pending { border-color: var(--amber); }
904
905.tool-badge.tool-status-success:hover { box-shadow: 0 4px 20px oklch(0.72 0.19 145 / 0.35); }
906.tool-badge.tool-status-error:hover { box-shadow: 0 4px 20px oklch(0.65 0.22 25 / 0.35); }
907
908.tool-badge-status.success { background: oklch(0.72 0.19 145 / 0.8); }
909.tool-badge-status.error { background: oklch(0.65 0.22 25 / 0.85); }
910.tool-badge-status.pending { background: oklch(0.78 0.16 75 / 0.85); }
911
912/* Overflow badge - "+X more" */
913.tool-badge.tool-overflow {
914 min-width: auto;
915 padding: 0 8px;
916 font-size: 0.6875rem;
917 font-weight: 600;
918 color: var(--muted-foreground);
919 border-style: dashed;
920}
921
922.tool-badge.tool-overflow:hover {
923 color: var(--foreground);
924 border-style: solid;
925}
926
927/* Overflowed badges stay in the DOM so expansion can reveal them. */
928.tool-badge.tool-overflow-extra {
929 display: none;
930}
931
932.message-header-right.expanded .tool-overflow-extra {
933 display: inline-flex;
934}
935
936.message-header-right.expanded .tool-overflow {
937 order: 999; /* Move to end */
938}
939
940/* Popover - Glassmorphic with fixed positioning */
941.tool-popover {
942 position: absolute;
943 z-index: 1000;
944 min-width: 280px;
945 max-width: 400px;
946 max-height: 300px;
947 overflow: auto;
948 padding: var(--space-3);
949 background: oklch(0.14 0.02 260 / 0.95);
950 backdrop-filter: blur(16px);
951 -webkit-backdrop-filter: blur(16px);
952 border: 1px solid oklch(0.3 0.02 260 / 0.5);
953 border-radius: var(--radius-lg);
954 box-shadow: var(--shadow-xl), var(--shadow-glow-sm);
955 opacity: 0;
956 visibility: hidden;
957 transform: translateY(-4px);
958 transition: all 0.15s ease-out;
959 pointer-events: none;
960 text-align: left;
961 white-space: normal;
962 top: calc(100% + 8px);
963 left: 0;
964}
965
966.tool-popover.visible {
967 opacity: 1;
968 visibility: visible;
969 transform: translateY(0);
970 pointer-events: auto;
971}
972
973/* Fallback: show popover on hover/focus even if JS fails */
974.tool-badge:hover .tool-popover,
975.tool-badge:focus-within .tool-popover {
976 opacity: 1;
977 visibility: visible;
978 transform: translateY(0);
979 pointer-events: auto;
980}
981
982/* Light theme popover */
983[data-theme="light"] .tool-popover {
984 background: oklch(1 0 0 / 0.95);
985 border-color: var(--border);
986 box-shadow: 0 8px 32px oklch(0 0 0 / 0.15);
987}
988
989/* Arrow indicator (CSS-only, optional) */
990.tool-popover::before {
991 content: '';
992 position: absolute;
993 top: -6px;
994 left: 20px;
995 width: 12px;
996 height: 12px;
997 background: inherit;
998 border: inherit;
999 border-right: none;
1000 border-bottom: none;
1001 transform: rotate(45deg);
1002 pointer-events: none;
1003}
1004
1005.tool-popover.popover-above::before {
1006 top: auto;
1007 bottom: -6px;
1008 transform: rotate(225deg);
1009}
1010
1011.tool-popover-header {
1012 display: flex;
1013 align-items: center;
1014 gap: var(--space-2);
1015 padding-bottom: var(--space-2);
1016 margin-bottom: var(--space-2);
1017 border-bottom: 1px solid var(--border);
1018 font-weight: 600;
1019 color: var(--amber);
1020}
1021
1022.tool-popover-header .lucide-icon {
1023 width: 14px;
1024 height: 14px;
1025 flex-shrink: 0;
1026}
1027
1028.tool-popover-header span {
1029 overflow: hidden;
1030 text-overflow: ellipsis;
1031 white-space: nowrap;
1032}
1033
1034.tool-popover-section {
1035 margin-bottom: var(--space-2);
1036}
1037.tool-popover-section:last-child { margin-bottom: 0; }
1038
1039.tool-popover-label {
1040 font-size: 0.5625rem;
1041 font-weight: 700;
1042 text-transform: uppercase;
1043 letter-spacing: 0.8px;
1044 color: var(--muted-foreground);
1045 margin-bottom: 0.25rem;
1046}
1047
1048.tool-popover pre {
1049 margin: 0;
1050 padding: var(--space-2);
1051 font-size: 0.625rem;
1052 background: var(--secondary);
1053 border-radius: var(--radius-sm);
1054 max-height: 150px;
1055 overflow: auto;
1056 white-space: pre-wrap;
1057 word-break: break-word;
1058}
1059
1060.tool-truncated {
1061 font-size: 0.5625rem;
1062 color: var(--amber);
1063 margin-top: 0.25rem;
1064 font-weight: 500;
1065 font-style: italic;
1066}
1067
1068/* ============================================
1069 Floating Navigation
1070 ============================================ */
1071
1072.floating-nav {
1073 position: fixed;
1074 bottom: calc(24px + env(safe-area-inset-bottom, 0px));
1075 right: 24px;
1076 display: flex;
1077 flex-direction: column;
1078 gap: var(--space-2);
1079 opacity: 0;
1080 transform: translateY(20px) scale(0.9);
1081 transition: all var(--transition-normal);
1082 pointer-events: none;
1083 z-index: 100;
1084}
1085
1086.floating-nav.visible {
1087 opacity: 1;
1088 transform: translateY(0) scale(1);
1089 pointer-events: auto;
1090}
1091
1092.floating-btn {
1093 width: 48px;
1094 height: 48px;
1095 display: flex;
1096 align-items: center;
1097 justify-content: center;
1098 background: oklch(0.14 0.02 260 / 0.8);
1099 backdrop-filter: blur(12px);
1100 -webkit-backdrop-filter: blur(12px);
1101 border: 1px solid oklch(0.3 0.02 260 / 0.3);
1102 border-radius: var(--radius-xl);
1103 color: var(--muted-foreground);
1104 cursor: pointer;
1105 box-shadow: var(--shadow-lg);
1106 transition: all var(--transition-fast);
1107}
1108
1109.floating-btn:hover {
1110 background: var(--secondary);
1111 border-color: var(--primary);
1112 color: var(--primary);
1113 box-shadow: var(--shadow-lg), var(--shadow-glow);
1114 transform: translateY(-2px);
1115}
1116
1117.floating-btn:active {
1118 transform: scale(0.95);
1119}
1120
1121.floating-btn svg {
1122 width: 22px;
1123 height: 22px;
1124}
1125
1126/* ============================================
1127 Scroll Progress
1128 ============================================ */
1129
1130.scroll-progress {
1131 position: fixed;
1132 top: 0;
1133 left: 0;
1134 height: 3px;
1135 background: linear-gradient(90deg, var(--primary), var(--magenta), var(--primary));
1136 background-size: 200% 100%;
1137 z-index: 1000;
1138 width: 0;
1139 transition: width 0.1s ease-out;
1140 box-shadow: 0 0 10px var(--primary);
1141}
1142
1143/* ============================================
1144 Keyboard Shortcuts Hint
1145 ============================================ */
1146
1147.shortcuts-hint {
1148 position: fixed;
1149 bottom: calc(24px + env(safe-area-inset-bottom, 0px));
1150 left: 50%;
1151 transform: translateX(-50%) translateY(20px);
1152 padding: 0.75rem 1.25rem;
1153 background: oklch(0.14 0.02 260 / 0.8);
1154 backdrop-filter: blur(12px);
1155 -webkit-backdrop-filter: blur(12px);
1156 border: 1px solid oklch(0.3 0.02 260 / 0.3);
1157 border-radius: var(--radius-xl);
1158 font-size: var(--text-xs);
1159 color: var(--secondary-foreground);
1160 opacity: 0;
1161 transition: all var(--transition-normal);
1162 z-index: 100;
1163 box-shadow: var(--shadow-xl);
1164 white-space: nowrap;
1165}
1166
1167.shortcuts-hint.visible {
1168 opacity: 1;
1169 transform: translateX(-50%) translateY(0);
1170}
1171
1172.shortcuts-hint kbd {
1173 display: inline-block;
1174 padding: 0.1875rem 0.5rem;
1175 margin: 0 0.1875rem;
1176 font-family: 'JetBrains Mono', ui-monospace, monospace;
1177 font-size: 0.6875rem;
1178 font-weight: 500;
1179 background: var(--secondary);
1180 border: 1px solid var(--border);
1181 border-radius: 5px;
1182 box-shadow: 0 2px 0 var(--background);
1183}
1184
1185/* ============================================
1186 Animations
1187 ============================================ */
1188
1189@keyframes fadeIn {
1190 from {
1191 opacity: 0;
1192 transform: translateY(12px);
1193 }
1194 to {
1195 opacity: 1;
1196 transform: translateY(0);
1197 }
1198}
1199
1200@keyframes slideUp {
1201 from {
1202 opacity: 0;
1203 transform: translateY(20px);
1204 }
1205 to {
1206 opacity: 1;
1207 transform: translateY(0);
1208 }
1209}
1210
1211/* Staggered fade-in animation - uses forwards to ensure visibility after animation */
1212.message {
1213 animation: fadeIn 0.35s cubic-bezier(0.33, 1, 0.68, 1) forwards;
1214 opacity: 1; /* Fallback for when animations don't run */
1215}
1216
1217/* Staggered animation delays for visual polish */
1218.message:nth-child(1) { animation-delay: 0.02s; }
1219.message:nth-child(2) { animation-delay: 0.04s; }
1220.message:nth-child(3) { animation-delay: 0.06s; }
1221.message:nth-child(4) { animation-delay: 0.08s; }
1222.message:nth-child(5) { animation-delay: 0.1s; }
1223.message:nth-child(n+6) { animation-delay: 0.12s; }
1224
1225/* ============================================
1226 Accessibility
1227 ============================================ */
1228
1229@media (prefers-reduced-motion: reduce) {
1230 *, *::before, *::after {
1231 animation-duration: 0.01ms !important;
1232 animation-delay: 0ms !important;
1233 transition-duration: 0.01ms !important;
1234 scroll-behavior: auto !important;
1235 }
1236 .message { animation: none; }
1237}
1238
1239:focus-visible {
1240 outline: 2px solid var(--primary);
1241 outline-offset: 2px;
1242}
1243
1244@media (prefers-contrast: high) {
1245 :root {
1246 --border: oklch(0.5 0.02 260);
1247 --muted-foreground: oklch(0.75 0.02 260);
1248 }
1249 .tool-badge {
1250 border-width: 2px;
1251 }
1252 .message {
1253 border-width: 2px;
1254 }
1255 .tool-popover {
1256 border-width: 2px;
1257 }
1258}
1259
1260/* ============================================
1261 MOBILE (< 768px)
1262 ============================================ */
1263
1264@media (max-width: 767px) {
1265 .app-container {
1266 padding: var(--space-3);
1267 padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px));
1268 }
1269
1270 .header {
1271 padding: var(--space-3) var(--space-4);
1272 margin-bottom: var(--space-4);
1273 }
1274
1275 .header::before {
1276 width: 10px;
1277 height: 10px;
1278 top: var(--space-3);
1279 left: var(--space-4);
1280 box-shadow:
1281 16px 0 0 oklch(0.78 0.16 75),
1282 32px 0 0 oklch(0.72 0.19 145);
1283 }
1284
1285 .header-content {
1286 padding-left: 56px;
1287 }
1288
1289 .header-title {
1290 font-size: var(--text-base);
1291 }
1292
1293 .header-meta {
1294 gap: var(--space-1) var(--space-2);
1295 font-size: var(--text-xs);
1296 }
1297
1298 .toolbar {
1299 position: fixed;
1300 bottom: 0;
1301 left: 0;
1302 right: 0;
1303 top: auto;
1304 margin: 0;
1305 padding: var(--space-2);
1306 padding-bottom: calc(var(--space-2) + env(safe-area-inset-bottom, 0px));
1307 border-radius: var(--radius-xl) var(--radius-xl) 0 0;
1308 border-bottom: none;
1309 z-index: 100;
1310 }
1311
1312 .search-input {
1313 padding: 0.75rem;
1314 font-size: 1rem; /* Prevent zoom on iOS */
1315 }
1316
1317 .conversation {
1318 gap: var(--space-3);
1319 }
1320
1321 .message {
1322 padding: var(--space-3) var(--space-4);
1323 border-radius: var(--radius-lg);
1324 }
1325
1326 .message-header {
1327 gap: var(--space-1);
1328 margin-bottom: var(--space-2);
1329 padding-bottom: var(--space-1);
1330 }
1331
1332 .message-icon { font-size: 0.875rem; }
1333 .message-author { font-size: var(--text-xs); }
1334 .message-time { font-size: 0.625rem; }
1335
1336 .message-content {
1337 font-size: var(--text-sm);
1338 line-height: 1.6;
1339 }
1340
1341 .message-link {
1342 top: var(--space-3);
1343 right: var(--space-3);
1344 padding: 8px;
1345 opacity: 1; /* Always visible on mobile */
1346 }
1347
1348 .tool-call {
1349 margin-top: var(--space-3);
1350 }
1351
1352 .tool-call summary {
1353 padding: var(--space-2);
1354 min-height: 48px;
1355 }
1356
1357 .tool-call-body {
1358 padding: var(--space-3);
1359 }
1360
1361 .tool-call pre {
1362 font-size: 0.625rem;
1363 padding: var(--space-1) var(--space-2);
1364 max-height: 200px;
1365 }
1366
1367 .floating-nav {
1368 bottom: calc(80px + env(safe-area-inset-bottom, 0px));
1369 right: var(--space-3);
1370 }
1371
1372 .floating-btn {
1373 width: 44px;
1374 height: 44px;
1375 }
1376
1377 .shortcuts-hint {
1378 display: none;
1379 }
1380
1381 /* Larger tap targets */
1382 button, a, summary {
1383 min-height: var(--touch-min);
1384 }
1385
1386 /* Block-level code overflow */
1387 pre, code {
1388 max-width: 100%;
1389 }
1390
1391 /* Tool badges - larger touch targets on mobile */
1392 .tool-badge {
1393 min-width: 32px;
1394 height: 32px;
1395 }
1396
1397 .tool-badges {
1398 gap: 6px;
1399 }
1400
1401 /* Mobile popover - bottom sheet style */
1402 .tool-popover {
1403 position: fixed;
1404 bottom: 0;
1405 left: 0;
1406 right: 0;
1407 top: auto;
1408 max-width: 100%;
1409 max-height: 60vh;
1410 border-radius: var(--radius-xl) var(--radius-xl) 0 0;
1411 padding: var(--space-4);
1412 padding-bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
1413 transform: translateY(100%);
1414 }
1415
1416 .tool-popover.visible {
1417 transform: translateY(0);
1418 }
1419
1420 /* Hide arrow on mobile */
1421 .tool-popover::before {
1422 display: none;
1423 }
1424
1425 /* Add drag handle indicator */
1426 .tool-popover::after {
1427 content: '';
1428 position: absolute;
1429 top: 8px;
1430 left: 50%;
1431 transform: translateX(-50%);
1432 width: 36px;
1433 height: 4px;
1434 background: oklch(0.4 0.02 260);
1435 border-radius: 2px;
1436 }
1437}
1438
1439/* ============================================
1440 TABLET (768px - 1023px)
1441 ============================================ */
1442
1443@media (min-width: 768px) and (max-width: 1023px) {
1444 .message {
1445 padding: var(--space-4) var(--space-5);
1446 }
1447}
1448
1449/* ============================================
1450 LARGE DESKTOP (1280px+)
1451 ============================================ */
1452
1453@media (min-width: 1280px) {
1454 .message {
1455 padding: var(--space-5) var(--space-6);
1456 }
1457
1458 .message-content {
1459 font-size: 1.0625rem;
1460 line-height: 1.75;
1461 }
1462
1463 .toolbar {
1464 padding: var(--space-4) var(--space-5);
1465 }
1466}
1467
1468/* ============================================
1469 Message Collapse
1470 ============================================ */
1471
1472.message-collapse summary {
1473 cursor: pointer;
1474 list-style: none;
1475}
1476
1477.message-collapse summary::-webkit-details-marker { display: none; }
1478
1479.message-preview {
1480 color: var(--secondary-foreground);
1481 display: -webkit-box;
1482 -webkit-line-clamp: 3;
1483 -webkit-box-orient: vertical;
1484 overflow: hidden;
1485}
1486
1487.message-expand-hint {
1488 display: block;
1489 margin-top: 6px;
1490 font-size: var(--text-xs);
1491 font-weight: 500;
1492 color: var(--primary);
1493}
1494
1495.message-collapse[open] .message-expand-hint { display: none; }
1496
1497.message-expanded { margin-top: var(--space-3); }
1498
1499/* ============================================
1500 Code Block Copy Button
1501 ============================================ */
1502
1503pre {
1504 position: relative;
1505}
1506
1507.copy-code-btn {
1508 position: absolute;
1509 top: 8px;
1510 right: 8px;
1511 padding: 4px;
1512 background: var(--card);
1513 border: 1px solid var(--border);
1514 border-radius: var(--radius-sm);
1515 color: var(--muted-foreground);
1516 cursor: pointer;
1517 opacity: 0;
1518 transition: opacity var(--transition-fast), color var(--transition-fast);
1519}
1520
1521pre:hover .copy-code-btn { opacity: 1; }
1522.copy-code-btn:hover { color: var(--primary); border-color: var(--primary); }
1523.copy-code-btn.copied { color: var(--green); border-color: var(--green); }
1524
1525/* ============================================
1526 Toast Notifications
1527 ============================================ */
1528
1529.toast {
1530 padding: 0.625rem 1rem;
1531 background: var(--card);
1532 border: 1px solid var(--border);
1533 border-radius: var(--radius-md);
1534 color: var(--foreground);
1535 box-shadow: var(--shadow-lg);
1536 font-size: var(--text-sm);
1537}
1538
1539.toast-success { border-color: var(--green); }
1540.toast-error { border-color: var(--red); }
1541
1542/* ============================================
1543 Agent-Specific Theming
1544 ============================================ */
1545
1546.agent-claude .message-assistant { border-left-color: oklch(0.7 0.18 50); }
1547.agent-codex .message-assistant { border-left-color: oklch(0.7 0.2 145); }
1548.agent-cursor .message-assistant { border-left-color: oklch(0.7 0.2 280); }
1549.agent-chatgpt .message-assistant { border-left-color: oklch(0.72 0.19 165); }
1550.agent-gemini .message-assistant { border-left-color: oklch(0.7 0.2 250); }
1551.agent-aider .message-assistant { border-left-color: oklch(0.72 0.16 85); }
1552.agent-copilot .message-assistant { border-left-color: oklch(0.7 0.18 200); }
1553.agent-cody .message-assistant { border-left-color: oklch(0.68 0.2 340); }
1554.agent-windsurf .message-assistant { border-left-color: oklch(0.7 0.2 205); }
1555.agent-amp .message-assistant { border-left-color: oklch(0.7 0.18 270); }
1556.agent-grok .message-assistant { border-left-color: oklch(0.7 0.22 350); }
1557.agent-hermes .message-assistant { border-left-color: oklch(0.78 0.16 95); }
1558
1559/* Print styles */
1560@media print {
1561 body::before { display: none; }
1562 .toolbar, .floating-nav, .scroll-progress { display: none !important; }
1563 .message {
1564 background: white;
1565 backdrop-filter: none;
1566 box-shadow: none;
1567 border: 1px solid #ccc;
1568 break-inside: avoid;
1569 }
1570 .message-link { display: none; }
1571 .copy-code-btn { display: none; }
1572 .tool-popover { display: none !important; }
1573 .tool-badge {
1574 border: 1px solid #666;
1575 background: #f5f5f5;
1576 color: #333;
1577 }
1578 .tool-badge-icon { color: #666; }
1579}
1580"#;
1581
1582const SEARCH_STYLES: &str = r#"
1583/* Search highlighting */
1584.search-highlight {
1585 background: oklch(0.75 0.18 195 / 0.3);
1586 border-radius: 2px;
1587 padding: 1px 0;
1588 box-shadow: 0 0 0 1px oklch(0.75 0.18 195 / 0.35);
1589}
1590
1591.search-current {
1592 background: oklch(0.78 0.16 75 / 0.5);
1593 box-shadow: 0 0 0 1px oklch(0.78 0.16 75 / 0.6);
1594}
1595"#;
1596
1597const ENCRYPTION_STYLES: &str = r#"
1598/* Encryption modal */
1599.decrypt-modal {
1600 position: fixed;
1601 inset: 0;
1602 z-index: 1000;
1603 display: flex;
1604 align-items: center;
1605 justify-content: center;
1606 background: oklch(0 0 0 / 0.85);
1607 backdrop-filter: blur(8px);
1608}
1609
1610.decrypt-form {
1611 width: 100%;
1612 max-width: 360px;
1613 padding: var(--space-6);
1614 background: var(--card);
1615 border: 1px solid var(--border);
1616 border-radius: var(--radius-xl);
1617 box-shadow: var(--shadow-xl);
1618}
1619
1620.decrypt-form h2 {
1621 margin: 0 0 var(--space-4);
1622 font-size: var(--text-lg);
1623 color: var(--foreground);
1624}
1625
1626.decrypt-form input {
1627 width: 100%;
1628 padding: 0.625rem 0.75rem;
1629 margin-bottom: var(--space-3);
1630 background: var(--input);
1631 border: 1px solid var(--border);
1632 border-radius: var(--radius-md);
1633 color: var(--foreground);
1634 font-size: var(--text-sm);
1635}
1636
1637.decrypt-form input:focus {
1638 outline: none;
1639 border-color: var(--primary);
1640 box-shadow: 0 0 0 3px oklch(0.75 0.18 195 / 0.15);
1641}
1642
1643.decrypt-form button {
1644 width: 100%;
1645 padding: 0.625rem;
1646 background: var(--primary);
1647 border: none;
1648 border-radius: var(--radius-md);
1649 color: var(--primary-foreground);
1650 font-size: var(--text-sm);
1651 font-weight: 600;
1652 cursor: pointer;
1653 transition: background var(--transition-fast);
1654}
1655
1656.decrypt-form button:hover {
1657 background: oklch(0.8 0.18 195);
1658}
1659
1660.decrypt-error {
1661 color: var(--red);
1662 font-size: var(--text-sm);
1663 margin-top: var(--space-2);
1664}
1665"#;
1666
1667fn generate_print_css() -> String {
1668 String::from(
1669 r#"@media print {
1670 body {
1671 font-size: 11pt;
1672 background: #fff;
1673 color: #000;
1674 }
1675 .message {
1676 border: 1px solid #ddd;
1677 page-break-inside: avoid;
1678 }
1679 pre {
1680 border: 1px solid #ddd;
1681 background: #f5f5f5;
1682 }
1683 a {
1684 color: #000;
1685 text-decoration: underline;
1686 }
1687}"#,
1688 )
1689}
1690
1691#[cfg(test)]
1692mod tests {
1693 use super::*;
1694
1695 #[test]
1696 fn test_generate_styles_includes_colors() {
1697 let opts = ExportOptions::default();
1698 let bundle = generate_styles(&opts);
1699 assert!(bundle.critical_css.contains("--background"));
1700 assert!(bundle.critical_css.contains("--foreground"));
1701 }
1702
1703 #[test]
1704 fn test_generate_styles_includes_search_when_enabled() {
1705 let opts = ExportOptions {
1706 include_search: true,
1707 ..Default::default()
1708 };
1709 let bundle = generate_styles(&opts);
1710 assert!(bundle.critical_css.contains(".search-highlight"));
1711 }
1712
1713 #[test]
1714 fn test_generate_styles_excludes_search_when_disabled() {
1715 let opts = ExportOptions {
1716 include_search: false,
1717 ..Default::default()
1718 };
1719 let bundle = generate_styles(&opts);
1720 assert!(!bundle.critical_css.contains(".search-highlight"));
1721 }
1722
1723 #[test]
1724 fn test_styles_include_theme_toggle_when_enabled() {
1725 let opts = ExportOptions {
1726 include_theme_toggle: true,
1727 ..Default::default()
1728 };
1729 let bundle = generate_styles(&opts);
1730 assert!(bundle.critical_css.contains("[data-theme=\"light\"]"));
1731 }
1732
1733 #[test]
1734 fn test_styles_include_responsive_breakpoints() {
1735 let opts = ExportOptions::default();
1736 let bundle = generate_styles(&opts);
1737 assert!(bundle.critical_css.contains("@media"));
1738 }
1739
1740 #[test]
1741 fn test_print_css_hides_interactive_elements() {
1742 let opts = ExportOptions::default();
1743 let bundle = generate_styles(&opts);
1744 assert!(bundle.print_css.contains("@media print"));
1745 }
1746
1747 #[test]
1748 fn test_styles_include_accessibility() {
1749 let opts = ExportOptions::default();
1750 let bundle = generate_styles(&opts);
1751 assert!(bundle.critical_css.contains("prefers-reduced-motion"));
1752 }
1753
1754 #[test]
1755 fn test_styles_include_animations() {
1756 let opts = ExportOptions::default();
1757 let bundle = generate_styles(&opts);
1758 assert!(bundle.critical_css.contains("@keyframes"));
1759 }
1760
1761 #[test]
1762 fn test_styles_include_glass_morphism() {
1763 let opts = ExportOptions::default();
1764 let bundle = generate_styles(&opts);
1765 assert!(bundle.critical_css.contains("backdrop-filter: blur"));
1766 assert!(bundle.critical_css.contains(".glass"));
1767 }
1768
1769 #[test]
1770 fn test_styles_include_oklch_colors() {
1771 let opts = ExportOptions::default();
1772 let bundle = generate_styles(&opts);
1773 assert!(bundle.critical_css.contains("oklch(0.11 0.015 260)"));
1774 assert!(bundle.critical_css.contains("oklch(0.75 0.18 195)"));
1775 }
1776
1777 #[test]
1778 fn test_styles_include_tool_badge_styling() {
1779 let opts = ExportOptions::default();
1780 let bundle = generate_styles(&opts);
1781
1782 assert!(bundle.critical_css.contains(".tool-badge"));
1784 assert!(bundle.critical_css.contains("min-width: 24px"));
1785 assert!(bundle.critical_css.contains("height: 24px"));
1786
1787 assert!(bundle.critical_css.contains(".tool-status-success"));
1789 assert!(bundle.critical_css.contains(".tool-status-error"));
1790
1791 assert!(bundle.critical_css.contains(".tool-overflow"));
1793 assert!(
1794 bundle
1795 .critical_css
1796 .contains(".tool-badge.tool-overflow-extra")
1797 );
1798 assert!(
1799 bundle
1800 .critical_css
1801 .contains(".message-header-right.expanded .tool-overflow-extra")
1802 );
1803 }
1804
1805 #[test]
1806 fn test_styles_include_glassmorphism_popover() {
1807 let opts = ExportOptions::default();
1808 let bundle = generate_styles(&opts);
1809
1810 assert!(bundle.critical_css.contains(".tool-popover"));
1812 assert!(bundle.critical_css.contains("backdrop-filter: blur(16px)"));
1813 assert!(
1814 bundle
1815 .critical_css
1816 .contains("-webkit-backdrop-filter: blur(16px)")
1817 );
1818
1819 assert!(bundle.critical_css.contains("position: fixed"));
1821
1822 assert!(bundle.critical_css.contains(".tool-popover.visible"));
1824 }
1825
1826 #[test]
1827 fn test_styles_include_mobile_bottom_sheet() {
1828 let opts = ExportOptions::default();
1829 let bundle = generate_styles(&opts);
1830
1831 assert!(bundle.critical_css.contains("max-height: 60vh"));
1833 assert!(
1834 bundle
1835 .critical_css
1836 .contains("border-radius: var(--radius-xl) var(--radius-xl) 0 0")
1837 );
1838 }
1839
1840 #[test]
1841 fn test_styles_include_high_contrast() {
1842 let opts = ExportOptions::default();
1843 let bundle = generate_styles(&opts);
1844
1845 assert!(bundle.critical_css.contains("prefers-contrast: high"));
1847 assert!(bundle.critical_css.contains("border-width: 2px"));
1848 }
1849
1850 #[test]
1851 fn test_styles_include_glow_effects() {
1852 let opts = ExportOptions::default();
1853 let bundle = generate_styles(&opts);
1854
1855 assert!(bundle.critical_css.contains("--shadow-glow"));
1857 assert!(bundle.critical_css.contains("--shadow-glow-amber"));
1858
1859 assert!(
1861 bundle
1862 .critical_css
1863 .contains("box-shadow: var(--shadow-glow-amber)")
1864 );
1865 }
1866
1867 #[test]
1868 fn test_print_styles_hide_popovers() {
1869 let opts = ExportOptions::default();
1870 let bundle = generate_styles(&opts);
1871
1872 assert!(
1874 bundle
1875 .critical_css
1876 .contains(".tool-popover { display: none")
1877 );
1878 }
1879}