@nomideusz/svelte-calendar 0.2.2 → 0.3.0
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 +207 -6
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/memory.d.ts +7 -1
- package/dist/adapters/memory.js +35 -4
- package/dist/adapters/recurring.d.ts +39 -0
- package/dist/adapters/recurring.js +117 -0
- package/dist/calendar/Calendar.svelte +20 -5
- package/dist/calendar/Calendar.svelte.d.ts +4 -0
- package/dist/core/types.d.ts +4 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/primitives/EmptySlot.svelte +2 -2
- package/dist/primitives/EventBlock.svelte +75 -14
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/theme/presets.d.ts +10 -15
- package/dist/theme/presets.js +5 -11
- package/dist/views/agenda/Agenda.svelte +3 -3
- package/dist/views/day/DayGrid.svelte +5 -0
- package/dist/views/day/DayGrid.svelte.d.ts +5 -0
- package/dist/views/index.d.ts +1 -0
- package/dist/views/index.js +1 -0
- package/dist/views/schedule/WeekSchedule.svelte +133 -0
- package/dist/views/schedule/WeekSchedule.svelte.d.ts +38 -0
- package/dist/views/schedule/index.d.ts +1 -0
- package/dist/views/schedule/index.js +2 -0
- package/dist/views/settings/Settings.svelte +6 -2
- package/dist/views/week/WeekGrid.svelte +7 -2
- package/dist/views/week/WeekGrid.svelte.d.ts +3 -0
- package/dist/views/week/WeekHeatmap.svelte +12 -5
- package/dist/views/week/WeekHeatmap.svelte.d.ts +5 -1
- package/dist/widget/CalendarWidget.svelte +138 -0
- package/dist/widget/CalendarWidget.svelte.d.ts +23 -0
- package/dist/widget/index.d.ts +1 -0
- package/dist/widget/index.js +1 -0
- package/dist/widget/widget.d.ts +7 -0
- package/dist/widget/widget.js +9 -0
- package/package.json +5 -2
- package/widget/widget.js +260 -0
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ Switching between Day and Week preserves the active concept.
|
|
|
13
13
|
| **Timeline** | DayTimeline | — | Horizontal day timeline. Day-only. |
|
|
14
14
|
| **Agenda** | Agenda `mode="day"` | Agenda `mode="week"` | List / feed — Done, Now, Next (day) or grouped-by-day scroll (week). |
|
|
15
15
|
| **Heatmap** | — | WeekHeatmap | Density view — 24 cells per day showing busy/free intensity. Week-only. |
|
|
16
|
+
| **Schedule** | — | WeekSchedule | Zero-config weekly schedule display. Single import convenience wrapper. |
|
|
16
17
|
|
|
17
18
|
## Installation
|
|
18
19
|
|
|
@@ -39,8 +40,16 @@ pnpm add @nomideusz/svelte-calendar
|
|
|
39
40
|
import type { CalendarView, TimelineEvent } from '@nomideusz/svelte-calendar';
|
|
40
41
|
|
|
41
42
|
const events: TimelineEvent[] = [
|
|
42
|
-
{
|
|
43
|
-
|
|
43
|
+
{
|
|
44
|
+
id: '1', title: 'Yoga Flow',
|
|
45
|
+
start: new Date('2025-03-01T09:00'), end: new Date('2025-03-01T10:00'),
|
|
46
|
+
color: '#818cf8', subtitle: 'With Anna', tags: ['Beginner'],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: '2', title: 'Meditation',
|
|
50
|
+
start: new Date('2025-03-01T12:00'), end: new Date('2025-03-01T12:45'),
|
|
51
|
+
color: '#34d399',
|
|
52
|
+
},
|
|
44
53
|
];
|
|
45
54
|
|
|
46
55
|
// Adapters provide the data layer (in-memory, REST, etc.)
|
|
@@ -67,6 +76,137 @@ pnpm add @nomideusz/svelte-calendar
|
|
|
67
76
|
/>
|
|
68
77
|
```
|
|
69
78
|
|
|
79
|
+
## Recurring Weekly Schedules
|
|
80
|
+
|
|
81
|
+
Define a weekly schedule once — the adapter auto-projects it onto whatever week the calendar is viewing. No manual date math needed.
|
|
82
|
+
|
|
83
|
+
```svelte
|
|
84
|
+
<script lang="ts">
|
|
85
|
+
import { Calendar, WeekGrid, createRecurringAdapter, neutral } from '@nomideusz/svelte-calendar';
|
|
86
|
+
import type { CalendarView, RecurringEvent } from '@nomideusz/svelte-calendar';
|
|
87
|
+
|
|
88
|
+
const schedule: RecurringEvent[] = [
|
|
89
|
+
{ id: '1', title: 'Morning Yoga', dayOfWeek: 1, startTime: '07:00', endTime: '08:30', color: '#818cf8' },
|
|
90
|
+
{ id: '2', title: 'Pilates', dayOfWeek: 3, startTime: '18:00', endTime: '19:00', color: '#f472b6' },
|
|
91
|
+
{ id: '3', title: 'Sound Bath', dayOfWeek: 5, startTime: '19:00', endTime: '20:00', color: '#2dd4bf', subtitle: 'Crystal bowls', tags: ['Relaxing'] },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const adapter = createRecurringAdapter(schedule);
|
|
95
|
+
const views: CalendarView[] = [
|
|
96
|
+
{ id: 'week-grid', label: 'Grid', granularity: 'week', component: WeekGrid },
|
|
97
|
+
];
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<Calendar {views} {adapter} defaultView="week-grid" theme={neutral} readOnly />
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### RecurringEvent
|
|
104
|
+
|
|
105
|
+
| Field | Type | Description |
|
|
106
|
+
|-------|------|-------------|
|
|
107
|
+
| `id` | `string` | Unique identifier |
|
|
108
|
+
| `title` | `string` | Event title |
|
|
109
|
+
| `dayOfWeek` | `1–7` | ISO weekday (1 = Monday … 7 = Sunday) |
|
|
110
|
+
| `startTime` | `string` | Start time in `"HH:MM"` format |
|
|
111
|
+
| `endTime` | `string` | End time in `"HH:MM"` format |
|
|
112
|
+
| `color` | `string?` | Accent color |
|
|
113
|
+
| `subtitle` | `string?` | Subtitle (rendered below title) |
|
|
114
|
+
| `tags` | `string[]?` | Tag pills |
|
|
115
|
+
| `category` | `string?` | Category for grouping / colorMap |
|
|
116
|
+
| `data` | `Record?` | Arbitrary payload |
|
|
117
|
+
|
|
118
|
+
## WeekSchedule — Zero-Config Convenience
|
|
119
|
+
|
|
120
|
+
One import, one component. Pre-wires adapter, views, and toolbar internally:
|
|
121
|
+
|
|
122
|
+
```svelte
|
|
123
|
+
<script>
|
|
124
|
+
import { WeekSchedule } from '@nomideusz/svelte-calendar';
|
|
125
|
+
import { neutral } from '@nomideusz/svelte-calendar';
|
|
126
|
+
|
|
127
|
+
const schedule = [
|
|
128
|
+
{ id: '1', title: 'Yoga', dayOfWeek: 1, startTime: '07:00', endTime: '08:30', color: '#818cf8' },
|
|
129
|
+
{ id: '2', title: 'Pilates', dayOfWeek: 3, startTime: '18:00', endTime: '19:00', color: '#f472b6' },
|
|
130
|
+
];
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<WeekSchedule {schedule} theme={neutral} locale="pl-PL" height={560} readOnly />
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Works with concrete events too:
|
|
137
|
+
|
|
138
|
+
```svelte
|
|
139
|
+
<WeekSchedule events={myEvents} theme={neutral} height={560} />
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Read-Only Mode
|
|
143
|
+
|
|
144
|
+
Pass `readOnly` to disable drag, resize, and click-to-create interactions:
|
|
145
|
+
|
|
146
|
+
```svelte
|
|
147
|
+
<Calendar {views} {adapter} readOnly />
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
In read-only mode:
|
|
151
|
+
- Drag handles and resize affordances are disabled
|
|
152
|
+
- Empty-slot creation clicks are suppressed
|
|
153
|
+
- `oneventcreate` and `oneventmove` callbacks are not fired
|
|
154
|
+
- `oneventclick` still works for navigation/display purposes
|
|
155
|
+
|
|
156
|
+
## Visible Hours
|
|
157
|
+
|
|
158
|
+
Crop the grid to relevant hours — no more scrolling past empty early morning / late night rows:
|
|
159
|
+
|
|
160
|
+
```svelte
|
|
161
|
+
<!-- Only show 6 AM to 9 PM -->
|
|
162
|
+
<Calendar {views} {adapter} visibleHours={[6, 21]} />
|
|
163
|
+
|
|
164
|
+
<!-- Works on WeekSchedule too -->
|
|
165
|
+
<WeekSchedule {schedule} visibleHours={[7, 20]} />
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The `visibleHours` prop is a `[startHour, endHour)` tuple. It applies to the WeekHeatmap grid cells and is passed through to all views.
|
|
169
|
+
|
|
170
|
+
## Subtitle & Tags on Events
|
|
171
|
+
|
|
172
|
+
`TimelineEvent` now supports `subtitle` and `tags` fields — rendered automatically by `EventBlock`:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
const events: TimelineEvent[] = [
|
|
176
|
+
{
|
|
177
|
+
id: '1',
|
|
178
|
+
title: 'Power Vinyasa',
|
|
179
|
+
start: new Date('2025-03-01T10:00'),
|
|
180
|
+
end: new Date('2025-03-01T11:15'),
|
|
181
|
+
color: '#f472b6',
|
|
182
|
+
subtitle: 'With Marco', // shown below the title
|
|
183
|
+
tags: ['Advanced', 'Hot'], // rendered as small color pills
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
- **subtitle** — displayed as secondary text below the title in card and row variants
|
|
189
|
+
- **tags** — rendered as small accent-colored pills
|
|
190
|
+
|
|
191
|
+
## Color Map & Auto-Coloring
|
|
192
|
+
|
|
193
|
+
Instead of setting `color` on every event, let the adapter assign colors by category or title:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
// Explicit mapping
|
|
197
|
+
const adapter = createMemoryAdapter(events, {
|
|
198
|
+
colorMap: {
|
|
199
|
+
yoga: '#818cf8',
|
|
200
|
+
wellness: '#34d399',
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Or auto-assign from a built-in 15-color palette
|
|
205
|
+
const adapter = createMemoryAdapter(events, { autoColor: true });
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Both `createMemoryAdapter` and `createRecurringAdapter` accept `colorMap` and `autoColor` options. Events with an explicit `color` field always take priority.
|
|
209
|
+
|
|
70
210
|
## Settings Panel
|
|
71
211
|
|
|
72
212
|
The `Settings` component provides a theme picker and dynamic fields for controlling view parameters:
|
|
@@ -254,10 +394,10 @@ const viewState = createViewState({
|
|
|
254
394
|
src/lib/
|
|
255
395
|
├── core/ # Clock, time utils, locale, types
|
|
256
396
|
├── engine/ # Reactive state: event-store, view-state, selection, drag
|
|
257
|
-
├── adapters/ # Data layer: memory
|
|
397
|
+
├── adapters/ # Data layer: memory, recurring, REST adapters
|
|
258
398
|
├── primitives/ # Low-level UI atoms: NowIndicator, EventBlock, TimeGutter...
|
|
259
399
|
├── calendar/ # Calendar shell, Toolbar
|
|
260
|
-
├── views/ # View components (day/, week/, agenda/, settings/)
|
|
400
|
+
├── views/ # View components (day/, week/, agenda/, schedule/, settings/)
|
|
261
401
|
└── theme/ # Preset themes and token definitions
|
|
262
402
|
```
|
|
263
403
|
|
|
@@ -281,8 +421,9 @@ import {
|
|
|
281
421
|
|
|
282
422
|
| Adapter | Use |
|
|
283
423
|
|---------|-----|
|
|
284
|
-
| `createMemoryAdapter(events)` | In-memory — great for demos and prototyping |
|
|
285
|
-
| `
|
|
424
|
+
| `createMemoryAdapter(events, options?)` | In-memory — great for demos and prototyping. Supports `colorMap` and `autoColor`. |
|
|
425
|
+
| `createRecurringAdapter(schedule, options?)` | Weekly recurring schedules — auto-projects onto viewed weeks. Read-only. |
|
|
426
|
+
| `createRestAdapter(options)` | Fetch from a REST API with configurable endpoints. |
|
|
286
427
|
|
|
287
428
|
## Standalone Views
|
|
288
429
|
|
|
@@ -302,6 +443,65 @@ Each view works independently without the Calendar shell:
|
|
|
302
443
|
<WeekHeatmap style={parchment} events={events} height={320} />
|
|
303
444
|
```
|
|
304
445
|
|
|
446
|
+
## Embeddable Widget
|
|
447
|
+
|
|
448
|
+
Drop a single `<script>` tag into **any** HTML page — no Svelte, no build tools, no npm needed.
|
|
449
|
+
|
|
450
|
+
### From CDN
|
|
451
|
+
|
|
452
|
+
```html
|
|
453
|
+
<script src="https://cdn.jsdelivr.net/npm/@nomideusz/svelte-calendar/widget/widget.js"></script>
|
|
454
|
+
|
|
455
|
+
<day-calendar
|
|
456
|
+
api="https://myschool.com/api/events"
|
|
457
|
+
theme="neutral"
|
|
458
|
+
height="600"
|
|
459
|
+
></day-calendar>
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
That's it. Two lines.
|
|
463
|
+
|
|
464
|
+
### With inline events (no API)
|
|
465
|
+
|
|
466
|
+
```html
|
|
467
|
+
<script src="https://cdn.jsdelivr.net/npm/@nomideusz/svelte-calendar/widget/widget.js"></script>
|
|
468
|
+
|
|
469
|
+
<day-calendar
|
|
470
|
+
theme="midnight"
|
|
471
|
+
height="500"
|
|
472
|
+
events='[
|
|
473
|
+
{ "id": "1", "title": "Yoga Flow", "start": "2025-03-01T09:00", "end": "2025-03-01T10:00", "color": "#818cf8" },
|
|
474
|
+
{ "id": "2", "title": "Meditation", "start": "2025-03-01T12:00", "end": "2025-03-01T12:45", "color": "#34d399" }
|
|
475
|
+
]'
|
|
476
|
+
></day-calendar>
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Widget attributes
|
|
480
|
+
|
|
481
|
+
| Attribute | Default | Description |
|
|
482
|
+
|-----------|---------|-------------|
|
|
483
|
+
| `api` | — | REST API base URL — fetches from `{api}/events?start=...&end=...` |
|
|
484
|
+
| `events` | — | JSON string of events (alternative to `api`) |
|
|
485
|
+
| `theme` | `neutral` | Preset: `midnight`, `parchment`, `indigo`, `neutral`, `bare` |
|
|
486
|
+
| `view` | `week-grid` | Default view: `day-grid`, `week-grid`, `day-timeline`, `day-agenda`, `week-agenda`, `week-heatmap` |
|
|
487
|
+
| `height` | `600` | Height in pixels |
|
|
488
|
+
| `locale` | — | BCP 47 locale (`en-US`, `pl-PL`, `ar-SA`, etc.) |
|
|
489
|
+
| `dir` | — | Text direction: `ltr`, `rtl`, `auto` |
|
|
490
|
+
| `mondaystart` | `true` | Start week on Monday (`true`/`false`) |
|
|
491
|
+
| `headers` | — | JSON string of HTTP headers for the REST adapter |
|
|
492
|
+
|
|
493
|
+
### REST API contract
|
|
494
|
+
|
|
495
|
+
When using the `api` attribute, the widget expects your endpoint to accept:
|
|
496
|
+
|
|
497
|
+
```
|
|
498
|
+
GET {api}/events?start={ISO}&end={ISO}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
And return either:
|
|
502
|
+
- `[{ id, title, start, end, color? }, ...]`
|
|
503
|
+
- `{ events: [{ id, title, start, end, color? }, ...] }`
|
|
504
|
+
|
|
305
505
|
## Development
|
|
306
506
|
|
|
307
507
|
```bash
|
|
@@ -309,6 +509,7 @@ pnpm install
|
|
|
309
509
|
pnpm dev # SvelteKit dev server (demo app)
|
|
310
510
|
pnpm check # Type check
|
|
311
511
|
pnpm run package # Build the library into dist/
|
|
512
|
+
pnpm run build:widget # Build standalone widget.js
|
|
312
513
|
```
|
|
313
514
|
|
|
314
515
|
## License
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export type { CalendarAdapter, DateRange } from './types.js';
|
|
2
2
|
export { createMemoryAdapter } from './memory.js';
|
|
3
|
+
export type { MemoryAdapterOptions } from './memory.js';
|
|
3
4
|
export { createRestAdapter } from './rest.js';
|
|
4
5
|
export type { RestAdapterOptions } from './rest.js';
|
|
6
|
+
export { createRecurringAdapter } from './recurring.js';
|
|
7
|
+
export type { RecurringEvent, RecurringAdapterOptions } from './recurring.js';
|
package/dist/adapters/index.js
CHANGED
|
@@ -11,4 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { TimelineEvent } from '../core/types.js';
|
|
13
13
|
import type { CalendarAdapter } from './types.js';
|
|
14
|
-
export
|
|
14
|
+
export interface MemoryAdapterOptions {
|
|
15
|
+
/** Map of category/title to color */
|
|
16
|
+
colorMap?: Record<string, string>;
|
|
17
|
+
/** Auto-assign colors to events by category or title */
|
|
18
|
+
autoColor?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function createMemoryAdapter(initial?: TimelineEvent[], options?: MemoryAdapterOptions): CalendarAdapter;
|
package/dist/adapters/memory.js
CHANGED
|
@@ -2,26 +2,57 @@ let counter = 0;
|
|
|
2
2
|
function uid() {
|
|
3
3
|
return `mem-${Date.now()}-${++counter}`;
|
|
4
4
|
}
|
|
5
|
-
|
|
5
|
+
/** Default palette for auto-coloring */
|
|
6
|
+
const AUTO_COLORS = [
|
|
7
|
+
'#ef4444', '#f97316', '#eab308', '#22c55e', '#14b8a6',
|
|
8
|
+
'#3b82f6', '#6366f1', '#a855f7', '#ec4899', '#f43f5e',
|
|
9
|
+
'#06b6d4', '#84cc16', '#d946ef', '#0ea5e9', '#10b981',
|
|
10
|
+
];
|
|
11
|
+
export function createMemoryAdapter(initial = [], options = {}) {
|
|
12
|
+
const { colorMap, autoColor } = options;
|
|
6
13
|
const events = [...initial];
|
|
14
|
+
// Build auto-color assignments
|
|
15
|
+
const colorAssignments = new Map();
|
|
16
|
+
let colorIndex = 0;
|
|
17
|
+
function resolveColor(ev) {
|
|
18
|
+
if (ev.color)
|
|
19
|
+
return ev.color;
|
|
20
|
+
if (!colorMap && !autoColor)
|
|
21
|
+
return undefined;
|
|
22
|
+
const key = ev.category ?? ev.title;
|
|
23
|
+
if (colorMap?.[key])
|
|
24
|
+
return colorMap[key];
|
|
25
|
+
if (autoColor) {
|
|
26
|
+
if (!colorAssignments.has(key)) {
|
|
27
|
+
colorAssignments.set(key, AUTO_COLORS[colorIndex % AUTO_COLORS.length]);
|
|
28
|
+
colorIndex++;
|
|
29
|
+
}
|
|
30
|
+
return colorAssignments.get(key);
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
function withColor(ev) {
|
|
35
|
+
const color = resolveColor(ev);
|
|
36
|
+
return color ? { ...ev, color } : ev;
|
|
37
|
+
}
|
|
7
38
|
function overlaps(ev, range) {
|
|
8
39
|
return ev.start < range.end && ev.end > range.start;
|
|
9
40
|
}
|
|
10
41
|
return {
|
|
11
42
|
async fetchEvents(range) {
|
|
12
|
-
return events.filter((ev) => overlaps(ev, range));
|
|
43
|
+
return events.filter((ev) => overlaps(ev, range)).map(withColor);
|
|
13
44
|
},
|
|
14
45
|
async createEvent(data) {
|
|
15
46
|
const ev = { ...data, id: uid() };
|
|
16
47
|
events.push(ev);
|
|
17
|
-
return ev;
|
|
48
|
+
return withColor(ev);
|
|
18
49
|
},
|
|
19
50
|
async updateEvent(id, patch) {
|
|
20
51
|
const idx = events.findIndex((e) => e.id === id);
|
|
21
52
|
if (idx < 0)
|
|
22
53
|
throw new Error(`Event not found: ${id}`);
|
|
23
54
|
events[idx] = { ...events[idx], ...patch, id };
|
|
24
|
-
return events[idx];
|
|
55
|
+
return withColor(events[idx]);
|
|
25
56
|
},
|
|
26
57
|
async deleteEvent(id) {
|
|
27
58
|
const idx = events.findIndex((e) => e.id === id);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CalendarAdapter } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A weekly recurring event definition.
|
|
4
|
+
*/
|
|
5
|
+
export interface RecurringEvent {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
/** ISO weekday: 1 = Monday … 7 = Sunday */
|
|
9
|
+
dayOfWeek: number;
|
|
10
|
+
/** Start time in "HH:MM" 24-hour format */
|
|
11
|
+
startTime: string;
|
|
12
|
+
/** End time in "HH:MM" 24-hour format */
|
|
13
|
+
endTime: string;
|
|
14
|
+
/** Accent color */
|
|
15
|
+
color?: string;
|
|
16
|
+
/** Optional subtitle displayed below the title */
|
|
17
|
+
subtitle?: string;
|
|
18
|
+
/** Optional tags displayed as small pills */
|
|
19
|
+
tags?: string[];
|
|
20
|
+
/** Category for grouping */
|
|
21
|
+
category?: string;
|
|
22
|
+
/** Arbitrary payload */
|
|
23
|
+
data?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
export interface RecurringAdapterOptions {
|
|
26
|
+
/** Start weeks on Monday (default: true) */
|
|
27
|
+
mondayStart?: boolean;
|
|
28
|
+
/** Map of category/title to color */
|
|
29
|
+
colorMap?: Record<string, string>;
|
|
30
|
+
/** Auto-assign colors to events by category or title */
|
|
31
|
+
autoColor?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Create a CalendarAdapter that projects recurring weekly events
|
|
35
|
+
* onto concrete dates for whatever range the calendar requests.
|
|
36
|
+
*
|
|
37
|
+
* Read-only by default — create/update/delete throw unless custom handlers are provided.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createRecurringAdapter(schedule: RecurringEvent[], options?: RecurringAdapterOptions): CalendarAdapter;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { startOfWeek } from '../core/time.js';
|
|
2
|
+
import { DAY_MS } from '../core/time.js';
|
|
3
|
+
/** Parse "HH:MM" into [hours, minutes] */
|
|
4
|
+
function parseTime(time) {
|
|
5
|
+
const [h, m] = time.split(':').map(Number);
|
|
6
|
+
return [h, m ?? 0];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Convert ISO weekday (1=Mon…7=Sun) to JS Date weekday offset from Monday.
|
|
10
|
+
* Monday = 0, Tuesday = 1, … Sunday = 6
|
|
11
|
+
*/
|
|
12
|
+
function isoWeekdayToOffset(dayOfWeek) {
|
|
13
|
+
return dayOfWeek - 1; // 1→0, 2→1, …, 7→6
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Project a recurring event onto a specific week, returning a concrete TimelineEvent.
|
|
17
|
+
*/
|
|
18
|
+
function projectToWeek(rec, weekStartMs, weekIndex) {
|
|
19
|
+
const dayOffset = isoWeekdayToOffset(rec.dayOfWeek);
|
|
20
|
+
const dayMs = weekStartMs + dayOffset * DAY_MS;
|
|
21
|
+
const dayDate = new Date(dayMs);
|
|
22
|
+
const [sh, sm] = parseTime(rec.startTime);
|
|
23
|
+
const [eh, em] = parseTime(rec.endTime);
|
|
24
|
+
const start = new Date(dayDate.getFullYear(), dayDate.getMonth(), dayDate.getDate(), sh, sm);
|
|
25
|
+
const end = new Date(dayDate.getFullYear(), dayDate.getMonth(), dayDate.getDate(), eh, em);
|
|
26
|
+
return {
|
|
27
|
+
id: `${rec.id}--w${weekIndex}--d${rec.dayOfWeek}`,
|
|
28
|
+
title: rec.title,
|
|
29
|
+
start,
|
|
30
|
+
end,
|
|
31
|
+
color: rec.color,
|
|
32
|
+
category: rec.category,
|
|
33
|
+
data: {
|
|
34
|
+
...rec.data,
|
|
35
|
+
recurringId: rec.id,
|
|
36
|
+
...(rec.subtitle ? { subtitle: rec.subtitle } : {}),
|
|
37
|
+
...(rec.tags ? { tags: rec.tags } : {}),
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Find all weeks that overlap a given date range.
|
|
43
|
+
* Returns an array of { weekStartMs, weekIndex } objects.
|
|
44
|
+
*/
|
|
45
|
+
function getOverlappingWeeks(range, mondayStart) {
|
|
46
|
+
const weeks = [];
|
|
47
|
+
let cursor = startOfWeek(range.start.getTime(), mondayStart);
|
|
48
|
+
let index = 0;
|
|
49
|
+
while (cursor < range.end.getTime()) {
|
|
50
|
+
weeks.push({ weekStartMs: cursor, weekIndex: index });
|
|
51
|
+
cursor += 7 * DAY_MS;
|
|
52
|
+
index++;
|
|
53
|
+
}
|
|
54
|
+
return weeks;
|
|
55
|
+
}
|
|
56
|
+
/** Default palette for auto-coloring */
|
|
57
|
+
const AUTO_COLORS = [
|
|
58
|
+
'#ef4444', '#f97316', '#eab308', '#22c55e', '#14b8a6',
|
|
59
|
+
'#3b82f6', '#6366f1', '#a855f7', '#ec4899', '#f43f5e',
|
|
60
|
+
'#06b6d4', '#84cc16', '#d946ef', '#0ea5e9', '#10b981',
|
|
61
|
+
];
|
|
62
|
+
/**
|
|
63
|
+
* Create a CalendarAdapter that projects recurring weekly events
|
|
64
|
+
* onto concrete dates for whatever range the calendar requests.
|
|
65
|
+
*
|
|
66
|
+
* Read-only by default — create/update/delete throw unless custom handlers are provided.
|
|
67
|
+
*/
|
|
68
|
+
export function createRecurringAdapter(schedule, options = {}) {
|
|
69
|
+
const { mondayStart = true, colorMap, autoColor } = options;
|
|
70
|
+
// Build auto-color assignments
|
|
71
|
+
const colorAssignments = new Map();
|
|
72
|
+
if (autoColor || colorMap) {
|
|
73
|
+
let colorIndex = 0;
|
|
74
|
+
for (const rec of schedule) {
|
|
75
|
+
const key = rec.category ?? rec.title;
|
|
76
|
+
if (colorMap?.[key]) {
|
|
77
|
+
colorAssignments.set(key, colorMap[key]);
|
|
78
|
+
}
|
|
79
|
+
else if (autoColor && !colorAssignments.has(key)) {
|
|
80
|
+
colorAssignments.set(key, AUTO_COLORS[colorIndex % AUTO_COLORS.length]);
|
|
81
|
+
colorIndex++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function resolveColor(rec) {
|
|
86
|
+
if (rec.color)
|
|
87
|
+
return rec.color;
|
|
88
|
+
const key = rec.category ?? rec.title;
|
|
89
|
+
return colorAssignments.get(key);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
async fetchEvents(range) {
|
|
93
|
+
const weeks = getOverlappingWeeks(range, mondayStart);
|
|
94
|
+
const events = [];
|
|
95
|
+
for (const { weekStartMs, weekIndex } of weeks) {
|
|
96
|
+
for (const rec of schedule) {
|
|
97
|
+
const coloredRec = { ...rec, color: resolveColor(rec) };
|
|
98
|
+
const ev = projectToWeek(coloredRec, weekStartMs, weekIndex);
|
|
99
|
+
// Only include if the event overlaps the requested range
|
|
100
|
+
if (ev.start < range.end && ev.end > range.start) {
|
|
101
|
+
events.push(ev);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return events;
|
|
106
|
+
},
|
|
107
|
+
async createEvent() {
|
|
108
|
+
throw new Error('createRecurringAdapter is read-only. Use a memory or REST adapter for mutations.');
|
|
109
|
+
},
|
|
110
|
+
async updateEvent() {
|
|
111
|
+
throw new Error('createRecurringAdapter is read-only. Use a memory or REST adapter for mutations.');
|
|
112
|
+
},
|
|
113
|
+
async deleteEvent() {
|
|
114
|
+
throw new Error('createRecurringAdapter is read-only. Use a memory or REST adapter for mutations.');
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -63,6 +63,10 @@
|
|
|
63
63
|
dir?: 'ltr' | 'rtl' | 'auto';
|
|
64
64
|
/** BCP 47 locale tag (e.g. 'en-US', 'ar-SA') — sets lang and locale for formatting */
|
|
65
65
|
locale?: string;
|
|
66
|
+
/** Read-only mode: disables drag, resize, empty-slot creation */
|
|
67
|
+
readOnly?: boolean;
|
|
68
|
+
/** Visible hour range: [startHour, endHour). Crops the grid to these hours. */
|
|
69
|
+
visibleHours?: [number, number];
|
|
66
70
|
|
|
67
71
|
// ── Callbacks ──
|
|
68
72
|
oneventclick?: (event: TimelineEvent) => void;
|
|
@@ -81,11 +85,17 @@
|
|
|
81
85
|
links = [],
|
|
82
86
|
dir,
|
|
83
87
|
locale,
|
|
88
|
+
readOnly = false,
|
|
89
|
+
visibleHours,
|
|
84
90
|
oneventclick,
|
|
85
91
|
oneventcreate,
|
|
86
92
|
oneventmove,
|
|
87
93
|
}: Props = $props();
|
|
88
94
|
|
|
95
|
+
// In readOnly mode, suppress mutation callbacks
|
|
96
|
+
const effectiveCreate = $derived(readOnly ? undefined : oneventcreate);
|
|
97
|
+
const effectiveMove = $derived(readOnly ? undefined : oneventmove);
|
|
98
|
+
|
|
89
99
|
import { setDefaultLocale } from '../core/locale.js';
|
|
90
100
|
|
|
91
101
|
// ── Set locale when provided ──
|
|
@@ -105,6 +115,7 @@
|
|
|
105
115
|
// ── Drag commit handler ──
|
|
106
116
|
// Views call this on pointer-up to process drag results.
|
|
107
117
|
function commitDrag(): void {
|
|
118
|
+
if (readOnly) { drag.cancel(); return; }
|
|
108
119
|
const mode = drag.mode;
|
|
109
120
|
const payload = drag.commit();
|
|
110
121
|
if (!payload) return;
|
|
@@ -112,9 +123,9 @@
|
|
|
112
123
|
if ((mode === 'move' || mode === 'resize-start' || mode === 'resize-end') && payload.eventId) {
|
|
113
124
|
store.move(payload.eventId, payload.start, payload.end);
|
|
114
125
|
const ev = store.byId(payload.eventId);
|
|
115
|
-
if (ev)
|
|
126
|
+
if (ev) effectiveMove?.(ev, payload.start, payload.end);
|
|
116
127
|
} else if (mode === 'create') {
|
|
117
|
-
|
|
128
|
+
effectiveCreate?.({ start: payload.start, end: payload.end });
|
|
118
129
|
}
|
|
119
130
|
}
|
|
120
131
|
|
|
@@ -130,9 +141,11 @@
|
|
|
130
141
|
// always see the latest callback references.
|
|
131
142
|
setContext('calendar:callbacks', {
|
|
132
143
|
get oneventclick() { return oneventclick; },
|
|
133
|
-
get oneventcreate() { return
|
|
134
|
-
get oneventmove() { return
|
|
144
|
+
get oneventcreate() { return effectiveCreate; },
|
|
145
|
+
get oneventmove() { return effectiveMove; },
|
|
135
146
|
});
|
|
147
|
+
setContext('calendar:readOnly', { get current() { return readOnly; } });
|
|
148
|
+
setContext('calendar:visibleHours', { get current() { return visibleHours; } });
|
|
136
149
|
|
|
137
150
|
// ── Load events when range changes ──
|
|
138
151
|
$effect(() => {
|
|
@@ -170,7 +183,9 @@
|
|
|
170
183
|
mondayStart={viewState.mondayStart}
|
|
171
184
|
focusDate={viewState.focusDate}
|
|
172
185
|
oneventclick={oneventclick}
|
|
173
|
-
oneventcreate={
|
|
186
|
+
oneventcreate={effectiveCreate}
|
|
187
|
+
readOnly={readOnly}
|
|
188
|
+
visibleHours={visibleHours}
|
|
174
189
|
selectedEventId={selection.selectedId}
|
|
175
190
|
{...activeView.props ?? {}}
|
|
176
191
|
/>
|
|
@@ -37,6 +37,10 @@ interface Props {
|
|
|
37
37
|
dir?: 'ltr' | 'rtl' | 'auto';
|
|
38
38
|
/** BCP 47 locale tag (e.g. 'en-US', 'ar-SA') — sets lang and locale for formatting */
|
|
39
39
|
locale?: string;
|
|
40
|
+
/** Read-only mode: disables drag, resize, empty-slot creation */
|
|
41
|
+
readOnly?: boolean;
|
|
42
|
+
/** Visible hour range: [startHour, endHour). Crops the grid to these hours. */
|
|
43
|
+
visibleHours?: [number, number];
|
|
40
44
|
oneventclick?: (event: TimelineEvent) => void;
|
|
41
45
|
oneventcreate?: (range: {
|
|
42
46
|
start: Date;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -19,6 +19,10 @@ export interface TimelineEvent {
|
|
|
19
19
|
recurrence?: string;
|
|
20
20
|
/** Whether this event can be moved / resized by the user */
|
|
21
21
|
editable?: boolean;
|
|
22
|
+
/** Subtitle displayed below the title (e.g. instructor name, level) */
|
|
23
|
+
subtitle?: string;
|
|
24
|
+
/** Tags displayed as small pills (e.g. ["Beginner", "Yoga"]) */
|
|
25
|
+
tags?: string[];
|
|
22
26
|
/** Arbitrary payload from the source app (bookings, attendees, etc.) */
|
|
23
27
|
data?: Record<string, unknown>;
|
|
24
28
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
export { DayGrid, DayTimeline, WeekGrid, WeekHeatmap, Agenda, Settings, } from './views/index.js';
|
|
1
|
+
export { DayGrid, DayTimeline, WeekGrid, WeekHeatmap, Agenda, Settings, WeekSchedule, } from './views/index.js';
|
|
2
2
|
export type { SettingsField } from './views/index.js';
|
|
3
3
|
export { NowIndicator, EventBlock, TimeGutter, DayHeader, EmptySlot, } from './primitives/index.js';
|
|
4
4
|
export { Calendar, Toolbar } from './calendar/index.js';
|
|
5
5
|
export type { CalendarView } from './calendar/index.js';
|
|
6
6
|
export { createEventStore, createViewState, createSelection, createDragState, } from './engine/index.js';
|
|
7
7
|
export type { EventStore, ViewState, ViewStateOptions, CalendarViewId, BuiltInViewId, ViewGranularity, ViewDateRange, Selection, DragState, DragMode, DragPayload, } from './engine/index.js';
|
|
8
|
-
export { createMemoryAdapter, createRestAdapter } from './adapters/index.js';
|
|
9
|
-
export type { CalendarAdapter, DateRange, RestAdapterOptions } from './adapters/index.js';
|
|
8
|
+
export { createMemoryAdapter, createRestAdapter, createRecurringAdapter } from './adapters/index.js';
|
|
9
|
+
export type { CalendarAdapter, DateRange, RestAdapterOptions, RecurringEvent, RecurringAdapterOptions, MemoryAdapterOptions, } from './adapters/index.js';
|
|
10
10
|
export { createClock, DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, timeToX, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './core/index.js';
|
|
11
11
|
export type { Clock, TimelineEvent, WeekTimelineProps, DayTimelineProps, } from './core/index.js';
|
|
12
|
-
export { midnight, parchment, indigo, neutral, bare, presets
|
|
12
|
+
export { midnight, parchment, indigo, neutral, bare, presets } from './theme/index.js';
|
|
13
13
|
export type { PresetName } from './theme/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─── Views ──────────────────────────────────────────────
|
|
2
|
-
export { DayGrid, DayTimeline, WeekGrid, WeekHeatmap, Agenda, Settings, } from './views/index.js';
|
|
2
|
+
export { DayGrid, DayTimeline, WeekGrid, WeekHeatmap, Agenda, Settings, WeekSchedule, } from './views/index.js';
|
|
3
3
|
// ─── Primitives ─────────────────────────────────────────
|
|
4
4
|
export { NowIndicator, EventBlock, TimeGutter, DayHeader, EmptySlot, } from './primitives/index.js';
|
|
5
5
|
// ─── Calendar shell ─────────────────────────────────────
|
|
@@ -7,8 +7,8 @@ export { Calendar, Toolbar } from './calendar/index.js';
|
|
|
7
7
|
// ─── Engine (reactive state) ────────────────────────────
|
|
8
8
|
export { createEventStore, createViewState, createSelection, createDragState, } from './engine/index.js';
|
|
9
9
|
// ─── Adapters ───────────────────────────────────────────
|
|
10
|
-
export { createMemoryAdapter, createRestAdapter } from './adapters/index.js';
|
|
10
|
+
export { createMemoryAdapter, createRestAdapter, createRecurringAdapter } from './adapters/index.js';
|
|
11
11
|
// ─── Core: clock, time, locale, types ───────────────────
|
|
12
12
|
export { createClock, DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, timeToX, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './core/index.js';
|
|
13
13
|
// ─── Themes ─────────────────────────────────────────────
|
|
14
|
-
export { midnight, parchment, indigo, neutral, bare, presets
|
|
14
|
+
export { midnight, parchment, indigo, neutral, bare, presets } from './theme/index.js';
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
return m === 0 ? `${h12}${suffix}` : `${h12}:${String(m).padStart(2, '0')}${suffix}`;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
const dur = $derived(() => {
|
|
35
|
+
const dur = $derived.by(() => {
|
|
36
36
|
const mins = Math.round((end.getTime() - start.getTime()) / 60_000);
|
|
37
37
|
if (mins < 60) return `${mins}m free`;
|
|
38
38
|
const h = Math.floor(mins / 60);
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
class:es-h={orientation === 'horizontal'}
|
|
55
55
|
role="button"
|
|
56
56
|
tabindex="0"
|
|
57
|
-
aria-label="Create event, {fmtTime(start)} to {fmtTime(end)}, {dur
|
|
57
|
+
aria-label="Create event, {fmtTime(start)} to {fmtTime(end)}, {dur}"
|
|
58
58
|
onclick={() => onclick?.({ start, end })}
|
|
59
59
|
onkeydown={handleKeydown}
|
|
60
60
|
>
|