@nomideusz/svelte-calendar 0.2.3 → 0.3.1
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 +234 -6
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/memory.d.ts +12 -1
- package/dist/adapters/memory.js +38 -4
- package/dist/adapters/recurring.d.ts +44 -0
- package/dist/adapters/recurring.js +120 -0
- package/dist/calendar/Calendar.svelte +20 -5
- package/dist/calendar/Calendar.svelte.d.ts +4 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +2 -0
- package/dist/core/palette.d.ts +23 -0
- package/dist/core/palette.js +109 -0
- package/dist/core/types.d.ts +4 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +4 -4
- package/dist/primitives/EventBlock.svelte +42 -0
- 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 +110 -0
- package/dist/views/day/DayGrid.svelte +38 -0
- package/dist/views/day/DayGrid.svelte.d.ts +5 -0
- package/dist/views/day/DayTimeline.svelte +27 -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 +1 -1
- package/dist/views/week/WeekGrid.svelte +42 -5
- 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,164 @@ 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` supports `subtitle` and `tags` fields — rendered automatically in **all views**:
|
|
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** — secondary text below the title (all views: WeekGrid, DayGrid, DayTimeline, Agenda, EventBlock)
|
|
189
|
+
- **tags** — accent-colored pills after the title (all views)
|
|
190
|
+
- In space-constrained views (DayGrid, DayTimeline), subtitle/tags appear only when the event block is tall/wide enough
|
|
191
|
+
|
|
192
|
+
## Color Map & Auto-Coloring
|
|
193
|
+
|
|
194
|
+
Instead of setting `color` on every event, let the adapter assign colors by category or title:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
// Explicit mapping
|
|
198
|
+
const adapter = createMemoryAdapter(events, {
|
|
199
|
+
colorMap: {
|
|
200
|
+
yoga: '#818cf8',
|
|
201
|
+
wellness: '#34d399',
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Auto-assign from a built-in 15-color vivid palette
|
|
206
|
+
const adapter = createMemoryAdapter(events, { autoColor: true });
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Theme-Aware Auto-Coloring
|
|
210
|
+
|
|
211
|
+
Pass the theme's accent hex to `autoColor` and the palette is generated to harmonize with your theme — colors rotate via golden-angle hue spacing from the accent, with lightness adjusted for dark/light backgrounds:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
// Harmonious palette seeded from indigo accent
|
|
215
|
+
const adapter = createMemoryAdapter(events, { autoColor: '#6366f1' });
|
|
216
|
+
|
|
217
|
+
// Works with the recurring adapter too
|
|
218
|
+
const adapter = createRecurringAdapter(schedule, { autoColor: '#ef4444' });
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
| `autoColor` value | Behaviour |
|
|
222
|
+
|---|---|
|
|
223
|
+
| `true` | Original 15-color vivid palette (fixed, theme-independent) |
|
|
224
|
+
| `'#ef4444'` | Golden-angle hue rotation from that accent; lightness adapted to dark/light |
|
|
225
|
+
|
|
226
|
+
You can also use the palette generator directly:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import { generatePalette } from '@nomideusz/svelte-calendar';
|
|
230
|
+
|
|
231
|
+
generatePalette('#6366f1', 8); // 8 theme-harmonious hex colors
|
|
232
|
+
generatePalette(); // default vivid 15-color palette
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Both `createMemoryAdapter` and `createRecurringAdapter` accept `colorMap` and `autoColor` options. Events with an explicit `color` field always take priority.
|
|
236
|
+
|
|
70
237
|
## Settings Panel
|
|
71
238
|
|
|
72
239
|
The `Settings` component provides a theme picker and dynamic fields for controlling view parameters:
|
|
@@ -254,10 +421,10 @@ const viewState = createViewState({
|
|
|
254
421
|
src/lib/
|
|
255
422
|
├── core/ # Clock, time utils, locale, types
|
|
256
423
|
├── engine/ # Reactive state: event-store, view-state, selection, drag
|
|
257
|
-
├── adapters/ # Data layer: memory
|
|
424
|
+
├── adapters/ # Data layer: memory, recurring, REST adapters
|
|
258
425
|
├── primitives/ # Low-level UI atoms: NowIndicator, EventBlock, TimeGutter...
|
|
259
426
|
├── calendar/ # Calendar shell, Toolbar
|
|
260
|
-
├── views/ # View components (day/, week/, agenda/, settings/)
|
|
427
|
+
├── views/ # View components (day/, week/, agenda/, schedule/, settings/)
|
|
261
428
|
└── theme/ # Preset themes and token definitions
|
|
262
429
|
```
|
|
263
430
|
|
|
@@ -281,8 +448,9 @@ import {
|
|
|
281
448
|
|
|
282
449
|
| Adapter | Use |
|
|
283
450
|
|---------|-----|
|
|
284
|
-
| `createMemoryAdapter(events)` | In-memory — great for demos and prototyping |
|
|
285
|
-
| `
|
|
451
|
+
| `createMemoryAdapter(events, options?)` | In-memory — great for demos and prototyping. Supports `colorMap` and `autoColor` (including theme-aware). |
|
|
452
|
+
| `createRecurringAdapter(schedule, options?)` | Weekly recurring schedules — auto-projects onto viewed weeks. Read-only. Supports `colorMap` and `autoColor`. |
|
|
453
|
+
| `createRestAdapter(options)` | Fetch from a REST API with configurable endpoints. |
|
|
286
454
|
|
|
287
455
|
## Standalone Views
|
|
288
456
|
|
|
@@ -302,6 +470,65 @@ Each view works independently without the Calendar shell:
|
|
|
302
470
|
<WeekHeatmap style={parchment} events={events} height={320} />
|
|
303
471
|
```
|
|
304
472
|
|
|
473
|
+
## Embeddable Widget
|
|
474
|
+
|
|
475
|
+
Drop a single `<script>` tag into **any** HTML page — no Svelte, no build tools, no npm needed.
|
|
476
|
+
|
|
477
|
+
### From CDN
|
|
478
|
+
|
|
479
|
+
```html
|
|
480
|
+
<script src="https://cdn.jsdelivr.net/npm/@nomideusz/svelte-calendar/widget/widget.js"></script>
|
|
481
|
+
|
|
482
|
+
<day-calendar
|
|
483
|
+
api="https://myschool.com/api/events"
|
|
484
|
+
theme="neutral"
|
|
485
|
+
height="600"
|
|
486
|
+
></day-calendar>
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
That's it. Two lines.
|
|
490
|
+
|
|
491
|
+
### With inline events (no API)
|
|
492
|
+
|
|
493
|
+
```html
|
|
494
|
+
<script src="https://cdn.jsdelivr.net/npm/@nomideusz/svelte-calendar/widget/widget.js"></script>
|
|
495
|
+
|
|
496
|
+
<day-calendar
|
|
497
|
+
theme="midnight"
|
|
498
|
+
height="500"
|
|
499
|
+
events='[
|
|
500
|
+
{ "id": "1", "title": "Yoga Flow", "start": "2025-03-01T09:00", "end": "2025-03-01T10:00", "color": "#818cf8" },
|
|
501
|
+
{ "id": "2", "title": "Meditation", "start": "2025-03-01T12:00", "end": "2025-03-01T12:45", "color": "#34d399" }
|
|
502
|
+
]'
|
|
503
|
+
></day-calendar>
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Widget attributes
|
|
507
|
+
|
|
508
|
+
| Attribute | Default | Description |
|
|
509
|
+
|-----------|---------|-------------|
|
|
510
|
+
| `api` | — | REST API base URL — fetches from `{api}/events?start=...&end=...` |
|
|
511
|
+
| `events` | — | JSON string of events (alternative to `api`) |
|
|
512
|
+
| `theme` | `neutral` | Preset: `midnight`, `parchment`, `indigo`, `neutral`, `bare` |
|
|
513
|
+
| `view` | `week-grid` | Default view: `day-grid`, `week-grid`, `day-timeline`, `day-agenda`, `week-agenda`, `week-heatmap` |
|
|
514
|
+
| `height` | `600` | Height in pixels |
|
|
515
|
+
| `locale` | — | BCP 47 locale (`en-US`, `pl-PL`, `ar-SA`, etc.) |
|
|
516
|
+
| `dir` | — | Text direction: `ltr`, `rtl`, `auto` |
|
|
517
|
+
| `mondaystart` | `true` | Start week on Monday (`true`/`false`) |
|
|
518
|
+
| `headers` | — | JSON string of HTTP headers for the REST adapter |
|
|
519
|
+
|
|
520
|
+
### REST API contract
|
|
521
|
+
|
|
522
|
+
When using the `api` attribute, the widget expects your endpoint to accept:
|
|
523
|
+
|
|
524
|
+
```
|
|
525
|
+
GET {api}/events?start={ISO}&end={ISO}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
And return either:
|
|
529
|
+
- `[{ id, title, start, end, color? }, ...]`
|
|
530
|
+
- `{ events: [{ id, title, start, end, color? }, ...] }`
|
|
531
|
+
|
|
305
532
|
## Development
|
|
306
533
|
|
|
307
534
|
```bash
|
|
@@ -309,6 +536,7 @@ pnpm install
|
|
|
309
536
|
pnpm dev # SvelteKit dev server (demo app)
|
|
310
537
|
pnpm check # Type check
|
|
311
538
|
pnpm run package # Build the library into dist/
|
|
539
|
+
pnpm run build:widget # Build standalone widget.js
|
|
312
540
|
```
|
|
313
541
|
|
|
314
542
|
## 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,15 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* Auto-assign colors to events by category or title.
|
|
19
|
+
* true → use the default vivid palette
|
|
20
|
+
* string → hex accent color (e.g. '#6366f1') to generate a
|
|
21
|
+
* theme-harmonious palette via golden-angle hue rotation
|
|
22
|
+
*/
|
|
23
|
+
autoColor?: boolean | string;
|
|
24
|
+
}
|
|
25
|
+
export declare function createMemoryAdapter(initial?: TimelineEvent[], options?: MemoryAdapterOptions): CalendarAdapter;
|
package/dist/adapters/memory.js
CHANGED
|
@@ -1,27 +1,61 @@
|
|
|
1
|
+
import { generatePalette, VIVID_PALETTE } from '../core/palette.js';
|
|
1
2
|
let counter = 0;
|
|
2
3
|
function uid() {
|
|
3
4
|
return `mem-${Date.now()}-${++counter}`;
|
|
4
5
|
}
|
|
5
|
-
|
|
6
|
+
/** Default palette for auto-coloring */
|
|
7
|
+
const AUTO_COLORS = VIVID_PALETTE;
|
|
8
|
+
export function createMemoryAdapter(initial = [], options = {}) {
|
|
9
|
+
const { colorMap, autoColor } = options;
|
|
6
10
|
const events = [...initial];
|
|
11
|
+
// Resolve palette: vivid default or theme-aware
|
|
12
|
+
const palette = autoColor
|
|
13
|
+
? typeof autoColor === 'string'
|
|
14
|
+
? generatePalette(autoColor)
|
|
15
|
+
: AUTO_COLORS
|
|
16
|
+
: AUTO_COLORS;
|
|
17
|
+
// Build auto-color assignments
|
|
18
|
+
const colorAssignments = new Map();
|
|
19
|
+
let colorIndex = 0;
|
|
20
|
+
function resolveColor(ev) {
|
|
21
|
+
if (ev.color)
|
|
22
|
+
return ev.color;
|
|
23
|
+
if (!colorMap && !autoColor)
|
|
24
|
+
return undefined;
|
|
25
|
+
const key = ev.category ?? ev.title;
|
|
26
|
+
if (colorMap?.[key])
|
|
27
|
+
return colorMap[key];
|
|
28
|
+
if (autoColor) {
|
|
29
|
+
if (!colorAssignments.has(key)) {
|
|
30
|
+
colorAssignments.set(key, palette[colorIndex % palette.length]);
|
|
31
|
+
colorIndex++;
|
|
32
|
+
}
|
|
33
|
+
return colorAssignments.get(key);
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
function withColor(ev) {
|
|
38
|
+
const color = resolveColor(ev);
|
|
39
|
+
return color ? { ...ev, color } : ev;
|
|
40
|
+
}
|
|
7
41
|
function overlaps(ev, range) {
|
|
8
42
|
return ev.start < range.end && ev.end > range.start;
|
|
9
43
|
}
|
|
10
44
|
return {
|
|
11
45
|
async fetchEvents(range) {
|
|
12
|
-
return events.filter((ev) => overlaps(ev, range));
|
|
46
|
+
return events.filter((ev) => overlaps(ev, range)).map(withColor);
|
|
13
47
|
},
|
|
14
48
|
async createEvent(data) {
|
|
15
49
|
const ev = { ...data, id: uid() };
|
|
16
50
|
events.push(ev);
|
|
17
|
-
return ev;
|
|
51
|
+
return withColor(ev);
|
|
18
52
|
},
|
|
19
53
|
async updateEvent(id, patch) {
|
|
20
54
|
const idx = events.findIndex((e) => e.id === id);
|
|
21
55
|
if (idx < 0)
|
|
22
56
|
throw new Error(`Event not found: ${id}`);
|
|
23
57
|
events[idx] = { ...events[idx], ...patch, id };
|
|
24
|
-
return events[idx];
|
|
58
|
+
return withColor(events[idx]);
|
|
25
59
|
},
|
|
26
60
|
async deleteEvent(id) {
|
|
27
61
|
const idx = events.findIndex((e) => e.id === id);
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
/**
|
|
31
|
+
* Auto-assign colors to events by category or title.
|
|
32
|
+
* true → use the default vivid palette
|
|
33
|
+
* string → hex accent color (e.g. '#6366f1') to generate a
|
|
34
|
+
* theme-harmonious palette via golden-angle hue rotation
|
|
35
|
+
*/
|
|
36
|
+
autoColor?: boolean | string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create a CalendarAdapter that projects recurring weekly events
|
|
40
|
+
* onto concrete dates for whatever range the calendar requests.
|
|
41
|
+
*
|
|
42
|
+
* Read-only by default — create/update/delete throw unless custom handlers are provided.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createRecurringAdapter(schedule: RecurringEvent[], options?: RecurringAdapterOptions): CalendarAdapter;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { startOfWeek } from '../core/time.js';
|
|
2
|
+
import { DAY_MS } from '../core/time.js';
|
|
3
|
+
import { generatePalette, VIVID_PALETTE } from '../core/palette.js';
|
|
4
|
+
/** Parse "HH:MM" into [hours, minutes] */
|
|
5
|
+
function parseTime(time) {
|
|
6
|
+
const [h, m] = time.split(':').map(Number);
|
|
7
|
+
return [h, m ?? 0];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Convert ISO weekday (1=Mon…7=Sun) to JS Date weekday offset from Monday.
|
|
11
|
+
* Monday = 0, Tuesday = 1, … Sunday = 6
|
|
12
|
+
*/
|
|
13
|
+
function isoWeekdayToOffset(dayOfWeek) {
|
|
14
|
+
return dayOfWeek - 1; // 1→0, 2→1, …, 7→6
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Project a recurring event onto a specific week, returning a concrete TimelineEvent.
|
|
18
|
+
*/
|
|
19
|
+
function projectToWeek(rec, weekStartMs, weekIndex) {
|
|
20
|
+
const dayOffset = isoWeekdayToOffset(rec.dayOfWeek);
|
|
21
|
+
const dayMs = weekStartMs + dayOffset * DAY_MS;
|
|
22
|
+
const dayDate = new Date(dayMs);
|
|
23
|
+
const [sh, sm] = parseTime(rec.startTime);
|
|
24
|
+
const [eh, em] = parseTime(rec.endTime);
|
|
25
|
+
const start = new Date(dayDate.getFullYear(), dayDate.getMonth(), dayDate.getDate(), sh, sm);
|
|
26
|
+
const end = new Date(dayDate.getFullYear(), dayDate.getMonth(), dayDate.getDate(), eh, em);
|
|
27
|
+
return {
|
|
28
|
+
id: `${rec.id}--w${weekIndex}--d${rec.dayOfWeek}`,
|
|
29
|
+
title: rec.title,
|
|
30
|
+
start,
|
|
31
|
+
end,
|
|
32
|
+
color: rec.color,
|
|
33
|
+
category: rec.category,
|
|
34
|
+
data: {
|
|
35
|
+
...rec.data,
|
|
36
|
+
recurringId: rec.id,
|
|
37
|
+
...(rec.subtitle ? { subtitle: rec.subtitle } : {}),
|
|
38
|
+
...(rec.tags ? { tags: rec.tags } : {}),
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Find all weeks that overlap a given date range.
|
|
44
|
+
* Returns an array of { weekStartMs, weekIndex } objects.
|
|
45
|
+
*/
|
|
46
|
+
function getOverlappingWeeks(range, mondayStart) {
|
|
47
|
+
const weeks = [];
|
|
48
|
+
let cursor = startOfWeek(range.start.getTime(), mondayStart);
|
|
49
|
+
let index = 0;
|
|
50
|
+
while (cursor < range.end.getTime()) {
|
|
51
|
+
weeks.push({ weekStartMs: cursor, weekIndex: index });
|
|
52
|
+
cursor += 7 * DAY_MS;
|
|
53
|
+
index++;
|
|
54
|
+
}
|
|
55
|
+
return weeks;
|
|
56
|
+
}
|
|
57
|
+
/** Default palette for auto-coloring */
|
|
58
|
+
const AUTO_COLORS = VIVID_PALETTE;
|
|
59
|
+
/**
|
|
60
|
+
* Create a CalendarAdapter that projects recurring weekly events
|
|
61
|
+
* onto concrete dates for whatever range the calendar requests.
|
|
62
|
+
*
|
|
63
|
+
* Read-only by default — create/update/delete throw unless custom handlers are provided.
|
|
64
|
+
*/
|
|
65
|
+
export function createRecurringAdapter(schedule, options = {}) {
|
|
66
|
+
const { mondayStart = true, colorMap, autoColor } = options;
|
|
67
|
+
// Resolve palette: vivid default or theme-aware
|
|
68
|
+
const palette = autoColor
|
|
69
|
+
? typeof autoColor === 'string'
|
|
70
|
+
? generatePalette(autoColor)
|
|
71
|
+
: AUTO_COLORS
|
|
72
|
+
: AUTO_COLORS;
|
|
73
|
+
// Build auto-color assignments
|
|
74
|
+
const colorAssignments = new Map();
|
|
75
|
+
if (autoColor || colorMap) {
|
|
76
|
+
let colorIndex = 0;
|
|
77
|
+
for (const rec of schedule) {
|
|
78
|
+
const key = rec.category ?? rec.title;
|
|
79
|
+
if (colorMap?.[key]) {
|
|
80
|
+
colorAssignments.set(key, colorMap[key]);
|
|
81
|
+
}
|
|
82
|
+
else if (autoColor && !colorAssignments.has(key)) {
|
|
83
|
+
colorAssignments.set(key, palette[colorIndex % palette.length]);
|
|
84
|
+
colorIndex++;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function resolveColor(rec) {
|
|
89
|
+
if (rec.color)
|
|
90
|
+
return rec.color;
|
|
91
|
+
const key = rec.category ?? rec.title;
|
|
92
|
+
return colorAssignments.get(key);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
async fetchEvents(range) {
|
|
96
|
+
const weeks = getOverlappingWeeks(range, mondayStart);
|
|
97
|
+
const events = [];
|
|
98
|
+
for (const { weekStartMs, weekIndex } of weeks) {
|
|
99
|
+
for (const rec of schedule) {
|
|
100
|
+
const coloredRec = { ...rec, color: resolveColor(rec) };
|
|
101
|
+
const ev = projectToWeek(coloredRec, weekStartMs, weekIndex);
|
|
102
|
+
// Only include if the event overlaps the requested range
|
|
103
|
+
if (ev.start < range.end && ev.end > range.start) {
|
|
104
|
+
events.push(ev);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return events;
|
|
109
|
+
},
|
|
110
|
+
async createEvent() {
|
|
111
|
+
throw new Error('createRecurringAdapter is read-only. Use a memory or REST adapter for mutations.');
|
|
112
|
+
},
|
|
113
|
+
async updateEvent() {
|
|
114
|
+
throw new Error('createRecurringAdapter is read-only. Use a memory or REST adapter for mutations.');
|
|
115
|
+
},
|
|
116
|
+
async deleteEvent() {
|
|
117
|
+
throw new Error('createRecurringAdapter is read-only. Use a memory or REST adapter for mutations.');
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -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/index.d.ts
CHANGED
|
@@ -5,3 +5,4 @@ export { setDefaultLocale, getDefaultLocale, is24HourLocale, fmtH, weekdayShort,
|
|
|
5
5
|
export { toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './timezone.js';
|
|
6
6
|
export type { TimelineEvent, WeekTimelineProps, DayTimelineProps, } from './types.js';
|
|
7
7
|
export { timeToX } from './types.js';
|
|
8
|
+
export { generatePalette, VIVID_PALETTE } from './palette.js';
|
package/dist/core/index.js
CHANGED
|
@@ -8,3 +8,5 @@ export { setDefaultLocale, getDefaultLocale, is24HourLocale, fmtH, weekdayShort,
|
|
|
8
8
|
// Timezone utilities
|
|
9
9
|
export { toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './timezone.js';
|
|
10
10
|
export { timeToX } from './types.js';
|
|
11
|
+
// Palette
|
|
12
|
+
export { generatePalette, VIVID_PALETTE } from './palette.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart auto-color palette generator.
|
|
3
|
+
*
|
|
4
|
+
* Given a base accent hex (e.g. from `--dt-accent`), generates a
|
|
5
|
+
* palette of perceptually distinct colors that harmonize with the theme.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* generatePalette('#ef4444', 8) // 8 theme-harmonious colors
|
|
9
|
+
* generatePalette(undefined, 8) // falls back to the vivid default
|
|
10
|
+
*/
|
|
11
|
+
export declare const VIVID_PALETTE: string[];
|
|
12
|
+
/**
|
|
13
|
+
* Generate `count` visually distinct and theme-harmonious colors
|
|
14
|
+
* by rotating hue evenly from the base accent, keeping saturation
|
|
15
|
+
* and lightness within a pleasant range.
|
|
16
|
+
*
|
|
17
|
+
* Dark themes (l < 0.5): bump lightness to 0.55–0.65 so colors pop on dark bg.
|
|
18
|
+
* Light themes (l ≥ 0.5): pull lightness to 0.38–0.48 so colors read on light bg.
|
|
19
|
+
*
|
|
20
|
+
* @param accent Hex color string (e.g. '#ef4444'). If undefined, returns VIVID_PALETTE.
|
|
21
|
+
* @param count Number of colors to generate (default: 15).
|
|
22
|
+
*/
|
|
23
|
+
export declare function generatePalette(accent?: string, count?: number): string[];
|