@bimetal/calendar-svelte-components 0.30.0 → 0.32.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.
@@ -4,6 +4,7 @@
4
4
  import type { CalendarStore } from '@bimetal/calendar-data';
5
5
  import type { CalendarRule } from '@bimetal/calendar-rules';
6
6
  import { parseDragInfo } from '@bimetal/calendar-headless';
7
+ import { isEditableTarget } from '@bimetal/a11y-dom';
7
8
  import {
8
9
  setCalendarContext,
9
10
  setCalendarConfig,
@@ -11,8 +12,9 @@
11
12
  } from '../binding/index.js';
12
13
  import type {
13
14
  CalendarConfig, CalendarConfigInput, CustomViewDefinition, EventDialogMode, RenderEventFn, AnchorRect,
15
+ CalendarController, CalendarControllerSnapshot,
14
16
  } from '../binding/index.js';
15
- import { onDestroy, untrack } from 'svelte';
17
+ import { onMount, onDestroy, untrack } from 'svelte';
16
18
  import CalendarHeader from './CalendarHeader.svelte';
17
19
  import EventDialog from './EventDialog.svelte';
18
20
  import UndoToast from './UndoToast.svelte';
@@ -27,29 +29,41 @@
27
29
  import TimelineView from '../views/timeline/TimelineView.svelte';
28
30
 
29
31
  let {
30
- store,
32
+ controller = undefined,
33
+ store = undefined,
31
34
  rules = undefined,
32
35
  config: configInput = undefined,
33
36
  darkMode = false,
37
+ dir = undefined,
34
38
  showCategoryFilter = false,
35
39
  showResourceFilter = false,
36
40
  customViews = undefined,
37
41
  eventDialog = { kind: 'builtin' },
38
42
  onEventClick: consumerOnEventClick = undefined,
39
43
  onNewEventRequested: consumerOnNewEventRequested = undefined,
44
+ onSelectionChange = undefined,
40
45
  renderEvent = undefined,
41
46
  onError = undefined,
42
47
  }: {
43
- store: CalendarStore;
48
+ /** BOUND (composition contract, F1): an already-owned controller (host-owned).
49
+ * The component only subscribes + renders — it never creates, destroys, or
50
+ * reconfigures it (the host owns lifecycle, dialog mode, and delegation). */
51
+ controller?: CalendarController;
52
+ /** STANDALONE: the component creates + owns a controller from these. */
53
+ store?: CalendarStore;
44
54
  rules?: CalendarRule[];
45
55
  config?: CalendarConfigInput;
46
56
  darkMode?: boolean;
57
+ dir?: 'ltr' | 'rtl';
47
58
  showCategoryFilter?: boolean;
48
59
  showResourceFilter?: boolean;
49
60
  customViews?: readonly CustomViewDefinition[];
50
61
  eventDialog?: EventDialogMode;
51
62
  onEventClick?: (eventId: string, anchorRect: AnchorRect, instanceDate?: number) => void;
52
63
  onNewEventRequested?: (defaultDate: CalendarDateTime, anchorRect: AnchorRect, defaultResourceId?: string) => void;
64
+ /** OBSERVE hook: fires when the SELECTED EVENT changes (state, including clears)
65
+ * — a SEPARATE concern from the `none`-mode delegation callbacks. */
66
+ onSelectionChange?: (selectedEvent: { eventId: string; instanceDate?: number } | null) => void;
53
67
  renderEvent?: RenderEventFn;
54
68
  /** Controller error channel: domain rejections surface as toasts; any other
55
69
  * error (e.g. ConcurrencyError) is passed here instead of rejecting. */
@@ -59,14 +73,19 @@
59
73
  // ── Canonical controller binding ──────────────────────────────
60
74
  // The controller owns ALL interaction state and semantics. This component is
61
75
  // a thin bridge: it subscribes to the snapshot, forwards intents, and parses
62
- // DOM events into DragInfo. The controller is created ONCE (under `untrack`):
63
- // `store` is immutable after mount — runes + `untrack` deliberately do NOT
64
- // recreate it when the `store` prop changes; remount the component (e.g. via
65
- // `{#key store}`) to switch stores. config/rules/customViews/eventDialog are
66
- // captured here only as the initial value and then pushed reactively via the
67
- // `$effect`s below, so the initial-value reads are deliberate.
68
- const binding = untrack(() => createCalendarControllerBinding({
69
- store,
76
+ // DOM events into DragInfo.
77
+ //
78
+ // Dual-API (composition contract, F1): a passed `controller` selects the BOUND
79
+ // path (binding = null subscribe only, never create/destroy/reconfigure; the
80
+ // host owns lifecycle + dialog mode). Otherwise STANDALONE creates + owns one.
81
+ // The controller is created ONCE (under `untrack`): `store` is immutable after
82
+ // mount runes + `untrack` deliberately do NOT recreate it when the `store`
83
+ // prop changes; remount the component (e.g. via `{#key store}`) to switch
84
+ // stores. config/rules/customViews/eventDialog are captured here only as the
85
+ // initial value and then pushed reactively via the STANDALONE `$effect`s below,
86
+ // so the initial-value reads are deliberate.
87
+ const binding = untrack(() => (controller ? null : createCalendarControllerBinding({
88
+ store: store!,
70
89
  config: configInput,
71
90
  rules,
72
91
  customViews,
@@ -75,28 +94,42 @@
75
94
  // never go stale even though the controller is created once.
76
95
  onEventClickExternal: (id, rect, inst) => consumerOnEventClick?.(id, rect, inst),
77
96
  onNewEventRequestedExternal: (date, rect, rid) => consumerOnNewEventRequested?.(date, rect, rid),
97
+ // OBSERVE: fires on selection state change (separate from `none` delegation).
98
+ onSelectionChange: (sel) => onSelectionChange?.(sel),
78
99
  onError: (err) => onError?.(err),
79
- }));
80
- const ctrl = binding.ctrl;
100
+ })));
101
+ // `bound` is a mount-time decision (like the immutable `store`): captured once.
102
+ const bound = untrack(() => controller !== undefined);
103
+ const ctrl: CalendarController = controller ?? binding!.ctrl;
81
104
 
82
- // `$binding` auto-subscribes via the store contract; the snapshot is
83
- // identity-stable until an intent changes something.
84
- const snapshot = $derived($binding);
105
+ // Snapshot is a `$state` refreshed on subscribe one path for BOUND and
106
+ // STANDALONE alike (both `ctrl.subscribe`/`getSnapshot` off the same contract).
107
+ let snapshot = $state<CalendarControllerSnapshot>(untrack(() => ctrl.getSnapshot()));
108
+ onMount(() => {
109
+ snapshot = ctrl.getSnapshot();
110
+ return ctrl.subscribe(() => (snapshot = ctrl.getSnapshot()));
111
+ });
85
112
 
86
- // ── Reactive option updates (config is reactive, not mount-captured) ──
87
- $effect(() => { ctrl.setConfig(configInput); });
88
- $effect(() => { ctrl.setRules(rules); });
89
- $effect(() => { ctrl.setCustomViews(customViews); });
90
- // F8: dialog mode is a dynamic prop — keep the controller in sync after mount.
91
- $effect(() => { ctrl.setEventDialogKind(eventDialog.kind); });
113
+ // ── Reactive option updates STANDALONE only. The bound host owns lifecycle,
114
+ // config, dialog mode and delegation, so the bound path reconfigures NOTHING. ──
115
+ if (!bound) {
116
+ $effect(() => { ctrl.setConfig(configInput); });
117
+ $effect(() => { ctrl.setRules(rules); });
118
+ $effect(() => { ctrl.setCustomViews(customViews); });
119
+ // F8: dialog mode is a dynamic prop — keep the (owned) controller in sync.
120
+ $effect(() => { ctrl.setEventDialogKind(eventDialog.kind); });
92
121
 
93
- onDestroy(() => binding.destroy());
122
+ onDestroy(() => binding!.destroy());
123
+ }
94
124
 
95
125
  // ── v0.29 Tastatur: root-scoped keydown → controller (Ctrl/Cmd+Z undo, Escape
96
- // clears selection). Bound on the `.calendar` root below (onkeydown) — NOT a window
126
+ // clears selection). Bound on the `.bm-cal` root below (onkeydown) — NOT a window
97
127
  // listener (no document-level routing). Semantics live in the controller. ──
98
128
  function onKey(e: KeyboardEvent) {
99
- ctrl.handleKeydown(e);
129
+ ctrl.handleKeydown({
130
+ key: e.key, ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey,
131
+ isEditable: isEditableTarget(e.target), preventDefault: () => e.preventDefault(),
132
+ });
100
133
  }
101
134
 
102
135
  // ── DOM-event → controller-intent bindings (the only place DOM is parsed) ──
@@ -154,8 +187,8 @@
154
187
  onDragStart: ctrl.startDrag,
155
188
  onHidePreview: () => ctrl.hidePreview(),
156
189
  onDragEnd: () => ctrl.endDrag(),
157
- get selectedEventId() { return snapshot.selection?.eventId ?? null; },
158
- get selectedInstanceDate() { return snapshot.selection?.instanceDate; },
190
+ get selectedEventId() { return snapshot.selectedEvent?.eventId ?? null; },
191
+ get selectedInstanceDate() { return snapshot.selectedEvent?.instanceDate; },
159
192
  onSelectEvent: (eventId: string, instanceDate?: number) => ctrl.selectEvent(eventId, instanceDate),
160
193
  onClearSelection: () => ctrl.clearSelection(),
161
194
  onUndo: (entityId: string) => ctrl.undo(entityId),
@@ -167,7 +200,7 @@
167
200
  </script>
168
201
 
169
202
  <!-- svelte-ignore a11y_no_static_element_interactions -->
170
- <div class="calendar" class:dark={darkMode} onkeydown={onKey}>
203
+ <div class="bm-cal" data-theme={darkMode ? 'dark' : undefined} dir={dir} onkeydown={onKey}>
171
204
  <CalendarHeader
172
205
  title={snapshot.title}
173
206
  currentView={snapshot.currentView}
@@ -228,8 +261,8 @@
228
261
  config={snapshot.config}
229
262
  onNewEvent={(d, anchorRect) => ctrl.openCreate(d, anchorRect ?? new DOMRect(0, 0, 0, 0))}
230
263
  onEventClick={(eventId, anchorRect, instanceDate) => ctrl.openEdit(eventId, anchorRect, instanceDate)}
231
- selectedEventId={snapshot.selection?.eventId ?? null}
232
- selectedInstanceDate={snapshot.selection?.instanceDate}
264
+ selectedEventId={snapshot.selectedEvent?.eventId ?? null}
265
+ selectedInstanceDate={snapshot.selectedEvent?.instanceDate}
233
266
  onSelectEvent={(eventId, instanceDate) => ctrl.selectEvent(eventId, instanceDate)}
234
267
  onClearSelection={() => ctrl.clearSelection()}
235
268
  />
@@ -1,18 +1,30 @@
1
1
  import type { CalendarDateTime } from '@bimetal/core';
2
2
  import type { CalendarStore } from '@bimetal/calendar-data';
3
3
  import type { CalendarRule } from '@bimetal/calendar-rules';
4
- import type { CalendarConfigInput, CustomViewDefinition, EventDialogMode, RenderEventFn, AnchorRect } from '../binding/index.js';
4
+ import type { CalendarConfigInput, CustomViewDefinition, EventDialogMode, RenderEventFn, AnchorRect, CalendarController } from '../binding/index.js';
5
5
  type $$ComponentProps = {
6
- store: CalendarStore;
6
+ /** BOUND (composition contract, F1): an already-owned controller (host-owned).
7
+ * The component only subscribes + renders — it never creates, destroys, or
8
+ * reconfigures it (the host owns lifecycle, dialog mode, and delegation). */
9
+ controller?: CalendarController;
10
+ /** STANDALONE: the component creates + owns a controller from these. */
11
+ store?: CalendarStore;
7
12
  rules?: CalendarRule[];
8
13
  config?: CalendarConfigInput;
9
14
  darkMode?: boolean;
15
+ dir?: 'ltr' | 'rtl';
10
16
  showCategoryFilter?: boolean;
11
17
  showResourceFilter?: boolean;
12
18
  customViews?: readonly CustomViewDefinition[];
13
19
  eventDialog?: EventDialogMode;
14
20
  onEventClick?: (eventId: string, anchorRect: AnchorRect, instanceDate?: number) => void;
15
21
  onNewEventRequested?: (defaultDate: CalendarDateTime, anchorRect: AnchorRect, defaultResourceId?: string) => void;
22
+ /** OBSERVE hook: fires when the SELECTED EVENT changes (state, including clears)
23
+ * — a SEPARATE concern from the `none`-mode delegation callbacks. */
24
+ onSelectionChange?: (selectedEvent: {
25
+ eventId: string;
26
+ instanceDate?: number;
27
+ } | null) => void;
16
28
  renderEvent?: RenderEventFn;
17
29
  /** Controller error channel: domain rejections surface as toasts; any other
18
30
  * error (e.g. ConcurrencyError) is passed here instead of rejecting. */
@@ -1 +1 @@
1
- {"version":3,"file":"Calendar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/Calendar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAO5D,OAAO,KAAK,EACQ,mBAAmB,EAAE,oBAAoB,EAAE,eAAe,EAAE,aAAa,EAAE,UAAU,EACtG,MAAM,qBAAqB,CAAC;AAe9B,KAAK,gBAAgB,GAAI;IACtB,KAAK,EAAE,aAAa,CAAC;IACrB,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,WAAW,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;IAC9C,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACxF,mBAAmB,CAAC,EAAE,CAAC,WAAW,EAAE,gBAAgB,EAAE,UAAU,EAAE,UAAU,EAAE,iBAAiB,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAClH,WAAW,CAAC,EAAE,aAAa,CAAC;IAC5B;6EACyE;IACzE,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;CAClC,CAAC;AAkMJ,QAAA,MAAM,QAAQ,sDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"Calendar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/Calendar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAQ5D,OAAO,KAAK,EACQ,mBAAmB,EAAE,oBAAoB,EAAE,eAAe,EAAE,aAAa,EAAE,UAAU,EACrG,kBAAkB,EACnB,MAAM,qBAAqB,CAAC;AAe9B,KAAK,gBAAgB,GAAI;IACtB;;kFAE8E;IAC9E,UAAU,CAAC,EAAE,kBAAkB,CAAC;IAChC,wEAAwE;IACxE,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACpB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,WAAW,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;IAC9C,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACxF,mBAAmB,CAAC,EAAE,CAAC,WAAW,EAAE,gBAAgB,EAAE,UAAU,EAAE,UAAU,EAAE,iBAAiB,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAClH;0EACsE;IACtE,iBAAiB,CAAC,EAAE,CAAC,aAAa,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,KAAK,IAAI,CAAC;IAC/F,WAAW,CAAC,EAAE,aAAa,CAAC;IAC5B;6EACyE;IACzE,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;CAClC,CAAC;AAyNJ,QAAA,MAAM,QAAQ,sDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
@@ -24,24 +24,24 @@
24
24
  const config = getCalendarConfig();
25
25
  </script>
26
26
 
27
- <div class="calendar-header">
28
- <h1 class="month-title">{title}</h1>
29
- <div class="header-controls">
30
- <div class="view-switcher">
27
+ <div class="bm-cal__header">
28
+ <h1 class="bm-cal__month-title">{title}</h1>
29
+ <div class="bm-cal__header-controls">
30
+ <div class="bm-cal__view-switcher">
31
31
  {#each viewConfigs as vc (vc.type)}
32
32
  <button
33
- class="view-btn"
34
- class:view-btn--active={currentView === vc.type}
33
+ class="bm-cal__view-btn"
34
+ class:bm-cal__view-btn--active={currentView === vc.type}
35
35
  onclick={() => onViewChange(vc.type)}
36
36
  >
37
37
  {vc.label}
38
38
  </button>
39
39
  {/each}
40
40
  </div>
41
- <div class="nav-controls">
42
- <button class="nav-btn" aria-label={config.locale.prev} onclick={onPrev}>&lsaquo;</button>
43
- <button class="today-btn" onclick={onToday}>{config.locale.today}</button>
44
- <button class="nav-btn" aria-label={config.locale.next} onclick={onNext}>&rsaquo;</button>
41
+ <div class="bm-cal__nav-controls">
42
+ <button class="bm-cal__nav" aria-label={config.locale.prev} onclick={onPrev}>&lsaquo;</button>
43
+ <button class="bm-cal__today-btn" onclick={onToday}>{config.locale.today}</button>
44
+ <button class="bm-cal__nav" aria-label={config.locale.next} onclick={onNext}>&rsaquo;</button>
45
45
  </div>
46
46
  </div>
47
47
  </div>
@@ -13,11 +13,11 @@
13
13
  </script>
14
14
 
15
15
  {#if categories.length > 0}
16
- <div class="category-filter">
16
+ <div class="bm-cal-category-filter">
17
17
  {#each categories as cat (cat.name)}
18
18
  {@const isHidden = hiddenCategories.has(cat.name)}
19
19
  <button
20
- class="category-filter__pill{isHidden ? ' category-filter__pill--hidden' : ''}"
20
+ class="bm-cal-category-filter__pill{isHidden ? ' bm-cal-category-filter__pill--hidden' : ''}"
21
21
  style:background-color={cat.color}
22
22
  onclick={() => onToggle(cat.name)}
23
23
  type="button"
@@ -88,10 +88,10 @@
88
88
  const isBar = $derived(!isCompact);
89
89
  const color = $derived(event.metadata.color ?? '#007AFF');
90
90
  const isRecurring = $derived(isExpandedRecurrenceInstance(event) || !!event.recurrence);
91
- const ghostClass = $derived(ghost ? ' event-bar--ghost' : '');
91
+ const ghostClass = $derived(ghost ? ' bm-cal__event-bar--ghost' : '');
92
92
  const instanceDate = $derived(getRecurrenceInstanceMetadata(event)?.instanceDate);
93
93
  const isSelected = $derived(ctx.selectedEventId === event.id && ctx.selectedInstanceDate === instanceDate);
94
- const selectedClass = $derived(isSelected ? ' event-bar--selected' : '');
94
+ const selectedClass = $derived(isSelected ? ' bm-cal__event-bar--selected' : '');
95
95
 
96
96
  const gridStyle = $derived(`grid-column: ${startCol + 1} / span ${span}; grid-row: ${lane + 1};`);
97
97
  const barStyle = $derived(
@@ -118,7 +118,7 @@
118
118
 
119
119
  {#if isCompact}
120
120
  <div
121
- class="event-bar event-bar--single{ghostClass}{selectedClass}{extraClass}"
121
+ class="bm-cal__event-bar bm-cal__event-bar--single{ghostClass}{selectedClass}{extraClass}"
122
122
  style={fullStyle}
123
123
  draggable={!ghost}
124
124
  role="button"
@@ -134,9 +134,9 @@
134
134
  {#if slots.content}
135
135
  {#if typeof slots.content === 'string'}{slots.content}{:else}{@render slots.content()}{/if}
136
136
  {:else}
137
- <span class="event-dot" style:background-color={color}></span>
138
- <span class="event-time">{timeStr}</span>
139
- <span class="event-title event-title--dark">{isRecurring ? '↻ ' : ''}{renderedTitle}</span>
137
+ <span class="bm-cal__event-dot" style:background-color={color}></span>
138
+ <span class="bm-cal__event-time">{timeStr}</span>
139
+ <span class="bm-cal__event-title bm-cal__event-title--dark">{isRecurring ? '↻ ' : ''}{renderedTitle}</span>
140
140
  {/if}
141
141
  {#if slots.badges}
142
142
  {#if typeof slots.badges === 'string'}{slots.badges}{:else}{@render slots.badges()}{/if}
@@ -144,7 +144,7 @@
144
144
  </div>
145
145
  {:else}
146
146
  <div
147
- class="event-bar event-bar--multi{ghostClass}{selectedClass}{extraClass}"
147
+ class="bm-cal__event-bar bm-cal__event-bar--multi{ghostClass}{selectedClass}{extraClass}"
148
148
  style={fullStyle}
149
149
  draggable={!ghost}
150
150
  role="button"
@@ -160,9 +160,8 @@
160
160
  {#if isBar && isStart && !ghost}
161
161
  <!-- svelte-ignore a11y_no_noninteractive_element_interactions a11y_no_noninteractive_tabindex a11y_no_static_element_interactions -->
162
162
  <div
163
- class="resize-handle resize-handle--start"
163
+ class="bm-cal__resize-handle bm-cal__resize-handle--start"
164
164
  role="separator"
165
- aria-label="Resize start"
166
165
  draggable={true}
167
166
  ondragstart={(e) => handleResizeDragStart(e, 'start')}
168
167
  ondragend={ctx.onDragEnd}
@@ -172,7 +171,7 @@
172
171
  {#if slots.content}
173
172
  {#if typeof slots.content === 'string'}{slots.content}{:else}{@render slots.content()}{/if}
174
173
  {:else}
175
- <span class="event-title{eventBarWantsDarkText(color) ? ' event-title--on-light' : ''}">{isRecurring ? '↻ ' : ''}{renderedTitle}</span>
174
+ <span class="bm-cal__event-title{eventBarWantsDarkText(color) ? ' bm-cal__event-title--on-light' : ''}">{isRecurring ? '↻ ' : ''}{renderedTitle}</span>
176
175
  {/if}
177
176
  {#if slots.badges}
178
177
  {#if typeof slots.badges === 'string'}{slots.badges}{:else}{@render slots.badges()}{/if}
@@ -180,9 +179,8 @@
180
179
  {#if isBar && isEnd && !ghost}
181
180
  <!-- svelte-ignore a11y_no_noninteractive_element_interactions a11y_no_noninteractive_tabindex a11y_no_static_element_interactions -->
182
181
  <div
183
- class="resize-handle resize-handle--end"
182
+ class="bm-cal__resize-handle bm-cal__resize-handle--end"
184
183
  role="separator"
185
- aria-label="Resize end"
186
184
  draggable={true}
187
185
  ondragstart={(e) => handleResizeDragStart(e, 'end')}
188
186
  ondragend={ctx.onDragEnd}
@@ -88,11 +88,11 @@
88
88
 
89
89
  <!-- svelte-ignore a11y_click_events_have_key_events -->
90
90
  <!-- svelte-ignore a11y_no_static_element_interactions -->
91
- <div class="dialog-backdrop" onclick={onClose}></div>
91
+ <div class="bm-cal__dialog-backdrop" onclick={onClose}></div>
92
92
  <!-- svelte-ignore a11y_interactive_supports_focus -->
93
93
  <div
94
94
  bind:this={dialogEl}
95
- class="event-dialog"
95
+ class="bm-cal__dialog"
96
96
  role="dialog"
97
97
  aria-modal="true"
98
98
  aria-label={isNew ? config.locale.newEvent : config.locale.editEvent}
@@ -107,24 +107,24 @@
107
107
  />
108
108
  {:else}
109
109
  <!-- Header -->
110
- <div class="dialog-header">
111
- <span class="dialog-title">
110
+ <div class="bm-cal__dialog-header">
111
+ <span class="bm-cal__dialog-title">
112
112
  {isNew ? config.locale.newEvent : config.locale.editEvent}
113
113
  </span>
114
- <button class="dialog-close" aria-label={config.locale.close} onclick={onClose}>&times;</button>
114
+ <button class="bm-cal__dialog-close" aria-label={config.locale.close} onclick={onClose}>&times;</button>
115
115
  </div>
116
116
 
117
117
  <!-- Body -->
118
- <div class="dialog-body">
118
+ <div class="bm-cal__dialog-body">
119
119
  <input
120
- class="dialog-input dialog-input--title"
120
+ class="bm-cal__dialog-input bm-cal__dialog-input--title"
121
121
  type="text"
122
122
  placeholder={config.locale.titlePlaceholder}
123
123
  value={draft.title}
124
124
  oninput={(e) => setDraft({ title: (e.target as HTMLInputElement).value })}
125
125
  />
126
126
 
127
- <label class="dialog-toggle">
127
+ <label class="bm-cal__dialog-toggle">
128
128
  <span>{config.locale.allDay}</span>
129
129
  <input
130
130
  type="checkbox"
@@ -134,9 +134,9 @@
134
134
  </label>
135
135
 
136
136
  {#if !isInstance}
137
- <div class="dialog-recurrence">
138
- <label class="dialog-toggle">
139
- <span>Wiederholen</span>
137
+ <div class="bm-cal__dialog-recurrence">
138
+ <label class="bm-cal__dialog-toggle">
139
+ <span>{config.locale.recurrence}</span>
140
140
  <input
141
141
  type="checkbox"
142
142
  checked={draft.recurring}
@@ -144,22 +144,22 @@
144
144
  />
145
145
  </label>
146
146
  {#if draft.recurring}
147
- <div class="dialog-row" style="gap: 8px; align-items: center;">
148
- <span style="font-size: 13px;">Alle</span>
149
- <input class="dialog-input" type="number" min="1" max="99" style="width: 50px;"
147
+ <div class="bm-cal__dialog-row" style="gap: 8px; align-items: center;">
148
+ <span style="font-size: 13px;">{config.locale.recurEvery}</span>
149
+ <input class="bm-cal__dialog-input" type="number" min="1" max="99" style="width: 50px;"
150
150
  value={draft.recInterval}
151
151
  oninput={(e) => setDraft({ recInterval: parseInt((e.target as HTMLInputElement).value) })} />
152
- <select class="dialog-input"
152
+ <select class="bm-cal__dialog-input"
153
153
  value={draft.recFreq}
154
154
  onchange={(e) => setDraft({ recFreq: (e.target as HTMLSelectElement).value as EventDraft['recFreq'] })}>
155
- <option value="daily">Tage</option>
156
- <option value="weekly">Wochen</option>
157
- <option value="monthly">Monate</option>
158
- <option value="yearly">Jahre</option>
155
+ <option value="daily">{config.locale.recurUnits.daily}</option>
156
+ <option value="weekly">{config.locale.recurUnits.weekly}</option>
157
+ <option value="monthly">{config.locale.recurUnits.monthly}</option>
158
+ <option value="yearly">{config.locale.recurUnits.yearly}</option>
159
159
  </select>
160
- <label class="dialog-label" style="flex-direction: row; align-items: center; gap: 6px;">
161
- <span style="font-size: 13px;">bis</span>
162
- <input class="dialog-input" type="date"
160
+ <label class="bm-cal__dialog-label" style="flex-direction: row; align-items: center; gap: 6px;">
161
+ <span style="font-size: 13px;">{config.locale.recurUntil}</span>
162
+ <input class="bm-cal__dialog-input" type="date"
163
163
  value={draft.recUntil}
164
164
  onchange={(e) => setDraft({ recUntil: (e.target as HTMLInputElement).value })} />
165
165
  </label>
@@ -170,16 +170,16 @@
170
170
 
171
171
  {#if isInstance}
172
172
  <div style="font-size: 12px; color: var(--cal-color-text-muted); padding: 4px 0;">
173
- Instanz einer wiederkehrenden Serie
173
+ {config.locale.recurringInstanceNote}
174
174
  </div>
175
175
  {/if}
176
176
 
177
177
  <!-- Start date row -->
178
- <div class="dialog-row">
179
- <label class="dialog-label">
178
+ <div class="bm-cal__dialog-row">
179
+ <label class="bm-cal__dialog-label">
180
180
  <span>{config.locale.from}</span>
181
181
  <input
182
- class="dialog-input"
182
+ class="bm-cal__dialog-input"
183
183
  type="date"
184
184
  value={draft.startDate}
185
185
  onchange={(e) => setDraft({ startDate: (e.target as HTMLInputElement).value })}
@@ -187,7 +187,7 @@
187
187
  </label>
188
188
  {#if !draft.allDay}
189
189
  <input
190
- class="dialog-input dialog-input--time"
190
+ class="bm-cal__dialog-input bm-cal__dialog-input--time"
191
191
  type="time"
192
192
  value={draft.startTime}
193
193
  onchange={(e) => setDraft({ startTime: (e.target as HTMLInputElement).value })}
@@ -196,11 +196,11 @@
196
196
  </div>
197
197
 
198
198
  <!-- End date row -->
199
- <div class="dialog-row">
200
- <label class="dialog-label">
199
+ <div class="bm-cal__dialog-row">
200
+ <label class="bm-cal__dialog-label">
201
201
  <span>{config.locale.to}</span>
202
202
  <input
203
- class="dialog-input"
203
+ class="bm-cal__dialog-input"
204
204
  type="date"
205
205
  value={draft.endDate}
206
206
  min={draft.startDate}
@@ -209,7 +209,7 @@
209
209
  </label>
210
210
  {#if !draft.allDay}
211
211
  <input
212
- class="dialog-input dialog-input--time"
212
+ class="bm-cal__dialog-input bm-cal__dialog-input--time"
213
213
  type="time"
214
214
  value={draft.endTime}
215
215
  onchange={(e) => setDraft({ endTime: (e.target as HTMLInputElement).value })}
@@ -218,11 +218,11 @@
218
218
  </div>
219
219
 
220
220
  <!-- Category -->
221
- <label class="dialog-label">
221
+ <label class="bm-cal__dialog-label">
222
222
  <span>{config.locale.category}</span>
223
- <div class="dialog-category-row">
223
+ <div class="bm-cal__dialog-category-row">
224
224
  <select
225
- class="dialog-input"
225
+ class="bm-cal__dialog-input"
226
226
  value={draft.category}
227
227
  onchange={(e) => setDraft({ category: (e.target as HTMLSelectElement).value })}
228
228
  >
@@ -230,15 +230,15 @@
230
230
  <option value={cat.name}>{cat.name}</option>
231
231
  {/each}
232
232
  </select>
233
- <span class="category-dot" style:background-color={draft.color}></span>
233
+ <span class="bm-cal__category-dot" style:background-color={draft.color}></span>
234
234
  </div>
235
235
  </label>
236
236
 
237
237
  {#if config.resources.length > 0}
238
- <label class="dialog-label">
239
- <span>{config.locale.resources ?? 'Resources'}</span>
238
+ <label class="bm-cal__dialog-label">
239
+ <span>{config.locale.resources}</span>
240
240
  <select
241
- class="dialog-input"
241
+ class="bm-cal__dialog-input"
242
242
  value={draft.resourceId}
243
243
  onchange={(e) => setDraft({ resourceId: (e.target as HTMLSelectElement).value })}
244
244
  >
@@ -251,10 +251,10 @@
251
251
  {/if}
252
252
 
253
253
  <!-- Location -->
254
- <label class="dialog-label">
254
+ <label class="bm-cal__dialog-label">
255
255
  <span>{config.locale.location}</span>
256
256
  <input
257
- class="dialog-input"
257
+ class="bm-cal__dialog-input"
258
258
  type="text"
259
259
  value={draft.location}
260
260
  oninput={(e) => setDraft({ location: (e.target as HTMLInputElement).value })}
@@ -262,10 +262,10 @@
262
262
  </label>
263
263
 
264
264
  <!-- Description -->
265
- <label class="dialog-label">
265
+ <label class="bm-cal__dialog-label">
266
266
  <span>{config.locale.description}</span>
267
267
  <textarea
268
- class="dialog-input dialog-input--description"
268
+ class="bm-cal__dialog-input bm-cal__dialog-input--description"
269
269
  value={draft.description}
270
270
  oninput={(e) => setDraft({ description: (e.target as HTMLTextAreaElement).value })}
271
271
  ></textarea>
@@ -274,33 +274,33 @@
274
274
 
275
275
  <!-- Validation errors -->
276
276
  {#if validationErrors.length > 0}
277
- <div class="dialog-errors">
277
+ <div class="bm-cal__dialog-errors">
278
278
  {#each validationErrors as err, i (i)}
279
- <div class="dialog-error">{err}</div>
279
+ <div class="bm-cal__dialog-error">{err}</div>
280
280
  {/each}
281
281
  </div>
282
282
  {/if}
283
283
 
284
284
  <!-- Footer -->
285
- <div class="dialog-footer">
285
+ <div class="bm-cal__dialog-footer">
286
286
  {#if !isNew}
287
287
  {#if config.locale.delete !== false}
288
- <button class="dialog-btn dialog-btn--delete" onclick={() => onDelete(event!.id)}>
288
+ <button class="bm-cal__dialog-btn bm-cal__dialog-btn--delete" onclick={() => onDelete(event!.id)}>
289
289
  {config.locale.delete}
290
290
  </button>
291
291
  {/if}
292
292
  {#if history.length > 0}
293
- <button class="dialog-btn dialog-btn--history" onclick={() => { showHistory = true; }}>
293
+ <button class="bm-cal__dialog-btn bm-cal__dialog-btn--history" onclick={() => { showHistory = true; }}>
294
294
  {config.locale.history}
295
295
  </button>
296
296
  {/if}
297
297
  {/if}
298
- <div class="dialog-footer-right">
299
- <button class="dialog-btn dialog-btn--cancel" onclick={onClose}>
298
+ <div class="bm-cal__dialog-footer-right">
299
+ <button class="bm-cal__dialog-btn bm-cal__dialog-btn--cancel" onclick={onClose}>
300
300
  {config.locale.cancel}
301
301
  </button>
302
302
  <button
303
- class="dialog-btn dialog-btn--save"
303
+ class="bm-cal__dialog-btn bm-cal__dialog-btn--save"
304
304
  onclick={onSave}
305
305
  disabled={!draft.title.trim()}
306
306
  >