react-photo-calendar 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,47 @@
1
1
  # react-photo-calendar
2
2
 
3
- This folder hosts the stand-alone React + Vite workspace for the photo calendar
4
- library. The code will eventually move into its own repository; for now it lives
5
- alongside the main product so the team can iterate while shaping the public API
6
- and reference playground.
3
+ [![npm](https://img.shields.io/npm/v/react-photo-calendar.svg)](https://www.npmjs.com/package/react-photo-calendar)
4
+ [![CI](https://github.com/ljmerza/react-photo-calendar/actions/workflows/ci.yml/badge.svg)](https://github.com/ljmerza/react-photo-calendar/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/react-photo-calendar.svg)](LICENSE)
6
+
7
+ A React month-view calendar that renders photo thumbnails on each day. Headless
8
+ primitives with a styled reference UI on top, so you can use it as-is or rebuild
9
+ the entire surface without forking.
10
+
11
+ No runtime dependencies. React 18.2+ or 19 as a peer.
12
+
13
+ <details>
14
+ <summary><strong>Screenshots</strong></summary>
15
+
16
+ <br>
17
+
18
+ The default component on a wide viewport, with month chips, year navigation and per-day overflow badges.
19
+
20
+ ![Desktop calendar](docs/media/calendar-desktop.png)
21
+
22
+ The same component at 390px wide.
23
+
24
+ ![Mobile calendar](docs/media/calendar-mobile.png)
25
+
26
+ Setting `navigationMode="scroll"` swaps paged navigation for a continuous timeline with sticky month headers.
27
+
28
+ ![Mobile scroll timeline](docs/media/scroll-mobile.png)
29
+
30
+ `renderDay` replaces the day cell entirely while the calendar keeps managing state.
31
+
32
+ ![Custom day rendering](docs/media/custom-day-desktop.png)
33
+
34
+ The primitives assembled by hand, with your own controls.
35
+
36
+ ![Headless composition](docs/media/headless-desktop.png)
37
+
38
+ The bundled dark theme, which the consuming app switches on with a `dark` class on `<html>`.
39
+
40
+ ![Dark theme calendar](docs/media/calendar-dark.png)
41
+
42
+ ![Dark theme calendar on mobile](docs/media/calendar-dark-mobile.png)
43
+
44
+ </details>
7
45
 
8
46
  ## Installation
9
47
 
@@ -11,210 +49,187 @@ and reference playground.
11
49
  npm install react-photo-calendar
12
50
  ```
13
51
 
14
- `react` and `react-dom` are peer dependencies (`^18.2.0 || ^19.0.0`) — the package
15
- does not bundle its own copy.
16
-
17
52
  ```tsx
18
53
  import { PhotoCalendar } from 'react-photo-calendar';
19
54
  import 'react-photo-calendar/styles.css'; // optional reference styling
20
55
 
56
+ const entries = [
57
+ { datetime: '2026-09-07T10:00:00Z', photos: ['/a.jpg', '/b.jpg'] },
58
+ { datetime: '2026-09-11T18:30:00Z', photos: ['/c.jpg'] },
59
+ ];
60
+
21
61
  export function App() {
22
- return <PhotoCalendar monthKey="2030-01" />;
62
+ return (
63
+ <PhotoCalendar
64
+ entries={entries}
65
+ defaultMonthKey="2026-09"
66
+ onDaySelect={({ isoDate }) => console.log(isoDate)}
67
+ />
68
+ );
23
69
  }
24
70
  ```
25
71
 
26
- The stylesheet is optional: the primitives are headless, and `styles.css` only
72
+ The stylesheet is optional. The primitives ship unstyled; `styles.css` only
27
73
  carries the reference UI used by the `PhotoCalendar` convenience component.
28
74
 
29
- ## Getting started
75
+ ## Data
30
76
 
31
- ```bash
32
- npm install
33
- npm run dev
34
- ```
77
+ One entry per moment, grouped onto days by the calendar:
35
78
 
36
- The Storybook dev server replaces the old Vite playground and hosts interactive
37
- examples for the headless primitives (`npm run storybook` is available as an explicit alias).
38
- See `docs/storybook-guide.md` for more details. The `build` script bundles the
39
- library in ESM and CJS formats and emits TypeScript declarations that match the
40
- contract outlined in ADR-009.
79
+ ```ts
80
+ interface PhotoEntry {
81
+ datetime: string; // ISO datetime
82
+ photos: string[]; // first URL becomes the day's preview
83
+ }
84
+ ```
41
85
 
42
- ## Project layout
86
+ Days show up to `maxThumbnailsPerDay` thumbnails and a `+N` badge for the rest.
87
+
88
+ ## Props
89
+
90
+ `PhotoCalendar` also accepts every `div` attribute.
91
+
92
+ | Prop | Type | Default | Description |
93
+ |---|---|---|---|
94
+ | `entries` | `PhotoEntry[]` | `[]` | Photos to place on days |
95
+ | `monthKey` | `string` | — | Controlled month, `YYYY-MM` |
96
+ | `defaultMonthKey` | `string` | current month | Uncontrolled starting month |
97
+ | `onMonthChange` | `(monthKey: string) => void` | — | Fired when the month changes |
98
+ | `onDaySelect` | `({ isoDate, date }) => void` | — | Fired when a day is activated |
99
+ | `onRangeChange` | `(range: VisibleRange) => void` | — | Visible bounds; use it to fetch |
100
+ | `onVisibleMonthChange` | `(monthKey: string) => void` | — | Month scrolled into view |
101
+ | `firstDayOfWeek` | `0`–`6` | `0` | 0 = Sunday |
102
+ | `maxThumbnailsPerDay` | `number` | — | Thumbnails before the `+N` badge |
103
+ | `minMonthKey` / `maxMonthKey` | `string` | — | Clamp navigation |
104
+ | `locale` | `string` | system | Weekday and month label locale |
105
+ | `timeZone` | `string` | system | Day-boundary time zone |
106
+ | `navigationMode` | `'controls' \| 'scroll'` | `'controls'` | Paged or continuous timeline |
107
+ | `scrollMaxRenderedMonths` | `number` | — | Months kept mounted in scroll mode |
108
+ | `renderDay` | `(props: DayRenderProps) => ReactNode` | — | Replace the day cell |
109
+ | `renderDayContent` | `(ctx: DayRenderContext) => ReactNode` | — | Replace cell contents only |
110
+ | `renderNavigation` | `(props: NavigationRenderProps) => ReactNode` | — | Replace navigation |
111
+ | `renderWeekdays` | `(props: WeekdayRenderProps) => ReactNode` | — | Replace weekday headers |
112
+
113
+ Controlled and uncontrolled both work: pass `monthKey` with `onMonthChange` to
114
+ drive it yourself, or `defaultMonthKey` to let it manage its own state.
115
+
116
+ ## Fetching by visible range
117
+
118
+ `onRangeChange` reports the first and last dates on screen, including the
119
+ leading and trailing days from adjacent months:
43
120
 
44
- - `src/index.ts` – public exports for the library package.
45
- - `src/PhotoCalendar.tsx` – convenience component that composes the headless primitives with the reference UI (including the optional scroll timeline).
46
- - `src/components/PhotoCalendarScrollView.tsx` – mobile-first scroll navigation shell consumed when `navigationMode="scroll"`.
47
- - `src/primitives/` `PhotoCalendarRoot`, `PhotoCalendarNavigation`, `PhotoCalendarWeekdays`, `PhotoCalendarMonthGrid`, and `PhotoCalendarDay` headless building blocks.
48
- - `src/hooks/usePhotoCalendarState.ts` – shared state hook consumed by both the convenience component and primitives.
49
- - `.storybook/` – Storybook configuration powered by the React + Vite framework preset.
50
- - `src/stories/` – Storybook stories demonstrating the default calendar and headless compositions.
51
- - `vite.config.ts` – configures library builds and test environment defaults.
121
+ ```tsx
122
+ <PhotoCalendar
123
+ entries={entries}
124
+ onRangeChange={({ startIso, endIso }) => fetchPhotos(startIso, endIso)}
125
+ />
126
+ ```
52
127
 
53
- ## Composing your own UI
128
+ ## Headless primitives
54
129
 
55
- The new primitives let you mix and match calendar state with custom controls:
130
+ Compose your own UI against the same state. `PhotoCalendarRoot` provides
131
+ context; every other primitive consumes it.
56
132
 
57
133
  ```tsx
58
134
  import {
59
135
  PhotoCalendarRoot,
60
136
  PhotoCalendarNavigation,
61
- PhotoCalendarNavigationLayout,
62
- PhotoCalendarNavigationControls,
63
- PhotoCalendarNavigationPrevMonthButton,
64
- PhotoCalendarNavigationMonthChips,
65
- PhotoCalendarNavigationNextMonthButton,
66
- PhotoCalendarNavigationTodayButton,
67
137
  PhotoCalendarWeekdays,
68
138
  PhotoCalendarMonthGrid,
69
- PhotoCalendarDay,
70
- usePhotoCalendarContext,
71
139
  } from 'react-photo-calendar';
72
140
 
73
- function MyNavigation() {
74
- const { monthLabel } = usePhotoCalendarContext('MyNavigation');
141
+ <PhotoCalendarRoot entries={entries} defaultMonthKey="2026-09">
142
+ {(state) => (
143
+ <section role="grid" aria-label={`Photo calendar for ${state.monthLabel}`}>
144
+ <PhotoCalendarNavigation>
145
+ {(nav) => (
146
+ <header>
147
+ <button onClick={() => nav.navigateMonth(-1)} disabled={!nav.canNavigatePrevMonth}>
148
+ Back
149
+ </button>
150
+ <span>{nav.monthLabel}</span>
151
+ <button onClick={() => nav.navigateMonth(1)} disabled={!nav.canNavigateNextMonth}>
152
+ Forward
153
+ </button>
154
+ <button onClick={nav.goToToday}>Today</button>
155
+ </header>
156
+ )}
157
+ </PhotoCalendarNavigation>
158
+
159
+ <PhotoCalendarWeekdays />
160
+ <PhotoCalendarMonthGrid />
161
+ </section>
162
+ )}
163
+ </PhotoCalendarRoot>
164
+ ```
75
165
 
76
- return (
77
- <PhotoCalendarNavigationLayout>
78
- <PhotoCalendarNavigationControls>
79
- <PhotoCalendarNavigationPrevMonthButton />
80
- <PhotoCalendarNavigationNextMonthButton />
81
- <PhotoCalendarNavigationTodayButton />
82
- <span style={{ fontWeight: 600 }}>{monthLabel}</span>
83
- </PhotoCalendarNavigationControls>
84
- <PhotoCalendarNavigationMonthChips />
85
- </PhotoCalendarNavigationLayout>
86
- );
87
- }
166
+ `PhotoCalendarMonthGrid` takes a `renderDay` prop to override cell contents, and
167
+ a `dayStates` prop for multi-month layouts. Navigation can also be assembled from
168
+ smaller pieces such as `PhotoCalendarNavigationMonthChips` and
169
+ `PhotoCalendarNavigationTodayButton` instead of a render prop.
88
170
 
89
- export function MyCalendar() {
90
- return (
91
- <PhotoCalendarRoot defaultMonthKey="2030-01">
92
- {(state) => (
93
- <div role="grid" aria-label={`Photo calendar for ${state.monthLabel}`}>
94
- <MyNavigation />
95
- <PhotoCalendarWeekdays />
96
- <PhotoCalendarMonthGrid
97
- renderDay={(props) => (
98
- <PhotoCalendarDay
99
- day={{
100
- ...props,
101
- defaultContent: (
102
- <div
103
- style={{
104
- borderRadius: '12px',
105
- overflow: 'hidden',
106
- outline: props.isToday ? '2px solid #f97316' : 'none',
107
- outlineOffset: props.isToday ? '2px' : undefined
108
- }}
109
- >
110
- {props.defaultContent}
111
- </div>
112
- )
113
- }}
114
- />
115
- )}
116
- />
117
- </div>
118
- )}
119
- </PhotoCalendarRoot>
120
- );
121
- }
122
- ```
171
+ `usePhotoCalendarState` exposes the same state directly if you want no markup at
172
+ all, and `usePhotoCalendarContext` reads it from inside a `PhotoCalendarRoot`.
123
173
 
124
- Each primitive exposes render props so you can override just the pieces you need—see `docs/photo-calendar-render-props.md` for the full contract. If you prefer to stay on the convenience component, pass `renderNavigation`, `renderWeekdays`, or `renderDay` props to inject custom controls, or keep using the legacy `renderDayContent` helper. `PhotoCalendar` continues to provide the original all-in-one experience if you don’t need custom controls.
174
+ See [docs/photo-calendar-render-props.md](docs/photo-calendar-render-props.md)
175
+ for the full render-prop contracts.
125
176
 
126
- Navigation can also be assembled from the exported buttons and layout helpers (`PhotoCalendarNavigationLayout`, `PhotoCalendarNavigationPrevMonthButton`, etc.), letting you mix stock behaviour with bespoke markup without threading handlers manually.
177
+ ## Styling
127
178
 
128
- ```tsx
129
- import { PhotoCalendar, PhotoCalendarDay } from 'react-photo-calendar';
179
+ `styles.css` is plain CSS driven by custom properties, so most theming is a
180
+ matter of overriding tokens rather than rewriting rules:
130
181
 
131
- <PhotoCalendar
132
- renderDay={(props) => (
133
- <PhotoCalendarDay
134
- day={{
135
- ...props,
136
- defaultContent: (
137
- <div
138
- style={{
139
- borderRadius: '12px',
140
- overflow: 'hidden',
141
- outline: props.isToday ? '2px solid #f97316' : 'none',
142
- outlineOffset: props.isToday ? '2px' : undefined
143
- }}
144
- >
145
- {props.defaultContent}
146
- </div>
147
- )
148
- }}
149
- />
150
- )}
151
- />
182
+ ```css
183
+ :root {
184
+ --calendar-color-accent: #2563eb;
185
+ }
152
186
  ```
153
187
 
154
- Use `monthKey` + `onMonthChange` to control the visible month externally, or prefer `defaultMonthKey` for uncontrolled usage while still receiving navigation callbacks. `onDaySelect` emits both ISO and native `Date` values so consumers can open detail views, modals, or drawers.
188
+ A `.dark` block ships with the stylesheet and re-points those tokens. It keys off
189
+ a `dark` class on an ancestor, so the consuming app decides when to switch:
155
190
 
156
- `renderDayContent` receives the same `DayRenderContext` as before plus a `defaultContent` field—return it when you want to append to the stock thumbnails/day number layout instead of replacing it outright. Theme variables are documented in `docs/photo-calendar-design-tokens.md` so you can override colours/radii without touching JSX.
191
+ ```html
192
+ <html class="dark">
193
+ ```
157
194
 
158
- Once the component architecture stabilizes, this folder can be promoted into a stand-alone repository without significant changes—package metadata already assumes an eventual npm distribution.
195
+ The calendar paints no page background of its own, in either theme, so the app
196
+ around it supplies that.
159
197
 
160
- ## Mobile scroll timeline
198
+ See [docs/photo-calendar-design-tokens.md](docs/photo-calendar-design-tokens.md)
199
+ for the token list. Skip the stylesheet entirely and the primitives render
200
+ unstyled.
161
201
 
162
- Set `navigationMode="scroll"` on `PhotoCalendar` (or mount `PhotoCalendarScrollView` yourself) to swap the legacy button banner for the vertically scrolling timeline. The scroll shell keeps a small window of months mounted, sticks each month header to the top edge, and emits `onVisibleMonthChange` whenever the leading month shifts—ideal for lazy-loading more photo data as users skim the timeline.
202
+ ## Development
163
203
 
164
- ```tsx
165
- const loadMonth = (monthKey: string) => {
166
- // trigger fetch logic here
167
- };
204
+ ```bash
205
+ npm install
206
+ npm run storybook # http://localhost:6006
207
+ ```
168
208
 
169
- export function MobileTimeline() {
170
- const pending = useRef(new Set<string>());
209
+ | Script | Purpose |
210
+ |---|---|
211
+ | `npm test` | Vitest suite |
212
+ | `npm run test:coverage` | Tests with coverage |
213
+ | `npm run lint` | ESLint |
214
+ | `npm run typecheck` | `tsc --noEmit` |
215
+ | `npm run build` | ESM + CJS bundles and declarations |
171
216
 
172
- const prefetchCluster = useCallback((key: string) => {
173
- if (pending.current.has(key)) return;
174
- pending.current.add(key);
175
- loadMonth(key).finally(() => pending.current.delete(key));
176
- }, []);
217
+ Storybook is the playground; stories live in `src/stories/`. See
218
+ [docs/storybook-guide.md](docs/storybook-guide.md).
177
219
 
178
- return (
179
- <PhotoCalendar
180
- navigationMode="scroll"
181
- onVisibleMonthChange={(key) => {
182
- prefetchCluster(key);
183
- // grab adjacent months via scroll state helpers if needed
184
- }}
185
- scrollMaxRenderedMonths={7}
186
- />
187
- );
188
- }
220
+ ## Releasing
189
221
 
190
- // Access scroll helpers via the context when you need neighbouring keys
191
- export function PrefetchingTimeline() {
192
- // assumes PhotoCalendarScrollView + PhotoCalendarScrollState are imported
193
- const scrollRef = useRef<PhotoCalendarScrollState | null>(null);
222
+ CI runs lint, typecheck, tests and a build on every PR. To release: bump
223
+ `version` in `package.json`, merge, then tag.
194
224
 
195
- return (
196
- <PhotoCalendarRoot
197
- onVisibleMonthChange={(key) => {
198
- const scroll = scrollRef.current;
199
- if (!scroll) {
200
- return;
201
- }
202
- const neighbours = [
203
- scroll.getAdjacentMonthKey(key, -1),
204
- key,
205
- scroll.getAdjacentMonthKey(key, 1)
206
- ].filter(Boolean) as string[];
207
-
208
- neighbours.forEach(loadMonth);
209
- }}
210
- >
211
- {(state) => {
212
- scrollRef.current = state.scroll;
213
- return <PhotoCalendarScrollView />;
214
- }}
215
- </PhotoCalendarRoot>
216
- );
217
- }
225
+ ```bash
226
+ git tag v1.2.3 && git push origin v1.2.3
218
227
  ```
219
228
 
220
- Advanced consumers can access the scroll helpers (`getMonthSnapshot`, `getAdjacentMonthKey`, `syncVisibleMonth`) exposed on `state.scroll` by rendering through `PhotoCalendarRoot`. The helpers make it easy to prefetch neighbouring months, jump to specific anchors, or compute analytics without coupling to component internals.
229
+ The tag publishes to npm via [trusted publishing](https://docs.npmjs.com/trusted-publishers/)
230
+ (OIDC, no stored token) and creates a GitHub release. The build fails if the tag
231
+ and `package.json` version disagree.
232
+
233
+ ## License
234
+
235
+ MIT
@@ -1 +1 @@
1
- :root{--calendar-color-surface: #ffffff;--calendar-color-surface-muted: #f4f4f5;--calendar-color-text: #111827;--calendar-color-text-muted: rgba(17, 24, 39, .6);--calendar-color-accent: #007bff;--calendar-color-accent-soft: rgba(0, 123, 255, .15);--calendar-color-chip-bg: rgba(0, 0, 0, .08);--calendar-color-chip-active-bg: rgba(0, 123, 255, .12);--calendar-color-chip-active-text: #0051a8;--calendar-color-day-label-bg: rgba(0, 0, 0, .4);--calendar-color-overflow-bg: rgba(0, 0, 0, .7);--calendar-radius-day: 8px;--calendar-radius-chip: 16px;--calendar-radius-button: 6px;--calendar-shadow-today: 0 0 0 2px var(--calendar-color-accent-soft);--calendar-spacing-gap: .1rem;--calendar-font-size-weekday: .75rem;--calendar-font-size-day: .85rem}.calendar-banner{display:flex;flex-direction:column;align-items:center;gap:1rem;padding:1.5rem 1rem}.calendar-scroll-view{display:flex;flex-direction:column;height:100%;position:relative}.calendar-scroll-container{display:flex;flex-direction:column;gap:1.5rem;overflow-y:auto;padding:0 1rem 2rem;scroll-behavior:smooth;-webkit-overflow-scrolling:touch;background:var(--calendar-color-surface)}.calendar-scroll-sentinel{width:100%;height:1px}.calendar-month-section{display:flex;flex-direction:column;gap:.75rem}.calendar-month-header{position:sticky;top:0;z-index:10;padding:.75rem 1rem;margin:0 -1rem;background:var(--calendar-color-day-label-bg);color:#fff;font-size:1.1rem;box-shadow:0 2px 6px #0003}.calendar-month-header--active{background:var(--calendar-color-accent);color:#fff}.calendar-year-row{display:none;align-items:center;gap:1rem}.calendar-year{font-size:1.5rem;margin:0;color:var(--calendar-color-text)}.today-button-icon:disabled{opacity:.5;cursor:not-allowed}.today-button-icon{display:flex;align-items:center;justify-content:center;font-size:1.5rem;padding:.5rem;border-radius:var(--calendar-radius-button);transition:background .2s;background:var(--calendar-color-surface-muted)}.month-chips-row{display:flex;align-items:center;justify-content:space-between;gap:1rem;width:100%}.month-chips{display:flex;flex-wrap:wrap;gap:.5rem;justify-content:center;flex:1}.month-label-mobile{display:flex;justify-content:center;align-items:center;flex:1;font-size:1.25rem;text-align:center;color:var(--calendar-color-text)}.month-chip{padding:.4rem .75rem;font-size:.875rem;border-radius:var(--calendar-radius-chip);font-weight:500;transition:all .2s;white-space:nowrap;background:var(--calendar-color-chip-bg);color:var(--calendar-color-text)}.month-chip:focus-visible{outline:2px solid var(--calendar-color-accent);outline-offset:2px}.month-chip[disabled]{opacity:.4;cursor:not-allowed}.nav-button{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;flex-shrink:0;font-size:.8rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;border-radius:var(--calendar-radius-button);background:var(--calendar-color-surface-muted)}.nav-button-icon{font-size:1.5rem;line-height:1}.nav-button-label{line-height:1.1}.nav-button:focus-visible{outline:2px solid currentColor;outline-offset:2px;color:var(--calendar-color-accent)}.nav-button:disabled{opacity:.4;cursor:not-allowed}.nav-button--month{display:flex}.nav-button--year,.month-chips{display:none}@media(min-width:875px){.calendar-year-row{display:flex}.month-label-mobile{display:none}.month-chips-row{justify-content:center}.nav-button--month{display:none}.nav-button--year,.month-chips{display:flex}}.calendar-weekdays{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:var(--calendar-spacing-gap);margin-bottom:.4rem}.calendar-weekday{text-transform:uppercase;font-size:var(--calendar-font-size-weekday);letter-spacing:.1em;text-align:center;padding:.25rem 0;color:var(--calendar-color-text-muted)}.calendar-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:var(--calendar-spacing-gap)}.calendar-cell{aspect-ratio:1 / 1;border-radius:var(--calendar-radius-day);display:flex;justify-content:center;align-items:center;font-size:var(--calendar-font-size-day);text-transform:uppercase;letter-spacing:.04em;position:relative;overflow:hidden;padding:0;border:1px solid transparent}.cell-label{pointer-events:none;position:absolute;top:.5rem;right:.5rem;z-index:2;padding:.2rem .45rem;border-radius:4px;font-weight:600;font-size:1rem;background:var(--calendar-color-day-label-bg);color:#fff}@media(max-width:600px){.cell-label{top:.25rem;right:.25rem;padding:.1rem .225rem;border-radius:2px;font-size:.5rem}}.calendar-cell-thumbnails{position:absolute;top:0;right:0;bottom:0;left:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(50%,1fr));grid-auto-rows:1fr;z-index:1;gap:1px}.calendar-cell-thumbnails[data-count="1"]{grid-template-columns:1fr}.calendar-cell-image{width:100%;height:100%;object-fit:cover;object-position:center;border-radius:var(--calendar-radius-day)}.calendar-cell-overflow{position:absolute;bottom:.5rem;right:.5rem;z-index:2;padding:.25rem .45rem;border-radius:999px;font-weight:600;color:#fff;background:var(--calendar-color-overflow-bg)}.calendar-cell--placeholder{opacity:.45}.calendar-cell--today{border-color:var(--calendar-color-accent);box-shadow:var(--calendar-shadow-today)}
1
+ :root{--calendar-color-surface: #ffffff;--calendar-color-surface-muted: #f4f4f5;--calendar-color-text: #111827;--calendar-color-text-muted: rgba(17, 24, 39, .6);--calendar-color-accent: #007bff;--calendar-color-accent-soft: rgba(0, 123, 255, .15);--calendar-color-chip-bg: rgba(0, 0, 0, .08);--calendar-color-chip-active-bg: rgba(0, 123, 255, .12);--calendar-color-chip-active-text: #0051a8;--calendar-color-cell-bg: var(--calendar-color-surface-muted);--calendar-color-day-label-bg: rgba(0, 0, 0, .4);--calendar-color-overflow-bg: rgba(0, 0, 0, .7);--calendar-radius-day: 8px;--calendar-radius-chip: 16px;--calendar-radius-button: 6px;--calendar-shadow-today: 0 0 0 2px var(--calendar-color-accent-soft);--calendar-spacing-gap: .1rem;--calendar-font-size-weekday: .75rem;--calendar-font-size-day: .85rem}.dark{--calendar-color-surface: #18181b;--calendar-color-surface-muted: #27272a;--calendar-color-text: #f4f4f5;--calendar-color-text-muted: rgba(244, 244, 245, .6);--calendar-color-accent: #3b9dff;--calendar-color-accent-soft: rgba(59, 157, 255, .25);--calendar-color-chip-bg: rgba(255, 255, 255, .1);--calendar-color-chip-active-bg: rgba(59, 157, 255, .2);--calendar-color-chip-active-text: #9ccbff}.calendar-banner{display:flex;flex-direction:column;align-items:center;gap:1rem;padding:1.5rem 1rem}.calendar-scroll-view{display:flex;flex-direction:column;height:100%;position:relative}.calendar-scroll-container{display:flex;flex-direction:column;gap:1.5rem;overflow-y:auto;padding:0 1rem 2rem;scroll-behavior:smooth;-webkit-overflow-scrolling:touch;background:var(--calendar-color-surface)}.calendar-scroll-sentinel{width:100%;height:1px}.calendar-month-section{display:flex;flex-direction:column;gap:.75rem}.calendar-month-header{position:sticky;top:0;z-index:10;padding:.75rem 1rem;margin:0 -1rem;background:var(--calendar-color-day-label-bg);color:#fff;font-size:1.1rem;box-shadow:0 2px 6px #0003}.calendar-month-header--active{background:var(--calendar-color-accent);color:#fff}.calendar-year-row{display:none;align-items:center;gap:1rem}.calendar-year{font-size:1.5rem;margin:0;color:var(--calendar-color-text)}.today-button-icon:disabled{opacity:.5;cursor:not-allowed}.today-button-icon{display:flex;align-items:center;justify-content:center;font-size:1.5rem;padding:.5rem;border-radius:var(--calendar-radius-button);transition:background .2s;background:var(--calendar-color-surface-muted)}.month-chips-row{display:flex;align-items:center;justify-content:space-between;gap:1rem;width:100%}.month-chips{display:flex;flex-wrap:wrap;gap:.5rem;justify-content:center;flex:1}.month-label-mobile{display:flex;justify-content:center;align-items:center;flex:1;font-size:1.25rem;text-align:center;color:var(--calendar-color-text)}.month-chip{padding:.4rem .75rem;font-size:.875rem;border-radius:var(--calendar-radius-chip);font-weight:500;transition:all .2s;white-space:nowrap;background:var(--calendar-color-chip-bg);color:var(--calendar-color-text)}.month-chip:focus-visible{outline:2px solid var(--calendar-color-accent);outline-offset:2px}.month-chip[disabled]{opacity:.4;cursor:not-allowed}.nav-button{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;flex-shrink:0;font-size:.8rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;border-radius:var(--calendar-radius-button);background:var(--calendar-color-surface-muted)}.nav-button-icon{font-size:1.5rem;line-height:1}.nav-button-label{line-height:1.1}.nav-button:focus-visible{outline:2px solid currentColor;outline-offset:2px;color:var(--calendar-color-accent)}.nav-button:disabled{opacity:.4;cursor:not-allowed}.nav-button--month{display:flex}.nav-button--year,.month-chips{display:none}@media(max-width:874.98px){.calendar-banner{padding:.5rem;gap:.5rem}.nav-button--month{padding:.25rem .5rem}.nav-button--month .nav-button-label,.today-button-icon{display:none}}@media(min-width:875px){.calendar-year-row{display:flex}.month-label-mobile{display:none}.month-chips-row{justify-content:center}.nav-button--month{display:none}.nav-button--year,.month-chips{display:flex}}.calendar-weekdays{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:var(--calendar-spacing-gap);margin-bottom:.4rem}.calendar-weekday{text-transform:uppercase;font-size:var(--calendar-font-size-weekday);letter-spacing:.1em;text-align:center;padding:.25rem 0;color:var(--calendar-color-text-muted)}.calendar-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:var(--calendar-spacing-gap)}.calendar-cell{aspect-ratio:1 / 1;background:var(--calendar-color-cell-bg);border-radius:var(--calendar-radius-day);display:flex;justify-content:center;align-items:center;font-size:var(--calendar-font-size-day);text-transform:uppercase;letter-spacing:.04em;position:relative;overflow:hidden;padding:0;border:1px solid transparent}.cell-label{pointer-events:none;position:absolute;top:.5rem;right:.5rem;z-index:2;padding:.2rem .45rem;border-radius:4px;font-weight:600;font-size:1rem;background:var(--calendar-color-day-label-bg);color:#fff}@media(max-width:600px){.cell-label{top:.25rem;right:.25rem;padding:.1rem .225rem;border-radius:2px;font-size:.5rem}}.calendar-cell-thumbnails{position:absolute;top:0;right:0;bottom:0;left:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(50%,1fr));grid-auto-rows:1fr;z-index:1;gap:1px}.calendar-cell-thumbnails[data-count="1"]{grid-template-columns:1fr}.calendar-cell-image{width:100%;height:100%;object-fit:cover;object-position:center;border-radius:var(--calendar-radius-day)}.calendar-cell-overflow{position:absolute;bottom:.5rem;right:.5rem;z-index:2;padding:.2rem .45rem;border-radius:4px;font-weight:600;color:#fff;background:var(--calendar-color-day-label-bg)}@media(max-width:600px){.calendar-cell-overflow{bottom:.25rem;right:.25rem;padding:.1rem .225rem;border-radius:2px;font-size:.5rem}}.calendar-cell--placeholder{opacity:.45}.calendar-cell--today{border-color:var(--calendar-color-accent);box-shadow:var(--calendar-shadow-today)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-photo-calendar",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Headless, composable React month-view calendar that renders photo thumbnails per day.",
5
5
  "keywords": [
6
6
  "react",