Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/aria-labels-press-themes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@dateforge/react-calendar": minor
---
Add cascading `actionLabels` config to `<Calendar>` for centralized aria-label customization across all modules.

Add `press` appearance — newspaper-style serif with sharp corners, wide letter-spacing, and flat shadows.

Add `atelier` (light) and `bauhaus` (dark) themes — paired warm cream / cool ink palette with red dateline accent.

Rename appearance tokens for clarity: `--header-padding` → `--cal-nav-padding`, `--header-min-height` → `--cal-nav-min-height`, `--cal-text-2xl` → `--cal-nav-font-size`, `--cal-text-xl` → `--cal-nav-meta-font-size`. The `headerPadding` / `headerMinHeight` TS keys become `navPadding` / `navMinHeight`, with new `navFontSize` / `navMetaFontSize` added.
2 changes: 1 addition & 1 deletion .size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"./dist/modules/nav.mjs": "{ CalendarNav }",
"./dist/modules/days.mjs": "{ CalendarDays }"
},
"limit": "24 KB",
"limit": "24.5 KB",
"ignore": ["react", "react-dom"]
},
{
Expand Down
36 changes: 36 additions & 0 deletions .storybook/helpers/use-frozen-time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useLayoutEffect } from "react";

/**
* Freezes `new Date()` and `Date.now()` globally for the duration of the
* component's lifecycle. Used in stories that render live clocks (e.g.
* `<CalendarNav showNowTime />`) so Chromatic snapshots are deterministic.
*
* Layout-effect timing matters: the override must be installed before the
* library reads the clock during render. Cleanup restores the real Date.
*/
export const useFrozenTime = (frozenAt: Date): void => {
useLayoutEffect(() => {
const RealDate = globalThis.Date;
const fixed = frozenAt.getTime();

class FrozenDate extends RealDate {
// biome-ignore lint/suspicious/noExplicitAny: variadic Date constructor
constructor(...args: any[]) {
if (args.length === 0) {
super(fixed);
} else {
// biome-ignore lint/suspicious/noExplicitAny: pass-through
super(...(args as [any]));
}
}
static now(): number {
return fixed;
}
}

globalThis.Date = FrozenDate as DateConstructor;
return () => {
globalThis.Date = RealDate;
};
}, [frozenAt]);
};
59 changes: 51 additions & 8 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,54 @@ const preview: Preview = {
},
},
decorators: [
(Story, ctx) => (
<div style={{ padding: 20, width: ctx.parameters.storyWidth ?? 305 }}>
<Story
key={`${ctx.globals.theme}-${ctx.globals.appearance}-${ctx.globals.locale}-${ctx.globals.gradient}`}
/>
</div>
),
(Story, ctx) => {
const viewportActive = Boolean(
(ctx.globals.viewport as { value?: string } | undefined)?.value,
);
const style: React.CSSProperties = viewportActive
? { padding: 8, width: "100%", boxSizing: "border-box" }
: { padding: 20, width: ctx.parameters.storyWidth ?? 305 };
return (
<div style={style}>
<Story
key={`${ctx.globals.theme}-${ctx.globals.appearance}-${ctx.globals.locale}-${ctx.globals.gradient}`}
/>
</div>
);
},
],
parameters: {
viewport: {
// Tuned to the calendar's container-query breakpoints
// (13.75em / 16.25em / 21.25em ≈ 220 / 260 / 340 px at 16px root).
options: {
narrow: {
name: "Narrow (220px)",
styles: { width: "220px", height: "640px" },
type: "mobile",
},
compact: {
name: "Compact (260px)",
styles: { width: "260px", height: "640px" },
type: "mobile",
},
medium: {
name: "Medium (340px)",
styles: { width: "340px", height: "720px" },
type: "mobile",
},
comfortable: {
name: "Comfortable (480px)",
styles: { width: "480px", height: "720px" },
type: "tablet",
},
full: {
name: "Full (800px)",
styles: { width: "800px", height: "900px" },
type: "desktop",
},
},
},
controls: {
disableSaveFromUI: true,
matchers: {
Expand All @@ -83,7 +122,11 @@ const preview: Preview = {
],
},
},
layout: "centered",
// Default Storybook layout is "padded". We avoid "centered" globally
// because it wraps `<body>` in a flex container that collapses our
// viewport-driven `width: 100%` wrapper to 0px (storybook-root is itself
// a flex child with no intrinsic width).
layout: "padded",
options: {
storySort: {
method: "alphabetical",
Expand Down
127 changes: 66 additions & 61 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,30 +33,30 @@ User layer wins over `themes` and `appearances`; both win over base/component/mo

Source: `themes/themes.ts`. 15 tokens per theme.

| Token | Role |
| ------- | --------------------------------------------- |
| `--c-a` | accent — primary action |
| `--c-at`| activeText — text on active/pressed |
| Token | Role |
| --------- | ------------------------------------------- |
| `--c-a` | accent — primary action |
| `--c-at` | activeText — text on active/pressed |
| `--c-t-d` | todayDot — dot under selected today |
| `--c-b` | backdrop — dialog/overlay background |
| `--c-h` | highlight — hover/focus indicator, selected |
| `--c-t` | tone — calendar grid background subtone |
| `--c-c` | text — primary text |
| `--c-s` | stroke — border, divider, outline |
| `--c-x` | shadow — shadow tint (alpha-blended) |
| `--c-d` | disabled — disabled control background |
| `--c-m` | mutedText — secondary/hint text |
| `--c-dt`| disabledText |
| `--c-we`| weekend — weekend cell highlight |
| `--c-r` | range — range selection background |
| `--c-e` | error — invalid state |
| `--c-b` | backdrop — dialog/overlay background |
| `--c-h` | highlight — hover/focus indicator, selected |
| `--c-t` | tone — calendar grid background subtone |
| `--c-c` | text — primary text |
| `--c-s` | stroke — border, divider, outline |
| `--c-x` | shadow — shadow tint (alpha-blended) |
| `--c-d` | disabled — disabled control background |
| `--c-m` | mutedText — secondary/hint text |
| `--c-dt` | disabledText |
| `--c-we` | weekend — weekend cell highlight |
| `--c-r` | range — range selection background |
| `--c-e` | error — invalid state |

### Typography tokens

Source: `src/core/layout.module.css`.

- `--cal-font-size`: `clamp(11px, 2.7cqw, 18px)` — container-relative base
- `--cal-text-2xs … --cal-text-2xl`: semantic scale (0.6em–1.1em)
- `--cal-text-2xs … --cal-text-lg`: semantic scale (0.6em–0.95em). `xl`/`2xl` were retired in favor of `--cal-nav-meta-font-size` / `--cal-nav-font-size` since they were nav-only.
- `--cal-text-day`: `clamp(0.72em, …, 1.15em)` — adaptive day-cell sizing
- `--cal-weight-{regular,medium,semibold,bold}`: 400–700
- `--cal-leading-{tight,normal,relaxed}`: 1 → 1.6
Expand All @@ -65,42 +65,47 @@ Source: `src/core/layout.module.css`.

Source: `appearances/index.ts`.

| Token | Purpose |
| ----------------------- | ----------------------------------------------- |
| `--cal-radius` | base border-radius |
| `--cal-container-radius`| outer container radius (multiplier of radius) |
| `--cal-spacing` | base gap/padding unit |
| `--cal-border` | stroke width |
| `--cal-shadow-{sm,md,lg}`| shadow depth (uses `var(--c-x)`) |
| `--cal-transition` | animation duration |
| `--cal-days-padding` | day-cell padding |
| `--cal-track-height` | scrollable track height |
| `--cal-day-ratio` | day-cell aspect ratio |
| Token | Purpose |
| -------------------------- | ----------------------------------------------------------------- |
| `--cal-radius` | base border-radius |
| `--cal-container-radius` | outer container radius (multiplier of radius) |
| `--cal-spacing` | base gap/padding unit |
| `--cal-border` | stroke width |
| `--cal-shadow-{sm,md,lg}` | shadow depth (uses `var(--c-x)`) |
| `--cal-transition` | animation duration |
| `--cal-days-padding` | day-cell padding |
| `--cal-track-height` | scrollable track height |
| `--cal-day-ratio` | day-cell aspect ratio |
| `--cal-nav-padding` | padding of `CalendarNav` container (was `--header-padding`) |
| `--cal-nav-min-height` | minimum height of `CalendarNav` (was `--header-min-height`) |
| `--cal-nav-font-size` | nav container root font-size; cascades to all nav children via em |
| `--cal-nav-meta-font-size` | font-size of `.currentYear` children (year/month text in nav) |

---

## Themes (38)
## Themes (42)

Generated via `scripts/generate-theme.ts` → `themes/<name>.ts`. Do **not** hand-edit generated files; re-run `npm run build`.

**Light / bright:** `tide`, `graphite`, `mint`, `snow`, `solar`, `slate`, `neon`, `prism`, `meadow`, `latte`, `split`, `riso`, `monsoon`, `pearl`, `chalk`, `comfy`.
**Light / bright:** `tide`, `graphite`, `mint`, `snow`, `solar`, `slate`, `neon`, `prism`, `meadow`, `latte`, `split`, `riso`, `monsoon`, `pearl`, `chalk`, `comfy`, `mono`, `atelier`.

**Dark / vibrant:** `fjord`, `industrial`, `crimson`, `amethyst`, `cyber`, `espresso`, `ember`, `phosphor`, `midnight`, `sandstone`, `rosa`, `dracula`, `nebula`, `aurora`, `forest`, `scarlet`, `temporal`, `flare`, `abyss`.
**Dark / vibrant:** `fjord`, `industrial`, `crimson`, `amethyst`, `cyber`, `espresso`, `ember`, `phosphor`, `midnight`, `sandstone`, `rosa`, `dracula`, `nebula`, `aurora`, `forest`, `scarlet`, `temporal`, `flare`, `abyss`, `cobalt`, `velvet`, `eclipse`, `noir`, `bauhaus`.

Range covers muted pastels (`latte`, `comfy`), neon/cyber (`phosphor`, `neon`, `abyss`), earth tones (`sandstone`, `forest`).

---

## Appearances (5)
## Appearances (7)

| Appearance | Character | Key tokens |
| ---------- | ----------------------------------------------- | ------------------------------------------------------------------- |
| `compact` | Dense, minimal padding, tight | radius 0.3em, spacing 0.35em, transition 0.15s, day-ratio 1/0.7 |
| `square` | Sharp corners, tight, minimal shadows | radius 0, spacing 0.5em, transition 0.12s |
| `soft` | Subtle rounding, balanced, gentle shadows | radius 0.75em, spacing 0.7em, transition 0.25s |
| `bubble` | Rounded, spacious, prominent shadows | radius 1.5em, spacing 0.7em, transition 0.28s, header 4em |
| `loft` | Large, relaxed | radius 1em, spacing 1em, transition 0.35s, day-padding 1.8em |
| `airy` | Borderless, light weights, generous spacing | radius 0.4em, border 1px, spacing 1em, weights 300, no shadows |
| Appearance | Character | Key tokens |
| ---------- | --------------------------------------------- | ------------------------------------------------------------------------ |
| `compact` | Dense, minimal padding, tight | radius 0.3em, spacing 0.35em, transition 0.15s, day-ratio 1/0.7 |
| `square` | Sharp corners, tight, minimal shadows | radius 0, spacing 0.5em, transition 0.12s |
| `soft` | Subtle rounding, balanced, gentle shadows | radius 0.75em, spacing 0.7em, transition 0.25s |
| `bubble` | Rounded, spacious, prominent shadows | radius 1.5em, spacing 0.7em, transition 0.28s, nav-min-height 4em |
| `loft` | Large, relaxed | radius 1em, spacing 1em, transition 0.35s, day-padding 1.8em |
| `airy` | light weights, generous spacing | radius 0.4em, border 1px, spacing 1em, weights 300, no shadows |
| `press` | Newspaper serif, sharp corners, wide tracking | radius 0.05em, serif font, letter-spacing 0.18em, no shadows, border 1px |

---

Expand Down Expand Up @@ -134,19 +139,19 @@ Source: `src/core/layout.module.css`.

### Transition durations (per appearance)

`compact 0.15s`, `square 0.12s`, `soft 0.25s`, `bubble 0.28s`, `loft 0.35s`, `airy 0.2s`. Used by CSS transitions on opacity / transform / colors via `var(--cal-transition)`.
`compact 0.15s`, `square 0.12s`, `soft 0.25s`, `bubble 0.28s`, `loft 0.35s`, `airy 0.2s`, `press 0.18s`. Used by CSS transitions on opacity / transform / colors via `var(--cal-transition)`.

### Track scroll physics

Source: `src/hooks/use-track.ts`.

| Constant | Value | Role |
| -------------- | ----- | --------------------------------- |
| `FRICTION` | 0.95 | velocity decay (inertia) |
| `SPRING_K` | 0.08 | snap stiffness |
| `SPRING_DAMP` | 0.82 | underdamped bounce |
| `RUBBER_K` | 0.12 | boundary resistance |
| `RUBBER_DAMP` | 0.75 | rubber-band damping |
| Constant | Value | Role |
| ------------- | ----- | ------------------------ |
| `FRICTION` | 0.95 | velocity decay (inertia) |
| `SPRING_K` | 0.08 | snap stiffness |
| `SPRING_DAMP` | 0.82 | underdamped bounce |
| `RUBBER_K` | 0.12 | boundary resistance |
| `RUBBER_DAMP` | 0.75 | rubber-band damping |

Snap threshold ~3.5px/frame; settle tolerance 0.4px. Pointer events drive position updates.

Expand All @@ -162,14 +167,14 @@ Snap threshold ~3.5px/frame; settle tolerance 0.4px. Pointer events drive positi

ARIA grid pattern (https://www.w3.org/WAI/ARIA/apg/patterns/grid/). Source: `src/hooks/use-calendar-keyboard.ts`.

| Key | Action |
| -------------------- | ------------------------------------ |
| Arrow Left / Right | Move focus by one day |
| Arrow Up / Down | Move focus by one week |
| Home / End | First / last day of focused week |
| Page Up / Page Down | Previous / next month |
| Shift + Page Up/Down | Previous / next year |
| Enter or Space | Select focused day |
| Key | Action |
| -------------------- | -------------------------------- |
| Arrow Left / Right | Move focus by one day |
| Arrow Up / Down | Move focus by one week |
| Home / End | First / last day of focused week |
| Page Up / Page Down | Previous / next month |
| Shift + Page Up/Down | Previous / next year |
| Enter or Space | Select focused day |

Roving tabindex: focused day = `tabindex="0"`, others `-1`. Focus crosses month boundaries via `navigateTo` unless `blockNavigation` is set.

Expand All @@ -182,11 +187,11 @@ Roving tabindex: focused day = `tabindex="0"`, others `-1`. Focus crosses month

Tracks (`<CalendarDaysTrack>`, `<CalendarMonthsTrack>`, `<CalendarYearsTrack>`):

| Key | Action |
| ------------------ | ---------------------------------- |
| Arrow Left / Right | Step backwards / forwards |
| Page Up / Down | Jump 7 items (DaysTrack) |
| Home / End | First / last allowed item |
| Key | Action |
| ------------------ | ------------------------- |
| Arrow Left / Right | Step backwards / forwards |
| Page Up / Down | Jump 7 items (DaysTrack) |
| Home / End | First / last allowed item |

### Focus management

Expand Down
Loading
Loading