mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-02 05:31:02 +00:00
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:
parent
f47bef1ded
commit
eea5daae74
6 changed files with 466 additions and 171 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { Header, StatsRow } from './Header';
|
import { StatsRow } from './Header';
|
||||||
import {
|
import {
|
||||||
AtAGlance,
|
AtAGlance,
|
||||||
NavToc,
|
NavToc,
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
FutureOpportunities,
|
FutureOpportunities,
|
||||||
MemorableMoment,
|
MemorableMoment,
|
||||||
} from './Qualitative';
|
} from './Qualitative';
|
||||||
import { ShareCard } from './ShareCard';
|
import { ShareCard, type Theme } from './ShareCard';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
import { InsightData } from './types';
|
import { InsightData } from './types';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
|
@ -20,6 +20,62 @@ import React from 'react';
|
||||||
|
|
||||||
// Main App Component
|
// Main App Component
|
||||||
function InsightApp({ data }: { data: InsightData }) {
|
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) {
|
if (!data) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center text-slate-600">
|
<div className="text-center text-slate-600">
|
||||||
|
|
@ -42,10 +98,22 @@ function InsightApp({ data }: { data: InsightData }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="header-with-action">
|
{/* Elegant Header */}
|
||||||
<Header data={data} dateRangeStr={dateRangeStr} />
|
<header className="insights-header">
|
||||||
<ShareButton />
|
<div className="header-content">
|
||||||
</div>
|
<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 && (
|
{data.qualitative && (
|
||||||
<>
|
<>
|
||||||
|
|
@ -90,73 +158,118 @@ function InsightApp({ data }: { data: InsightData }) {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ShareCard data={data} />
|
<ShareCard data={data} theme={cardTheme} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Share Button Component
|
// Export Card Button with theme dropdown
|
||||||
function ShareButton() {
|
function ExportCardButton({ onExport }: { onExport: (theme: Theme) => void }) {
|
||||||
const [isExporting, setIsExporting] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const handleExport = async () => {
|
// Close dropdown on outside click
|
||||||
const card = document.getElementById('share-card');
|
useEffect(() => {
|
||||||
if (!card || !window.html2canvas) {
|
if (!isOpen) return;
|
||||||
alert('Export functionality is not available.');
|
const handleClick = (e: MouseEvent) => {
|
||||||
return;
|
if (
|
||||||
}
|
wrapperRef.current &&
|
||||||
|
!wrapperRef.current.contains(e.target as Node)
|
||||||
|
) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('mousedown', handleClick);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClick);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
setIsExporting(true);
|
const handleSelect = (theme: Theme) => {
|
||||||
try {
|
setIsOpen(false);
|
||||||
// Clone the card off-screen so it renders but isn't visible
|
onExport(theme);
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button onClick={handleExport} disabled={isExporting} className="share-btn">
|
<div className="export-dropdown-wrapper" ref={wrapperRef}>
|
||||||
<svg
|
<button className="export-card-btn" onClick={() => setIsOpen(!isOpen)}>
|
||||||
width="16"
|
<svg
|
||||||
height="16"
|
width="14"
|
||||||
viewBox="0 0 24 24"
|
height="14"
|
||||||
fill="none"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
fill="none"
|
||||||
strokeWidth="2"
|
stroke="currentColor"
|
||||||
strokeLinecap="round"
|
strokeWidth="2"
|
||||||
strokeLinejoin="round"
|
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" />
|
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
|
||||||
<line x1="12" y1="2" x2="12" y2="15" />
|
<polyline points="16 6 12 2 8 6" />
|
||||||
</svg>
|
<line x1="12" y1="2" x2="12" y2="15" />
|
||||||
{isExporting ? 'Exporting...' : 'Share as Card'}
|
</svg>
|
||||||
</button>
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,14 +49,14 @@ export function ActiveHoursChart({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Evening',
|
label: 'Evening',
|
||||||
time: '18:00 - 00:00',
|
time: '18:00 - 22:00',
|
||||||
hours: [18, 19, 20, 21, 22, 23],
|
hours: [18, 19, 20, 21],
|
||||||
color: '#6366f1', // indigo-500
|
color: '#6366f1', // indigo-500
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Night',
|
label: 'Night',
|
||||||
time: '00:00 - 06:00',
|
time: '22:00 - 06:00',
|
||||||
hours: [0, 1, 2, 3, 4, 5],
|
hours: [22, 23, 0, 1, 2, 3, 4, 5],
|
||||||
color: '#475569', // slate-600
|
color: '#475569', // slate-600
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -127,15 +127,16 @@ export function HeatmapSection({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${cardClass} mt-4 md:mt-6`}>
|
<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>
|
<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>
|
||||||
<div className="heatmap-container">
|
<div className="heatmap-container">
|
||||||
<div className="min-w-[720px] rounded-xl bg-white/70">
|
<div className="min-w-[720px] rounded-xl bg-white/70">
|
||||||
<ActivityHeatmap heatmapData={heatmap} />
|
<ActivityHeatmap heatmapData={heatmap} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<HeatmapLegend />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -147,7 +148,7 @@ function ActivityHeatmap({
|
||||||
heatmapData: Record<string, number>;
|
heatmapData: Record<string, number>;
|
||||||
}) {
|
}) {
|
||||||
const width = 1000;
|
const width = 1000;
|
||||||
const height = 150;
|
const height = 130;
|
||||||
const cellSize = 14;
|
const cellSize = 14;
|
||||||
const cellPadding = 2;
|
const cellPadding = 2;
|
||||||
|
|
||||||
|
|
@ -164,7 +165,8 @@ function ActivityHeatmap({
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorLevels = [0, 2, 4, 10, 20];
|
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) {
|
function getColor(value: number) {
|
||||||
if (value === 0) return colors[0];
|
if (value === 0) return colors[0];
|
||||||
|
|
@ -270,34 +272,29 @@ function ActivityHeatmap({
|
||||||
{label.text}
|
{label.text}
|
||||||
</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>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export function NavToc() {
|
||||||
return (
|
return (
|
||||||
<nav className="nav-toc">
|
<nav className="nav-toc">
|
||||||
<a href="#section-work">What You Work On</a>
|
<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-wins">Impressive Things</a>
|
||||||
<a href="#section-friction">Where Things Go Wrong</a>
|
<a href="#section-friction">Where Things Go Wrong</a>
|
||||||
<a href="#section-features">Features to Try</a>
|
<a href="#section-features">Features to Try</a>
|
||||||
|
|
@ -283,6 +283,9 @@ function HorizontalBarChart({
|
||||||
}
|
}
|
||||||
entries.sort((a, b) => b[1] - a[1]);
|
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;
|
if (entries.length === 0) return null;
|
||||||
|
|
||||||
const maxValue = Math.max(...entries.map(([, count]) => count));
|
const maxValue = Math.max(...entries.map(([, count]) => count));
|
||||||
|
|
@ -569,7 +572,7 @@ export function Improvements({
|
||||||
id="section-features"
|
id="section-features"
|
||||||
className="text-xl font-semibold text-slate-900 mt-8 mb-4"
|
className="text-xl font-semibold text-slate-900 mt-8 mb-4"
|
||||||
>
|
>
|
||||||
Existing QC Features to Try
|
Existing Qwen Code Features to Try
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* QWEN.md Additions */}
|
{/* QWEN.md Additions */}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,63 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { InsightData } from './types';
|
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.
|
* A hidden 1200x675 card optimized for Twitter/X sharing.
|
||||||
* Rendered off-screen; captured by html2canvas when the user clicks "Share as Card".
|
* 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 {
|
const {
|
||||||
totalMessages = 0,
|
totalMessages = 0,
|
||||||
totalSessions = 0,
|
totalSessions = 0,
|
||||||
|
|
@ -38,15 +90,15 @@ export function ShareCard({ data }: { data: InsightData }) {
|
||||||
const truncatedHeadline = data.qualitative?.memorableMoment?.headline ?? null;
|
const truncatedHeadline = data.qualitative?.memorableMoment?.headline ?? null;
|
||||||
|
|
||||||
// Mini heatmap: last 52 weeks (simplified 7-row grid)
|
// Mini heatmap: last 52 weeks (simplified 7-row grid)
|
||||||
const miniHeatmap = buildMiniHeatmap(data.heatmap || {});
|
const miniHeatmap = buildMiniHeatmap(data.heatmap || {}, t);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="share-card"
|
id="share-card"
|
||||||
style={{
|
style={{
|
||||||
width: '1200px',
|
width: '1200px',
|
||||||
background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 100%)',
|
background: t.background,
|
||||||
color: '#f8fafc',
|
color: t.textPrimary,
|
||||||
fontFamily:
|
fontFamily:
|
||||||
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"',
|
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|
@ -82,7 +134,7 @@ export function ShareCard({ data }: { data: InsightData }) {
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
color: '#94a3b8',
|
color: t.textMuted,
|
||||||
marginTop: '6px',
|
marginTop: '6px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -92,7 +144,7 @@ export function ShareCard({ data }: { data: InsightData }) {
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: '11px',
|
fontSize: '11px',
|
||||||
color: '#64748b',
|
color: t.textMuted,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: '0.15em',
|
letterSpacing: '0.15em',
|
||||||
paddingTop: '8px',
|
paddingTop: '8px',
|
||||||
|
|
@ -111,16 +163,17 @@ export function ShareCard({ data }: { data: InsightData }) {
|
||||||
marginBottom: '32px',
|
marginBottom: '32px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StatBox value={String(totalMessages)} label="Messages" />
|
<StatBox value={String(totalMessages)} label="Messages" theme={t} />
|
||||||
<StatBox value={String(totalSessions)} label="Sessions" />
|
<StatBox value={String(totalSessions)} label="Sessions" theme={t} />
|
||||||
<StatBox
|
<StatBox
|
||||||
value={`+${totalLinesAdded}/-${totalLinesRemoved}`}
|
value={`+${totalLinesAdded}/-${totalLinesRemoved}`}
|
||||||
label="Lines Changed"
|
label="Lines Changed"
|
||||||
small
|
small
|
||||||
|
theme={t}
|
||||||
/>
|
/>
|
||||||
<StatBox value={String(totalFiles)} label="Files" />
|
<StatBox value={String(totalFiles)} label="Files" theme={t} />
|
||||||
<StatBox value={`${currentStreak}d`} label="Streak" />
|
<StatBox value={`${currentStreak}d`} label="Streak" theme={t} />
|
||||||
<StatBox value={`${longestStreak}d`} label="Best Streak" />
|
<StatBox value={`${longestStreak}d`} label="Best Streak" theme={t} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Body: Heatmap + Tools + Moment */}
|
{/* Body: Heatmap + Tools + Moment */}
|
||||||
|
|
@ -135,7 +188,7 @@ export function ShareCard({ data }: { data: InsightData }) {
|
||||||
{/* Left: Mini Heatmap */}
|
{/* Left: Mini Heatmap */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(255,255,255,0.05)',
|
background: t.cardBackground,
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
padding: '20px',
|
padding: '20px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|
@ -146,7 +199,7 @@ export function ShareCard({ data }: { data: InsightData }) {
|
||||||
style={{
|
style={{
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: '#94a3b8',
|
color: t.textMuted,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: '0.08em',
|
letterSpacing: '0.08em',
|
||||||
marginBottom: '12px',
|
marginBottom: '12px',
|
||||||
|
|
@ -164,6 +217,7 @@ export function ShareCard({ data }: { data: InsightData }) {
|
||||||
>
|
>
|
||||||
<MiniHeatmapGrid cells={miniHeatmap} />
|
<MiniHeatmapGrid cells={miniHeatmap} />
|
||||||
</div>
|
</div>
|
||||||
|
<MiniHeatmapLegend theme={t} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right: Active Hours + Moment */}
|
{/* Right: Active Hours + Moment */}
|
||||||
|
|
@ -177,7 +231,7 @@ export function ShareCard({ data }: { data: InsightData }) {
|
||||||
{/* Active Hours */}
|
{/* Active Hours */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(255,255,255,0.05)',
|
background: t.cardBackground,
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
padding: '20px',
|
padding: '20px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|
@ -188,7 +242,7 @@ export function ShareCard({ data }: { data: InsightData }) {
|
||||||
style={{
|
style={{
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: '#94a3b8',
|
color: t.textMuted,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: '0.08em',
|
letterSpacing: '0.08em',
|
||||||
marginBottom: '12px',
|
marginBottom: '12px',
|
||||||
|
|
@ -205,14 +259,14 @@ export function ShareCard({ data }: { data: InsightData }) {
|
||||||
gap: '10px',
|
gap: '10px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ActiveHoursChart activeHours={activeHours} />
|
<ActiveHoursChart activeHours={activeHours} theme={t} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Key Pattern + Memorable Moment */}
|
{/* Key Pattern + Memorable Moment */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(255,255,255,0.04)',
|
background: t.cardBackgroundSecondary,
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
padding: '16px 16px',
|
padding: '16px 16px',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
|
@ -225,7 +279,10 @@ export function ShareCard({ data }: { data: InsightData }) {
|
||||||
left: '12px',
|
left: '12px',
|
||||||
fontSize: '64px',
|
fontSize: '64px',
|
||||||
fontWeight: 700,
|
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,
|
lineHeight: 1,
|
||||||
fontFamily: 'Georgia, "Times New Roman", serif',
|
fontFamily: 'Georgia, "Times New Roman", serif',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
|
|
@ -244,7 +301,7 @@ export function ShareCard({ data }: { data: InsightData }) {
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: '13px',
|
fontSize: '13px',
|
||||||
color: '#e2e8f0',
|
color: t.textSecondary,
|
||||||
lineHeight: 1.6,
|
lineHeight: 1.6,
|
||||||
marginBottom: truncatedHeadline ? '8px' : 0,
|
marginBottom: truncatedHeadline ? '8px' : 0,
|
||||||
}}
|
}}
|
||||||
|
|
@ -256,7 +313,7 @@ export function ShareCard({ data }: { data: InsightData }) {
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
color: '#94a3b8',
|
color: t.textMuted,
|
||||||
lineHeight: 1.5,
|
lineHeight: 1.5,
|
||||||
fontStyle: 'italic',
|
fontStyle: 'italic',
|
||||||
}}
|
}}
|
||||||
|
|
@ -277,14 +334,14 @@ export function ShareCard({ data }: { data: InsightData }) {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginTop: 'auto',
|
marginTop: 'auto',
|
||||||
paddingTop: '24px',
|
paddingTop: '24px',
|
||||||
borderTop: '1px solid rgba(255,255,255,0.08)',
|
borderTop: `1px solid ${t.borderColor}`,
|
||||||
flexShrink: 0,
|
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]}
|
Generated by Qwen Code · {new Date().toISOString().split('T')[0]}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '12px', color: '#64748b' }}>
|
<div style={{ fontSize: '12px', color: t.textMuted }}>
|
||||||
github.com/QwenLM/qwen-code
|
github.com/QwenLM/qwen-code
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -296,10 +353,12 @@ function StatBox({
|
||||||
value,
|
value,
|
||||||
label,
|
label,
|
||||||
small,
|
small,
|
||||||
|
theme,
|
||||||
}: {
|
}: {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
small?: boolean;
|
small?: boolean;
|
||||||
|
theme: ThemeConfig;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
|
@ -307,7 +366,7 @@ function StatBox({
|
||||||
style={{
|
style={{
|
||||||
fontSize: small ? '18px' : '28px',
|
fontSize: small ? '18px' : '28px',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: '#f8fafc',
|
color: theme.textPrimary,
|
||||||
lineHeight: 1.2,
|
lineHeight: 1.2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -316,7 +375,7 @@ function StatBox({
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: '11px',
|
fontSize: '11px',
|
||||||
color: '#64748b',
|
color: theme.textMuted,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: '0.05em',
|
letterSpacing: '0.05em',
|
||||||
marginTop: '4px',
|
marginTop: '4px',
|
||||||
|
|
@ -330,8 +389,10 @@ function StatBox({
|
||||||
|
|
||||||
function ActiveHoursChart({
|
function ActiveHoursChart({
|
||||||
activeHours,
|
activeHours,
|
||||||
|
theme,
|
||||||
}: {
|
}: {
|
||||||
activeHours: { [hour: number]: number };
|
activeHours: { [hour: number]: number };
|
||||||
|
theme: ThemeConfig;
|
||||||
}) {
|
}) {
|
||||||
const phases = [
|
const phases = [
|
||||||
{
|
{
|
||||||
|
|
@ -348,14 +409,14 @@ function ActiveHoursChart({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Evening',
|
label: 'Evening',
|
||||||
time: '18–00',
|
time: '18–22',
|
||||||
hours: [18, 19, 20, 21, 22, 23],
|
hours: [18, 19, 20, 21],
|
||||||
color: '#6366f1',
|
color: '#6366f1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Night',
|
label: 'Night',
|
||||||
time: '00–06',
|
time: '22–06',
|
||||||
hours: [0, 1, 2, 3, 4, 5],
|
hours: [22, 23, 0, 1, 2, 3, 4, 5],
|
||||||
color: '#475569',
|
color: '#475569',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -394,21 +455,21 @@ function ActiveHoursChart({
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span style={{ color: '#e2e8f0', fontWeight: 500 }}>
|
<span style={{ color: theme.textSecondary, fontWeight: 500 }}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ color: '#64748b', fontSize: '11px' }}>
|
<span style={{ color: theme.textMuted, fontSize: '11px' }}>
|
||||||
{item.time}
|
{item.time}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span style={{ color: '#94a3b8', fontWeight: 600 }}>
|
<span style={{ color: theme.textMuted, fontWeight: 600 }}>
|
||||||
{item.total}
|
{item.total}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: '6px',
|
height: '6px',
|
||||||
background: 'rgba(255,255,255,0.08)',
|
background: theme.borderColor,
|
||||||
borderRadius: '3px',
|
borderRadius: '3px',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
|
|
@ -432,6 +493,7 @@ function ActiveHoursChart({
|
||||||
/** Build a 7x~26 grid of intensity values for the mini heatmap (last ~6 months). */
|
/** Build a 7x~26 grid of intensity values for the mini heatmap (last ~6 months). */
|
||||||
function buildMiniHeatmap(
|
function buildMiniHeatmap(
|
||||||
heatmap: Record<string, number>,
|
heatmap: Record<string, number>,
|
||||||
|
theme: ThemeConfig,
|
||||||
): { color: string }[] {
|
): { color: string }[] {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const weeksToShow = 26;
|
const weeksToShow = 26;
|
||||||
|
|
@ -451,20 +513,20 @@ function buildMiniHeatmap(
|
||||||
while (d <= endDate) {
|
while (d <= endDate) {
|
||||||
const key = d.toISOString().split('T')[0];
|
const key = d.toISOString().split('T')[0];
|
||||||
const val = heatmap[key] || 0;
|
const val = heatmap[key] || 0;
|
||||||
cells.push({ color: heatColor(val) });
|
cells.push({ color: heatColor(val, theme) });
|
||||||
d.setDate(d.getDate() + 1);
|
d.setDate(d.getDate() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cells;
|
return cells;
|
||||||
}
|
}
|
||||||
|
|
||||||
function heatColor(val: number): string {
|
// GitHub contribution graph color palette - theme aware
|
||||||
if (val === 0) return 'rgba(255,255,255,0.06)';
|
function heatColor(val: number, theme: ThemeConfig): string {
|
||||||
if (val < 2) return '#1e3a5f';
|
if (val === 0) return theme.heatmapEmpty;
|
||||||
if (val < 4) return '#2563eb';
|
if (val < 2) return theme.heatmapColors[0];
|
||||||
if (val < 10) return '#3b82f6';
|
if (val < 4) return theme.heatmapColors[1];
|
||||||
if (val < 20) return '#60a5fa';
|
if (val < 10) return theme.heatmapColors[2];
|
||||||
return '#93c5fd';
|
return theme.heatmapColors[3];
|
||||||
}
|
}
|
||||||
|
|
||||||
function MiniHeatmapGrid({ cells }: { cells: { color: string }[] }) {
|
function MiniHeatmapGrid({ cells }: { cells: { color: string }[] }) {
|
||||||
|
|
@ -499,3 +561,38 @@ function MiniHeatmapGrid({ cells }: { cells: { color: string }[] }) {
|
||||||
</svg>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1136,42 +1136,127 @@ body {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Share Button */
|
/* Header Styles */
|
||||||
.share-btn {
|
.insights-header {
|
||||||
position: absolute;
|
margin-bottom: 2rem;
|
||||||
top: 0;
|
padding: 1.5rem 0;
|
||||||
right: 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;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 0.5rem;
|
||||||
border: none;
|
height: 36px;
|
||||||
|
padding: 0 0.875rem;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 10px 20px;
|
background: white;
|
||||||
font-size: 14px;
|
color: #334155;
|
||||||
font-weight: 600;
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #fff;
|
transition: all 0.15s ease;
|
||||||
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
|
|
||||||
transition: all 0.2s;
|
|
||||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-btn:hover:not(:disabled) {
|
.export-card-btn:hover {
|
||||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
background: #f8fafc;
|
||||||
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.3);
|
border-color: #cbd5e1;
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-btn:active:not(:disabled) {
|
.export-card-btn:active {
|
||||||
transform: translateY(0);
|
background: #f1f5f9;
|
||||||
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-btn:disabled {
|
.export-chevron {
|
||||||
|
transition: transform 0.15s ease;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-btn svg {
|
.export-chevron.open {
|
||||||
opacity: 0.8;
|
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
Loading…
Add table
Add a link
Reference in a new issue