feat(insight): polish insight page UI

- Add share card theme selection (light/dark) with contextual export controls
- Update heatmap colors to GitHub green palette and fix time ranges
- Limit bar charts to 10 items, use full Qwen Code name

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
tanzhenxin 2026-02-26 14:07:16 +08:00
parent f47bef1ded
commit eea5daae74
6 changed files with 466 additions and 171 deletions

View file

@ -1,6 +1,6 @@
import { useState } from 'react';
import { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom/client';
import { Header, StatsRow } from './Header';
import { StatsRow } from './Header';
import {
AtAGlance,
NavToc,
@ -12,7 +12,7 @@ import {
FutureOpportunities,
MemorableMoment,
} from './Qualitative';
import { ShareCard } from './ShareCard';
import { ShareCard, type Theme } from './ShareCard';
import './styles.css';
import { InsightData } from './types';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -20,6 +20,62 @@ import React from 'react';
// Main App Component
function InsightApp({ data }: { data: InsightData }) {
const [cardTheme, setCardTheme] = useState<Theme>('dark');
const pendingExport = useRef(false);
const performExport = async () => {
const card = document.getElementById('share-card');
if (!card || !window.html2canvas) {
alert('Export functionality is not available.');
return;
}
try {
const clone = card.cloneNode(true) as HTMLElement;
clone.style.position = 'fixed';
clone.style.left = '-9999px';
clone.style.top = '0';
clone.style.pointerEvents = 'none';
document.body.appendChild(clone);
const canvas = await window.html2canvas(clone, {
scale: 2,
useCORS: true,
logging: false,
width: 1200,
height: clone.scrollHeight,
});
document.body.removeChild(clone);
const imgData = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.href = imgData;
link.download = `qwen-insights-card-${new Date().toISOString().slice(0, 10)}.png`;
link.click();
} catch (error) {
console.error('Export card error:', error);
alert('Failed to export card. Please try again.');
}
};
// Export after React re-renders the card with the new theme
useEffect(() => {
if (pendingExport.current) {
pendingExport.current = false;
performExport();
}
}, [cardTheme]);
const handleExportWithTheme = (theme: Theme) => {
if (theme === cardTheme) {
performExport();
} else {
pendingExport.current = true;
setCardTheme(theme);
}
};
if (!data) {
return (
<div className="text-center text-slate-600">
@ -42,10 +98,22 @@ function InsightApp({ data }: { data: InsightData }) {
return (
<div>
<div className="header-with-action">
<Header data={data} dateRangeStr={dateRangeStr} />
<ShareButton />
</div>
{/* Elegant Header */}
<header className="insights-header">
<div className="header-content">
<div className="header-title-section">
<h1 className="header-title">Qwen Code Insights</h1>
<p className="header-subtitle">
{data.totalMessages
? `${data.totalMessages.toLocaleString()} messages across ${data.totalSessions?.toLocaleString()} sessions`
: 'Your personalized coding journey and patterns'}
{dateRangeStr && ` · ${dateRangeStr}`}
</p>
</div>
<ExportCardButton onExport={handleExportWithTheme} />
</div>
</header>
{data.qualitative && (
<>
@ -90,73 +158,118 @@ function InsightApp({ data }: { data: InsightData }) {
</>
)}
<ShareCard data={data} />
<ShareCard data={data} theme={cardTheme} />
</div>
);
}
// Share Button Component
function ShareButton() {
const [isExporting, setIsExporting] = useState(false);
// Export Card Button with theme dropdown
function ExportCardButton({ onExport }: { onExport: (theme: Theme) => void }) {
const [isOpen, setIsOpen] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
const handleExport = async () => {
const card = document.getElementById('share-card');
if (!card || !window.html2canvas) {
alert('Export functionality is not available.');
return;
}
// Close dropdown on outside click
useEffect(() => {
if (!isOpen) return;
const handleClick = (e: MouseEvent) => {
if (
wrapperRef.current &&
!wrapperRef.current.contains(e.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
}, [isOpen]);
setIsExporting(true);
try {
// Clone the card off-screen so it renders but isn't visible
const clone = card.cloneNode(true) as HTMLElement;
clone.style.position = 'fixed';
clone.style.left = '-9999px';
clone.style.top = '0';
clone.style.pointerEvents = 'none';
document.body.appendChild(clone);
const canvas = await window.html2canvas(clone, {
scale: 2,
useCORS: true,
logging: false,
width: 1200,
height: clone.scrollHeight,
});
document.body.removeChild(clone);
const imgData = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.href = imgData;
link.download = `qwen-insights-card-${new Date().toISOString().slice(0, 10)}.png`;
link.click();
} catch (error) {
console.error('Export card error:', error);
alert('Failed to export card. Please try again.');
} finally {
setIsExporting(false);
}
const handleSelect = (theme: Theme) => {
setIsOpen(false);
onExport(theme);
};
return (
<button onClick={handleExport} disabled={isExporting} className="share-btn">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
<polyline points="16 6 12 2 8 6" />
<line x1="12" y1="2" x2="12" y2="15" />
</svg>
{isExporting ? 'Exporting...' : 'Share as Card'}
</button>
<div className="export-dropdown-wrapper" ref={wrapperRef}>
<button className="export-card-btn" onClick={() => setIsOpen(!isOpen)}>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
<polyline points="16 6 12 2 8 6" />
<line x1="12" y1="2" x2="12" y2="15" />
</svg>
<span>Export Card</span>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={`export-chevron ${isOpen ? 'open' : ''}`}
>
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
{isOpen && (
<div className="export-dropdown">
<button
className="export-dropdown-item"
onClick={() => handleSelect('light')}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="5" />
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</svg>
<span>Light Theme</span>
</button>
<button
className="export-dropdown-item"
onClick={() => handleSelect('dark')}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
<span>Dark Theme</span>
</button>
</div>
)}
</div>
);
}

View file

@ -49,14 +49,14 @@ export function ActiveHoursChart({
},
{
label: 'Evening',
time: '18:00 - 00:00',
hours: [18, 19, 20, 21, 22, 23],
time: '18:00 - 22:00',
hours: [18, 19, 20, 21],
color: '#6366f1', // indigo-500
},
{
label: 'Night',
time: '00:00 - 06:00',
hours: [0, 1, 2, 3, 4, 5],
time: '22:00 - 06:00',
hours: [22, 23, 0, 1, 2, 3, 4, 5],
color: '#475569', // slate-600
},
];
@ -127,15 +127,16 @@ export function HeatmapSection({
return (
<div className={`${cardClass} mt-4 md:mt-6`}>
<div className="flex items-center justify-between">
<div className="mb-3">
<h3 className={sectionTitleClass}>Activity Heatmap</h3>
<span className="text-xs font-semibold text-slate-500">Past year</span>
<p className="text-xs text-slate-500">Showing past year of activity</p>
</div>
<div className="heatmap-container">
<div className="min-w-[720px] rounded-xl bg-white/70">
<ActivityHeatmap heatmapData={heatmap} />
</div>
</div>
<HeatmapLegend />
</div>
);
}
@ -147,7 +148,7 @@ function ActivityHeatmap({
heatmapData: Record<string, number>;
}) {
const width = 1000;
const height = 150;
const height = 130;
const cellSize = 14;
const cellPadding = 2;
@ -164,7 +165,8 @@ function ActivityHeatmap({
}
const colorLevels = [0, 2, 4, 10, 20];
const colors = ['#e2e8f0', '#a5d8ff', '#74c0fc', '#339af0', '#1c7ed6'];
// GitHub contribution graph color palette (green)
const colors = ['#ebedf0', '#9be9a8', '#40c463', '#30a14e', '#216e39'];
function getColor(value: number) {
if (value === 0) return colors[0];
@ -270,34 +272,29 @@ function ActivityHeatmap({
{label.text}
</text>
))}
{/* Render legend */}
<text x={startX} y={height - 40} fontSize="12" fill="#64748b">
Less
</text>
{colors.map((color, index) => {
const legendX = startX + 40 + index * (cellSize + 2);
return (
<rect
key={index}
x={legendX}
y={height - 30}
width="10"
height="10"
rx="2"
fill={color}
/>
);
})}
<text
x={startX + 40 + colors.length * (cellSize + 2) + 5}
y={height - 21}
fontSize="12"
fill="#64748b"
width={cellSize}
>
More
</text>
</svg>
);
}
// Heatmap Legend Component (outside SVG)
function HeatmapLegend() {
const colors = ['#ebedf0', '#9be9a8', '#40c463', '#30a14e', '#216e39'];
return (
<div className="flex items-center gap-2 mt-4">
<span className="text-xs text-slate-500">Less</span>
{colors.map((color, index) => (
<span
key={index}
className="inline-block rounded"
style={{
width: '10px',
height: '10px',
backgroundColor: color,
}}
/>
))}
<span className="text-xs text-slate-500">More</span>
</div>
);
}

View file

@ -55,7 +55,7 @@ export function NavToc() {
return (
<nav className="nav-toc">
<a href="#section-work">What You Work On</a>
<a href="#section-usage">How You Use QC</a>
<a href="#section-usage">How You Use Qwen Code</a>
<a href="#section-wins">Impressive Things</a>
<a href="#section-friction">Where Things Go Wrong</a>
<a href="#section-features">Features to Try</a>
@ -283,6 +283,9 @@ function HorizontalBarChart({
}
entries.sort((a, b) => b[1] - a[1]);
// Limit to at most 10 items
entries = entries.slice(0, 10);
if (entries.length === 0) return null;
const maxValue = Math.max(...entries.map(([, count]) => count));
@ -569,7 +572,7 @@ export function Improvements({
id="section-features"
className="text-xl font-semibold text-slate-900 mt-8 mb-4"
>
Existing QC Features to Try
Existing Qwen Code Features to Try
</h2>
{/* QWEN.md Additions */}

View file

@ -2,11 +2,63 @@
import React from 'react';
import { InsightData } from './types';
/**
* Theme configuration for the share card
*/
export type Theme = 'light' | 'dark';
interface ThemeConfig {
background: string;
textPrimary: string;
textSecondary: string;
textMuted: string;
cardBackground: string;
cardBackgroundSecondary: string;
borderColor: string;
heatmapColors: string[];
heatmapEmpty: string;
}
const themes: Record<Theme, ThemeConfig> = {
light: {
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
textPrimary: '#0f172a',
textSecondary: '#475569',
textMuted: '#64748b',
cardBackground: 'rgba(255,255,255,0.7)',
cardBackgroundSecondary: 'rgba(255,255,255,0.5)',
borderColor: '#e2e8f0',
// GitHub contribution graph color palette (light mode)
heatmapColors: ['#9be9a8', '#40c463', '#30a14e', '#216e39'],
heatmapEmpty: '#ebedf0',
},
dark: {
background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 100%)',
textPrimary: '#f8fafc',
textSecondary: '#e2e8f0',
textMuted: '#94a3b8',
cardBackground: 'rgba(255,255,255,0.05)',
cardBackgroundSecondary: 'rgba(255,255,255,0.04)',
borderColor: 'rgba(255,255,255,0.08)',
// GitHub contribution graph color palette (dark mode)
heatmapColors: ['#0e4429', '#006d32', '#26a641', '#39d353'],
heatmapEmpty: '#2d333b',
},
};
/**
* A hidden 1200x675 card optimized for Twitter/X sharing.
* Rendered off-screen; captured by html2canvas when the user clicks "Share as Card".
*/
export function ShareCard({ data }: { data: InsightData }) {
export function ShareCard({
data,
theme = 'light',
}: {
data: InsightData;
theme?: Theme;
}) {
const t = themes[theme];
const {
totalMessages = 0,
totalSessions = 0,
@ -38,15 +90,15 @@ export function ShareCard({ data }: { data: InsightData }) {
const truncatedHeadline = data.qualitative?.memorableMoment?.headline ?? null;
// Mini heatmap: last 52 weeks (simplified 7-row grid)
const miniHeatmap = buildMiniHeatmap(data.heatmap || {});
const miniHeatmap = buildMiniHeatmap(data.heatmap || {}, t);
return (
<div
id="share-card"
style={{
width: '1200px',
background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 100%)',
color: '#f8fafc',
background: t.background,
color: t.textPrimary,
fontFamily:
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"',
display: 'flex',
@ -82,7 +134,7 @@ export function ShareCard({ data }: { data: InsightData }) {
<div
style={{
fontSize: '14px',
color: '#94a3b8',
color: t.textMuted,
marginTop: '6px',
}}
>
@ -92,7 +144,7 @@ export function ShareCard({ data }: { data: InsightData }) {
<div
style={{
fontSize: '11px',
color: '#64748b',
color: t.textMuted,
textTransform: 'uppercase',
letterSpacing: '0.15em',
paddingTop: '8px',
@ -111,16 +163,17 @@ export function ShareCard({ data }: { data: InsightData }) {
marginBottom: '32px',
}}
>
<StatBox value={String(totalMessages)} label="Messages" />
<StatBox value={String(totalSessions)} label="Sessions" />
<StatBox value={String(totalMessages)} label="Messages" theme={t} />
<StatBox value={String(totalSessions)} label="Sessions" theme={t} />
<StatBox
value={`+${totalLinesAdded}/-${totalLinesRemoved}`}
label="Lines Changed"
small
theme={t}
/>
<StatBox value={String(totalFiles)} label="Files" />
<StatBox value={`${currentStreak}d`} label="Streak" />
<StatBox value={`${longestStreak}d`} label="Best Streak" />
<StatBox value={String(totalFiles)} label="Files" theme={t} />
<StatBox value={`${currentStreak}d`} label="Streak" theme={t} />
<StatBox value={`${longestStreak}d`} label="Best Streak" theme={t} />
</div>
{/* Body: Heatmap + Tools + Moment */}
@ -135,7 +188,7 @@ export function ShareCard({ data }: { data: InsightData }) {
{/* Left: Mini Heatmap */}
<div
style={{
background: 'rgba(255,255,255,0.05)',
background: t.cardBackground,
borderRadius: '12px',
padding: '20px',
display: 'flex',
@ -146,7 +199,7 @@ export function ShareCard({ data }: { data: InsightData }) {
style={{
fontSize: '12px',
fontWeight: 600,
color: '#94a3b8',
color: t.textMuted,
textTransform: 'uppercase',
letterSpacing: '0.08em',
marginBottom: '12px',
@ -164,6 +217,7 @@ export function ShareCard({ data }: { data: InsightData }) {
>
<MiniHeatmapGrid cells={miniHeatmap} />
</div>
<MiniHeatmapLegend theme={t} />
</div>
{/* Right: Active Hours + Moment */}
@ -177,7 +231,7 @@ export function ShareCard({ data }: { data: InsightData }) {
{/* Active Hours */}
<div
style={{
background: 'rgba(255,255,255,0.05)',
background: t.cardBackground,
borderRadius: '12px',
padding: '20px',
display: 'flex',
@ -188,7 +242,7 @@ export function ShareCard({ data }: { data: InsightData }) {
style={{
fontSize: '12px',
fontWeight: 600,
color: '#94a3b8',
color: t.textMuted,
textTransform: 'uppercase',
letterSpacing: '0.08em',
marginBottom: '12px',
@ -205,14 +259,14 @@ export function ShareCard({ data }: { data: InsightData }) {
gap: '10px',
}}
>
<ActiveHoursChart activeHours={activeHours} />
<ActiveHoursChart activeHours={activeHours} theme={t} />
</div>
</div>
{/* Key Pattern + Memorable Moment */}
<div
style={{
background: 'rgba(255,255,255,0.04)',
background: t.cardBackgroundSecondary,
borderRadius: '12px',
padding: '16px 16px',
position: 'relative',
@ -225,7 +279,10 @@ export function ShareCard({ data }: { data: InsightData }) {
left: '12px',
fontSize: '64px',
fontWeight: 700,
color: 'rgba(99,102,241,0.2)',
color:
theme === 'light'
? 'rgba(99,102,241,0.15)'
: 'rgba(99,102,241,0.2)',
lineHeight: 1,
fontFamily: 'Georgia, "Times New Roman", serif',
userSelect: 'none',
@ -244,7 +301,7 @@ export function ShareCard({ data }: { data: InsightData }) {
<div
style={{
fontSize: '13px',
color: '#e2e8f0',
color: t.textSecondary,
lineHeight: 1.6,
marginBottom: truncatedHeadline ? '8px' : 0,
}}
@ -256,7 +313,7 @@ export function ShareCard({ data }: { data: InsightData }) {
<div
style={{
fontSize: '12px',
color: '#94a3b8',
color: t.textMuted,
lineHeight: 1.5,
fontStyle: 'italic',
}}
@ -277,14 +334,14 @@ export function ShareCard({ data }: { data: InsightData }) {
alignItems: 'center',
marginTop: 'auto',
paddingTop: '24px',
borderTop: '1px solid rgba(255,255,255,0.08)',
borderTop: `1px solid ${t.borderColor}`,
flexShrink: 0,
}}
>
<div style={{ fontSize: '12px', color: '#64748b' }}>
<div style={{ fontSize: '12px', color: t.textMuted }}>
Generated by Qwen Code · {new Date().toISOString().split('T')[0]}
</div>
<div style={{ fontSize: '12px', color: '#64748b' }}>
<div style={{ fontSize: '12px', color: t.textMuted }}>
github.com/QwenLM/qwen-code
</div>
</div>
@ -296,10 +353,12 @@ function StatBox({
value,
label,
small,
theme,
}: {
value: string;
label: string;
small?: boolean;
theme: ThemeConfig;
}) {
return (
<div style={{ textAlign: 'center' }}>
@ -307,7 +366,7 @@ function StatBox({
style={{
fontSize: small ? '18px' : '28px',
fontWeight: 700,
color: '#f8fafc',
color: theme.textPrimary,
lineHeight: 1.2,
}}
>
@ -316,7 +375,7 @@ function StatBox({
<div
style={{
fontSize: '11px',
color: '#64748b',
color: theme.textMuted,
textTransform: 'uppercase',
letterSpacing: '0.05em',
marginTop: '4px',
@ -330,8 +389,10 @@ function StatBox({
function ActiveHoursChart({
activeHours,
theme,
}: {
activeHours: { [hour: number]: number };
theme: ThemeConfig;
}) {
const phases = [
{
@ -348,14 +409,14 @@ function ActiveHoursChart({
},
{
label: 'Evening',
time: '1800',
hours: [18, 19, 20, 21, 22, 23],
time: '1822',
hours: [18, 19, 20, 21],
color: '#6366f1',
},
{
label: 'Night',
time: '0006',
hours: [0, 1, 2, 3, 4, 5],
time: '2206',
hours: [22, 23, 0, 1, 2, 3, 4, 5],
color: '#475569',
},
];
@ -394,21 +455,21 @@ function ActiveHoursChart({
flexShrink: 0,
}}
/>
<span style={{ color: '#e2e8f0', fontWeight: 500 }}>
<span style={{ color: theme.textSecondary, fontWeight: 500 }}>
{item.label}
</span>
<span style={{ color: '#64748b', fontSize: '11px' }}>
<span style={{ color: theme.textMuted, fontSize: '11px' }}>
{item.time}
</span>
</div>
<span style={{ color: '#94a3b8', fontWeight: 600 }}>
<span style={{ color: theme.textMuted, fontWeight: 600 }}>
{item.total}
</span>
</div>
<div
style={{
height: '6px',
background: 'rgba(255,255,255,0.08)',
background: theme.borderColor,
borderRadius: '3px',
overflow: 'hidden',
}}
@ -432,6 +493,7 @@ function ActiveHoursChart({
/** Build a 7x~26 grid of intensity values for the mini heatmap (last ~6 months). */
function buildMiniHeatmap(
heatmap: Record<string, number>,
theme: ThemeConfig,
): { color: string }[] {
const today = new Date();
const weeksToShow = 26;
@ -451,20 +513,20 @@ function buildMiniHeatmap(
while (d <= endDate) {
const key = d.toISOString().split('T')[0];
const val = heatmap[key] || 0;
cells.push({ color: heatColor(val) });
cells.push({ color: heatColor(val, theme) });
d.setDate(d.getDate() + 1);
}
return cells;
}
function heatColor(val: number): string {
if (val === 0) return 'rgba(255,255,255,0.06)';
if (val < 2) return '#1e3a5f';
if (val < 4) return '#2563eb';
if (val < 10) return '#3b82f6';
if (val < 20) return '#60a5fa';
return '#93c5fd';
// GitHub contribution graph color palette - theme aware
function heatColor(val: number, theme: ThemeConfig): string {
if (val === 0) return theme.heatmapEmpty;
if (val < 2) return theme.heatmapColors[0];
if (val < 4) return theme.heatmapColors[1];
if (val < 10) return theme.heatmapColors[2];
return theme.heatmapColors[3];
}
function MiniHeatmapGrid({ cells }: { cells: { color: string }[] }) {
@ -499,3 +561,38 @@ function MiniHeatmapGrid({ cells }: { cells: { color: string }[] }) {
</svg>
);
}
// Mini Heatmap Legend Component for ShareCard (theme aware)
function MiniHeatmapLegend({ theme }: { theme: ThemeConfig }) {
return (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginTop: '12px',
}}
>
<span style={{ fontSize: '11px', color: theme.textMuted }}>Less</span>
{[
theme.heatmapEmpty,
theme.heatmapColors[0],
theme.heatmapColors[1],
theme.heatmapColors[2],
theme.heatmapColors[3],
].map((color, index) => (
<span
key={index}
style={{
width: '10px',
height: '10px',
borderRadius: '2px',
backgroundColor: color,
display: 'inline-block',
}}
/>
))}
<span style={{ fontSize: '11px', color: theme.textMuted }}>More</span>
</div>
);
}

View file

@ -1136,42 +1136,127 @@ body {
position: relative;
}
/* Share Button */
.share-btn {
position: absolute;
top: 0;
right: 0;
/* Header Styles */
.insights-header {
margin-bottom: 2rem;
padding: 1.5rem 0;
border-bottom: 1px solid #e2e8f0;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1.5rem;
flex-wrap: wrap;
}
.header-title-section {
flex: 1;
min-width: 0;
}
.header-title {
font-size: 1.75rem;
font-weight: 700;
color: #0f172a;
letter-spacing: -0.02em;
margin: 0 0 0.375rem 0;
line-height: 1.2;
}
.header-subtitle {
font-size: 0.875rem;
color: #64748b;
margin: 0;
font-weight: 400;
}
/* Export Card Dropdown */
.export-dropdown-wrapper {
position: relative;
}
.export-card-btn {
display: inline-flex;
align-items: center;
gap: 8px;
border: none;
gap: 0.5rem;
height: 36px;
padding: 0 0.875rem;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 10px 20px;
font-size: 14px;
font-weight: 600;
background: white;
color: #334155;
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
color: #fff;
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
transition: all 0.2s;
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.2);
transition: all 0.15s ease;
}
.share-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.3);
transform: translateY(-1px);
.export-card-btn:hover {
background: #f8fafc;
border-color: #cbd5e1;
}
.share-btn:active:not(:disabled) {
transform: translateY(0);
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.2);
.export-card-btn:active {
background: #f1f5f9;
}
.share-btn:disabled {
.export-chevron {
transition: transform 0.15s ease;
opacity: 0.5;
cursor: not-allowed;
}
.share-btn svg {
opacity: 0.8;
.export-chevron.open {
transform: rotate(180deg);
}
.export-dropdown {
position: absolute;
top: calc(100% + 4px);
right: 0;
min-width: 160px;
background: white;
border: 1px solid #e2e8f0;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
padding: 4px;
z-index: 50;
}
.export-dropdown-item {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.5rem 0.75rem;
border: none;
border-radius: 6px;
background: none;
color: #334155;
font-size: 0.8125rem;
font-weight: 400;
cursor: pointer;
transition: background 0.1s ease;
}
.export-dropdown-item:hover {
background: #f1f5f9;
}
.export-dropdown-item:active {
background: #e2e8f0;
}
/* Responsive adjustments */
@media (max-width: 640px) {
.header-content {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.header-title {
font-size: 1.5rem;
}
}

File diff suppressed because one or more lines are too long