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 +181 -166
- package/dist/photo-calendar.css +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,47 @@
|
|
|
1
1
|
# react-photo-calendar
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/react-photo-calendar)
|
|
4
|
+
[](https://github.com/ljmerza/react-photo-calendar/actions/workflows/ci.yml)
|
|
5
|
+
[](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
|
+

|
|
21
|
+
|
|
22
|
+
The same component at 390px wide.
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
Setting `navigationMode="scroll"` swaps paged navigation for a continuous timeline with sticky month headers.
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
`renderDay` replaces the day cell entirely while the calendar keeps managing state.
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
The primitives assembled by hand, with your own controls.
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
The bundled dark theme, which the consuming app switches on with a `dark` class on `<html>`.
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+

|
|
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
|
|
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
|
|
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
|
-
##
|
|
75
|
+
## Data
|
|
30
76
|
|
|
31
|
-
|
|
32
|
-
npm install
|
|
33
|
-
npm run dev
|
|
34
|
-
```
|
|
77
|
+
One entry per moment, grouped onto days by the calendar:
|
|
35
78
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
##
|
|
128
|
+
## Headless primitives
|
|
54
129
|
|
|
55
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
174
|
+
See [docs/photo-calendar-render-props.md](docs/photo-calendar-render-props.md)
|
|
175
|
+
for the full render-prop contracts.
|
|
125
176
|
|
|
126
|
-
|
|
177
|
+
## Styling
|
|
127
178
|
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
+
```html
|
|
192
|
+
<html class="dark">
|
|
193
|
+
```
|
|
157
194
|
|
|
158
|
-
|
|
195
|
+
The calendar paints no page background of its own, in either theme, so the app
|
|
196
|
+
around it supplies that.
|
|
159
197
|
|
|
160
|
-
|
|
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
|
-
|
|
202
|
+
## Development
|
|
163
203
|
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
204
|
+
```bash
|
|
205
|
+
npm install
|
|
206
|
+
npm run storybook # http://localhost:6006
|
|
207
|
+
```
|
|
168
208
|
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
package/dist/photo-calendar.css
CHANGED
|
@@ -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:.
|
|
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)}
|