Add new styles for home and landing pages
- Created home.css with comprehensive styles for the home page layout, including typography, buttons, and responsive design adjustments. - Created landing.css to style the landing page, focusing on typography, layout, and responsive behavior for various screen sizes.
This commit is contained in:
245
src/components/design-system/CalendarHeatmap.tsx
Normal file
245
src/components/design-system/CalendarHeatmap.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import { useEffect, useId, useRef, useState, type ReactNode } from "react";
|
||||
import { CalendarLegend } from "./CalendarLegend";
|
||||
import {
|
||||
describeDay,
|
||||
progressShade,
|
||||
MONTH_VIEWS,
|
||||
visibleCalendarDays,
|
||||
type CalendarDay,
|
||||
type MonthView,
|
||||
} from "./calendar-model";
|
||||
|
||||
export function CalendarHeatmap({
|
||||
days: allDays,
|
||||
unit,
|
||||
label,
|
||||
color = "#111111",
|
||||
emptyColor = "#eeeeee",
|
||||
compact = false,
|
||||
selectedDate: controlledDate,
|
||||
onSelectDate,
|
||||
historyLabel,
|
||||
legend,
|
||||
}: {
|
||||
days: CalendarDay[];
|
||||
unit: string;
|
||||
label: string;
|
||||
color?: string;
|
||||
emptyColor?: string;
|
||||
compact?: boolean;
|
||||
selectedDate?: string;
|
||||
onSelectDate?: (date: string) => void;
|
||||
historyLabel?: string;
|
||||
legend?: ReactNode;
|
||||
}) {
|
||||
const [months, setMonths] = useState<MonthView>(6);
|
||||
const latestDate =
|
||||
allDays.findLast((day) => day.state !== "future")?.date ??
|
||||
allDays.at(-1)?.date;
|
||||
// A date selected in the editor must stay visible even outside the current window.
|
||||
const anchor =
|
||||
controlledDate && allDays.some((day) => day.date === controlledDate)
|
||||
? controlledDate
|
||||
: latestDate;
|
||||
const [localSelectedDate, setLocalSelectedDate] = useState(anchor);
|
||||
const selectedDate = controlledDate ?? localSelectedDate;
|
||||
function setSelectedDate(date: string) {
|
||||
if (onSelectDate) onSelectDate(date);
|
||||
else setLocalSelectedDate(date);
|
||||
}
|
||||
const days = anchor ? visibleCalendarDays(allDays, months, anchor) : [];
|
||||
const selected =
|
||||
days.find((day) => day.date === selectedDate) ??
|
||||
days.find((day) => day.date === anchor) ??
|
||||
days[0];
|
||||
const offset = days[0]
|
||||
? new Date(`${days[0].date}T12:00:00Z`).getUTCDay()
|
||||
: 0;
|
||||
const weeks = Math.ceil((days.length + offset) / 7);
|
||||
const buttons = useRef<(HTMLButtonElement | null)[]>([]);
|
||||
const scrollArea = useRef<HTMLDivElement | null>(null);
|
||||
const inspectorId = useId();
|
||||
const selectedIndex = selected ? days.indexOf(selected) : -1;
|
||||
useEffect(() => {
|
||||
const container = scrollArea.current;
|
||||
const button = buttons.current[selectedIndex];
|
||||
if (!container || !button) return;
|
||||
const cellBounds = button.getBoundingClientRect();
|
||||
const bounds = container.getBoundingClientRect();
|
||||
if (cellBounds.right > bounds.right - 5)
|
||||
container.scrollLeft += cellBounds.right - bounds.right + 5;
|
||||
else if (cellBounds.left < bounds.left + 5)
|
||||
container.scrollLeft += cellBounds.left - bounds.left - 5;
|
||||
}, [months, selectedIndex]);
|
||||
if (!selected) return <p>No calendar dates to display.</p>;
|
||||
return (
|
||||
<div
|
||||
className={`ds-calendar${compact ? " ds-calendar--compact" : ""}`}
|
||||
data-months={months}
|
||||
>
|
||||
<div className="ds-calendar-range-toolbar">
|
||||
<span className="ds-calendar-range-label" aria-live="polite">
|
||||
{new Date(`${days[0]!.date}T12:00:00Z`).toLocaleDateString("en", {
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
timeZone: "UTC",
|
||||
})}{" "}
|
||||
–{" "}
|
||||
{new Date(`${days.at(-1)!.date}T12:00:00Z`).toLocaleDateString("en", {
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
timeZone: "UTC",
|
||||
})}
|
||||
</span>
|
||||
<div
|
||||
className="ds-month-views"
|
||||
role="group"
|
||||
aria-label={`${label} time range`}
|
||||
>
|
||||
{MONTH_VIEWS.map((value) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
aria-label={`${value} months`}
|
||||
aria-pressed={months === value}
|
||||
onClick={() => setMonths(value)}
|
||||
>
|
||||
{value}m
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={scrollArea}
|
||||
className="ds-calendar-scroll"
|
||||
role="group"
|
||||
aria-label={label}
|
||||
>
|
||||
<div
|
||||
className="ds-calendar-inner"
|
||||
style={{
|
||||
minWidth:
|
||||
months === 12
|
||||
? 0
|
||||
: Math.max(compact ? 280 : 600, weeks * 14 + 38),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="ds-calendar-months"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${weeks}, minmax(0, 1fr))`,
|
||||
}}
|
||||
>
|
||||
{days.map(
|
||||
(day, index) =>
|
||||
(index === 0 || day.date.endsWith("-01")) && (
|
||||
<span
|
||||
key={day.date}
|
||||
style={{ gridColumn: Math.floor((index + offset) / 7) + 1 }}
|
||||
>
|
||||
{new Date(`${day.date}T12:00:00Z`).toLocaleDateString(
|
||||
"en",
|
||||
{ month: "short", timeZone: "UTC" },
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<div className="ds-calendar-body">
|
||||
<div className="ds-calendar-weekdays" aria-hidden="true">
|
||||
<span>Mon</span>
|
||||
<span>Wed</span>
|
||||
<span>Fri</span>
|
||||
</div>
|
||||
<div
|
||||
className="ds-calendar-grid"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${weeks}, 1fr)`,
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: offset }, (_, index) => (
|
||||
<span key={`padding-${index}`} aria-hidden="true" />
|
||||
))}
|
||||
{days.map((day, index) => (
|
||||
<button
|
||||
key={day.date}
|
||||
ref={(element) => {
|
||||
buttons.current[index] = element;
|
||||
}}
|
||||
type="button"
|
||||
className={`ds-day ds-day--${day.state}`}
|
||||
style={{
|
||||
backgroundColor:
|
||||
day.state === "not-due"
|
||||
? "transparent"
|
||||
: day.state === "future"
|
||||
? emptyColor
|
||||
: progressShade(day, color),
|
||||
}}
|
||||
aria-label={`${day.date}: ${describeDay(day, unit)}`}
|
||||
title={`${day.date}: ${describeDay(day, unit)}`}
|
||||
aria-pressed={day.date === selected.date}
|
||||
aria-describedby={inspectorId}
|
||||
tabIndex={day.date === selected.date ? 0 : -1}
|
||||
onClick={() => setSelectedDate(day.date)}
|
||||
onKeyDown={(event) => {
|
||||
const offset = {
|
||||
ArrowDown: 1,
|
||||
ArrowUp: -1,
|
||||
ArrowRight: 7,
|
||||
ArrowLeft: -7,
|
||||
}[event.key];
|
||||
const next =
|
||||
event.key === "Home"
|
||||
? 0
|
||||
: event.key === "End"
|
||||
? days.length - 1
|
||||
: offset !== undefined
|
||||
? Math.max(
|
||||
0,
|
||||
Math.min(days.length - 1, index + offset),
|
||||
)
|
||||
: undefined;
|
||||
if (next !== undefined) {
|
||||
event.preventDefault();
|
||||
setSelectedDate(days[next]!.date);
|
||||
buttons.current[next]?.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{legend ?? <CalendarLegend days={days} color={color} label={label} />}
|
||||
<div className="ds-calendar-caption">
|
||||
<span>
|
||||
{months} months{" "}
|
||||
<span className="ds-muted">
|
||||
/{" "}
|
||||
{historyLabel ??
|
||||
(compact ? "Demo history" : "Illustrative history")}
|
||||
</span>
|
||||
</span>
|
||||
<span className="ds-muted">
|
||||
{compact
|
||||
? "Select a day · Arrow keys"
|
||||
: "Select a day to inspect · Arrow keys to move"}
|
||||
</span>
|
||||
</div>
|
||||
<div id={inspectorId} className="ds-date-inspector" aria-live="polite">
|
||||
<span>
|
||||
{new Date(`${selected.date}T12:00:00Z`).toLocaleDateString("en", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
timeZone: "UTC",
|
||||
})}
|
||||
</span>
|
||||
<span>{describeDay(selected, unit)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user