@forcecalendar/interface 1.4.0 → 1.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forcecalendar/interface",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "description": "Official interface layer for forceCalendar Core - Enterprise calendar components",
6
6
  "main": "dist/force-calendar-interface.umd.js",
@@ -20,9 +20,9 @@
20
20
  "types/**/*.d.ts"
21
21
  ],
22
22
  "sideEffects": [
23
+ "./dist/*.js",
23
24
  "./src/index.js",
24
- "./src/components/ForceCalendar.js",
25
- "./src/components/EventForm.js"
25
+ "./src/components/*.js"
26
26
  ],
27
27
  "scripts": {
28
28
  "dev": "vite",
@@ -58,7 +58,7 @@
58
58
  "devDependencies": {
59
59
  "@babel/core": "^7.28.5",
60
60
  "@babel/preset-env": "^7.28.5",
61
- "@forcecalendar/core": "^2.1.68",
61
+ "@forcecalendar/core": "^2.5.0",
62
62
  "babel-jest": "^30.2.0",
63
63
  "eslint": "^8.57.1",
64
64
  "jest": "^30.2.0",
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { BaseComponent } from '../core/BaseComponent.js';
8
8
  import StateManager from '../core/StateManager.js';
9
- import { StyleUtils } from '../utils/StyleUtils.js';
9
+ import { StyleUtils, THEME_PRESETS } from '../utils/StyleUtils.js';
10
10
  import { DateUtils } from '../utils/DateUtils.js';
11
11
  import { DOMUtils } from '../utils/DOMUtils.js';
12
12
 
@@ -26,7 +26,7 @@ export class ForceCalendar extends BaseComponent {
26
26
  };
27
27
 
28
28
  static get observedAttributes() {
29
- return ['view', 'date', 'locale', 'timezone', 'week-starts-on', 'height'];
29
+ return ['view', 'date', 'locale', 'timezone', 'week-starts-on', 'height', 'theme'];
30
30
  }
31
31
 
32
32
  constructor() {
@@ -35,6 +35,7 @@ export class ForceCalendar extends BaseComponent {
35
35
  this.currentView = null;
36
36
  this._hasRendered = false; // Track if initial render is complete
37
37
  this._busUnsubscribers = [];
38
+ this._pendingEvents = null; // Snapshot assigned before the state manager exists
38
39
  }
39
40
 
40
41
  /**
@@ -86,6 +87,30 @@ export class ForceCalendar extends BaseComponent {
86
87
 
87
88
  // Listen for events
88
89
  this.setupEventListeners();
90
+
91
+ // Frameworks may assign `events` before the element is upgraded (for
92
+ // example when the component is imported lazily); re-run the setter so
93
+ // the snapshot reaches the state manager instead of shadowing the accessor.
94
+ this._upgradeProperty('events');
95
+ if (this._pendingEvents) {
96
+ const pending = this._pendingEvents;
97
+ this._pendingEvents = null;
98
+ this.stateManager.setEvents(pending.events, pending.options);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Re-apply a property that was set as an own data property before the
104
+ * custom element was upgraded, so the class accessor sees the value.
105
+ * @param {string} name
106
+ * @private
107
+ */
108
+ _upgradeProperty(name) {
109
+ if (Object.prototype.hasOwnProperty.call(this, name)) {
110
+ const value = this[name];
111
+ delete this[name];
112
+ this[name] = value;
113
+ }
89
114
  }
90
115
 
91
116
  setupEventListeners() {
@@ -150,6 +175,21 @@ export class ForceCalendar extends BaseComponent {
150
175
  })
151
176
  );
152
177
 
178
+ // Snapshot loads (setEvents / events property) — one event per snapshot
179
+ this._busUnsubscribers.push(
180
+ bus.on('events:set', data => {
181
+ this.emit('calendar-events-set', data);
182
+ })
183
+ );
184
+
185
+ // Visible window changes (view, date or week start), emitted after the
186
+ // navigation / view-change events that caused them
187
+ this._busUnsubscribers.push(
188
+ bus.on('range:changed', data => {
189
+ this.emit('calendar-range-change', data);
190
+ })
191
+ );
192
+
153
193
  // Date selection events
154
194
  this._busUnsubscribers.push(
155
195
  bus.on('date:selected', data => {
@@ -276,6 +316,17 @@ export class ForceCalendar extends BaseComponent {
276
316
  }
277
317
 
278
318
  mount() {
319
+ // Re-entrant: connectedCallback runs again whenever the element is moved
320
+ // or re-attached (React reconciliation/portals, LWC re-render, StrictMode
321
+ // double-mount). The StateManager survives unmount(), so only the bindings
322
+ // released there are restored here; after an explicit destroy() start over.
323
+ if (!this.stateManager || !this.stateManager.state) {
324
+ this.initialize();
325
+ }
326
+ if (!this._stateUnsubscribe) {
327
+ this._stateUnsubscribe = this.stateManager.subscribe(this.handleStateChange.bind(this));
328
+ this.setupEventListeners();
329
+ }
279
330
  this.currentView = this.stateManager.getView();
280
331
  super.mount();
281
332
  }
@@ -801,7 +852,25 @@ export class ForceCalendar extends BaseComponent {
801
852
  return '<div id="calendar-view-container"></div>';
802
853
  }
803
854
 
855
+ /**
856
+ * Apply a named theme preset (e.g. theme="slds") as host-level custom
857
+ * properties so it cascades into the shadow DOM and stays overridable
858
+ * by page-level --fc-* variables.
859
+ */
860
+ _applyTheme(name) {
861
+ const preset = THEME_PRESETS[name];
862
+ for (const token of Object.keys(THEME_PRESETS.slds)) {
863
+ if (preset && preset[token]) {
864
+ this.style.setProperty(token, preset[token]);
865
+ } else {
866
+ this.style.removeProperty(token);
867
+ }
868
+ }
869
+ }
870
+
804
871
  afterRender() {
872
+ this._applyTheme(this.getAttribute('theme'));
873
+
805
874
  // Manually instantiate and mount view renderer (bypasses Locker Service)
806
875
  const container = this.$('#calendar-view-container');
807
876
 
@@ -893,7 +962,19 @@ export class ForceCalendar extends BaseComponent {
893
962
  }
894
963
 
895
964
  // Mark initial render as complete for targeted updates
965
+ const firstRender = !this._hasRendered;
896
966
  this._hasRendered = true;
967
+
968
+ // Announce the initial visible window once the first mount is complete so
969
+ // consumers can load data for it without waiting for a navigation
970
+ if (firstRender && this.stateManager) {
971
+ const state = this.stateManager.getState();
972
+ this.emit('calendar-range-change', {
973
+ ...this.stateManager.getVisibleRange(),
974
+ view: state.view,
975
+ date: state.currentDate
976
+ });
977
+ }
897
978
  }
898
979
 
899
980
  handleNavigation(event) {
@@ -973,6 +1054,58 @@ export class ForceCalendar extends BaseComponent {
973
1054
  return this.stateManager.getEvents();
974
1055
  }
975
1056
 
1057
+ /**
1058
+ * Replace the calendar's events with a complete snapshot, applying only the
1059
+ * differences: unchanged events keep their instance, changed ones are
1060
+ * replaced, new ones are added and events missing from the snapshot are
1061
+ * removed unless `removeMissing` is false. The view re-renders at most once
1062
+ * and a single `calendar-events-set` event describes the change set; no
1063
+ * per-event `calendar-event-add`/`-remove` events are dispatched.
1064
+ *
1065
+ * Before the element is connected the snapshot is stored and applied on
1066
+ * initialisation, in which case `null` is returned.
1067
+ *
1068
+ * @param {Iterable<object|import('../core/StateManager.js').CalendarEvent>} events - Complete snapshot of events
1069
+ * @param {import('../core/StateManager.js').EventsSetOptions} [options={}]
1070
+ * @returns {import('../core/StateManager.js').EventsSetResult|null}
1071
+ */
1072
+ setEvents(events, options = {}) {
1073
+ if (!this.stateManager) {
1074
+ this._pendingEvents = { events: events ? Array.from(events) : [], options };
1075
+ return null;
1076
+ }
1077
+ return this.stateManager.setEvents(events, options);
1078
+ }
1079
+
1080
+ /**
1081
+ * Declarative form of {@link ForceCalendar#setEvents}: assign a complete
1082
+ * snapshot and the calendar reconciles it with `removeMissing: true`.
1083
+ * Reading it returns the events currently held by the calendar.
1084
+ *
1085
+ * @returns {import('../core/StateManager.js').CalendarEvent[]}
1086
+ */
1087
+ get events() {
1088
+ if (this.stateManager) return this.stateManager.getEvents();
1089
+ return this._pendingEvents ? this._pendingEvents.events : [];
1090
+ }
1091
+
1092
+ /**
1093
+ * @param {Iterable<object|import('../core/StateManager.js').CalendarEvent>|null} events - Complete snapshot of events
1094
+ */
1095
+ set events(events) {
1096
+ this.setEvents(events);
1097
+ }
1098
+
1099
+ /**
1100
+ * Get the window of dates the current view covers (leading and trailing
1101
+ * other-month days included). `end` is inclusive.
1102
+ *
1103
+ * @returns {import('../core/StateManager.js').VisibleRange|null} The range, or null before the element is initialised
1104
+ */
1105
+ getVisibleRange() {
1106
+ return this.stateManager ? this.stateManager.getVisibleRange() : null;
1107
+ }
1108
+
976
1109
  setView(view) {
977
1110
  this.stateManager.setView(view);
978
1111
  }
@@ -994,11 +1127,22 @@ export class ForceCalendar extends BaseComponent {
994
1127
  }
995
1128
 
996
1129
  unmount() {
997
- // Called by disconnectedCallback clean up all subscriptions
998
- this.destroy();
1130
+ // Called by disconnectedCallback. Release everything bound to the rendered
1131
+ // tree (subscriptions, view renderer and its timers, DOM listeners) but keep
1132
+ // the StateManager so the element keeps its view, date and events when it
1133
+ // is re-attached. Full teardown is opt-in via destroy().
1134
+ this._releaseBindings();
999
1135
  }
1000
1136
 
1001
1137
  destroy() {
1138
+ this._releaseBindings();
1139
+
1140
+ if (this.stateManager) {
1141
+ this.stateManager.destroy();
1142
+ }
1143
+ }
1144
+
1145
+ _releaseBindings() {
1002
1146
  this._busUnsubscribers.forEach(unsub => unsub());
1003
1147
  this._busUnsubscribers = [];
1004
1148
 
@@ -1009,12 +1153,9 @@ export class ForceCalendar extends BaseComponent {
1009
1153
 
1010
1154
  if (this._currentViewInstance && this._currentViewInstance.cleanup) {
1011
1155
  this._currentViewInstance.cleanup();
1012
- this._currentViewInstance = null;
1013
- }
1014
-
1015
- if (this.stateManager) {
1016
- this.stateManager.destroy();
1017
1156
  }
1157
+ this._currentViewInstance = null;
1158
+ this._hasRendered = false;
1018
1159
  super.cleanup();
1019
1160
  }
1020
1161
  }
@@ -31,6 +31,10 @@ export class BaseComponent extends HTMLElement {
31
31
  disconnectedCallback() {
32
32
  this.unmount();
33
33
  this.cleanup();
34
+ // Drop the rendered tree so a later re-attach starts from an empty shadow
35
+ // root instead of appending a second style/content pair next to the old one.
36
+ if (this._styleEl) this._styleEl.remove();
37
+ if (this._contentWrapper) this._contentWrapper.remove();
34
38
  this._styleEl = null;
35
39
  this._contentWrapper = null;
36
40
  }
@@ -8,6 +8,40 @@
8
8
  import { Calendar } from '@forcecalendar/core';
9
9
  import { EventBus } from './EventBus.js';
10
10
 
11
+ /**
12
+ * @typedef {import('@forcecalendar/core').Event} CalendarEvent
13
+ */
14
+
15
+ /**
16
+ * @typedef {Object} EventsSetOptions
17
+ * @property {boolean} [removeMissing=true] - Remove stored events that are absent from the snapshot
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} EventsSetUpdate
22
+ * @property {CalendarEvent} event - Event now held by the calendar
23
+ * @property {CalendarEvent} oldEvent - Event instance it replaced
24
+ */
25
+
26
+ /**
27
+ * @typedef {Object} EventsSetResult
28
+ * @property {CalendarEvent[]} events - All events after the snapshot was applied
29
+ * @property {CalendarEvent[]} added - Events that were not present before
30
+ * @property {EventsSetUpdate[]} updated - Events whose data changed
31
+ * @property {CalendarEvent[]} removed - Events dropped because they were missing from the snapshot
32
+ * @property {CalendarEvent[]} unchanged - Events left untouched (same instances as before)
33
+ */
34
+
35
+ /**
36
+ * @typedef {Object} VisibleRange
37
+ * @property {Date} start - First instant shown by the current view
38
+ * @property {Date} end - Last instant shown by the current view (inclusive)
39
+ */
40
+
41
+ /**
42
+ * @typedef {VisibleRange & { view: string, date: Date }} VisibleRangeChange
43
+ */
44
+
11
45
  class StateManager {
12
46
  constructor(config = {}) {
13
47
  // Each StateManager gets its own EventBus to prevent cross-instance
@@ -46,6 +80,9 @@ class StateManager {
46
80
 
47
81
  // Initial sync of events from Core (in case events were pre-loaded)
48
82
  this._syncEventsFromCore({ silent: true });
83
+
84
+ // Remember the visible window so range:changed only fires on real changes
85
+ this._visibleRangeKey = this._rangeKey(this.getVisibleRange());
49
86
  }
50
87
 
51
88
  /**
@@ -183,6 +220,7 @@ class StateManager {
183
220
  this.calendar.setView(view);
184
221
  this.setState({ view });
185
222
  this.eventBus.emit('view:changed', { view });
223
+ this._syncVisibleRange();
186
224
  }
187
225
 
188
226
  getView() {
@@ -193,6 +231,7 @@ class StateManager {
193
231
  this.calendar.goToDate(date);
194
232
  this.setState({ currentDate: this.calendar.getCurrentDate() });
195
233
  this.eventBus.emit('date:changed', { date: this.state.currentDate });
234
+ this._syncVisibleRange();
196
235
  }
197
236
 
198
237
  getCurrentDate() {
@@ -204,24 +243,28 @@ class StateManager {
204
243
  this.calendar.next();
205
244
  this.setState({ currentDate: this.calendar.getCurrentDate() });
206
245
  this.eventBus.emit('navigation:next', { date: this.state.currentDate });
246
+ this._syncVisibleRange();
207
247
  }
208
248
 
209
249
  previous() {
210
250
  this.calendar.previous();
211
251
  this.setState({ currentDate: this.calendar.getCurrentDate() });
212
252
  this.eventBus.emit('navigation:previous', { date: this.state.currentDate });
253
+ this._syncVisibleRange();
213
254
  }
214
255
 
215
256
  today() {
216
257
  this.calendar.today();
217
258
  this.setState({ currentDate: this.calendar.getCurrentDate() });
218
259
  this.eventBus.emit('navigation:today', { date: this.state.currentDate });
260
+ this._syncVisibleRange();
219
261
  }
220
262
 
221
263
  goToDate(date) {
222
264
  this.calendar.goToDate(date);
223
265
  this.setState({ currentDate: this.calendar.getCurrentDate() });
224
266
  this.eventBus.emit('navigation:goto', { date: this.state.currentDate });
267
+ this._syncVisibleRange();
225
268
  }
226
269
 
227
270
  // Event management
@@ -285,6 +328,133 @@ class StateManager {
285
328
  return this.calendar.getEvents() || [];
286
329
  }
287
330
 
331
+ /**
332
+ * Replace the calendar's events with a complete snapshot, applying only the
333
+ * differences.
334
+ *
335
+ * Unchanged events keep their existing instance, changed ones are replaced,
336
+ * new ones are added and events missing from the snapshot are removed
337
+ * (unless `removeMissing` is false). The state is updated at most once and a
338
+ * single `events:set` bus event carries the change set. The per-event
339
+ * `event:add`/`event:added`/`event:remove`/`event:deleted` events are NOT
340
+ * emitted, so listeners that persist user edits are not triggered by a
341
+ * snapshot load.
342
+ *
343
+ * Uses `Calendar#reconcileEvents` when the installed core provides it
344
+ * (2.4.0+) and falls back to an id-based diff on older cores.
345
+ *
346
+ * @param {Iterable<object|CalendarEvent>} events - Complete snapshot of events
347
+ * @param {EventsSetOptions} [options={}]
348
+ * @returns {EventsSetResult} The applied change set
349
+ * @throws {Error} If an entry fails validation or two entries share an id (an `event:error` bus event is emitted first)
350
+ */
351
+ setEvents(events, options = {}) {
352
+ const { removeMissing = true } = options;
353
+ const snapshot = events ? Array.from(events) : [];
354
+
355
+ let result;
356
+ try {
357
+ result =
358
+ typeof this.calendar.reconcileEvents === 'function'
359
+ ? this.calendar.setEvents(snapshot, { reconcile: true, removeMissing })
360
+ : this._reconcileFallback(snapshot, removeMissing);
361
+ } catch (error) {
362
+ // Nothing has been applied (core rolls the batch back), so surface the
363
+ // problem to the caller instead of leaving the snapshot half-loaded
364
+ this.eventBus.emit('event:error', { action: 'set', events: snapshot, error });
365
+ throw error;
366
+ }
367
+
368
+ const { added, updated, removed, unchanged } = result;
369
+ /** @type {EventsSetResult} */
370
+ const payload = { events: this.getEvents(), added, updated, removed, unchanged };
371
+
372
+ if (added.length > 0 || updated.length > 0 || removed.length > 0) {
373
+ this.setState({ events: [...payload.events] });
374
+ }
375
+ this.eventBus.emit('events:set', payload);
376
+ return payload;
377
+ }
378
+
379
+ /**
380
+ * Id-based diff for cores that predate `Calendar#reconcileEvents`.
381
+ * Works on the core calendar directly so no per-event bus events fire.
382
+ * Equivalence is approximated by comparing the fields present in the
383
+ * snapshot entry, so an entry that only omits fields is treated as unchanged.
384
+ *
385
+ * @param {Array<object|CalendarEvent>} snapshot
386
+ * @param {boolean} removeMissing
387
+ * @returns {{ added: CalendarEvent[], updated: EventsSetUpdate[], removed: CalendarEvent[], unchanged: CalendarEvent[] }}
388
+ * @private
389
+ */
390
+ _reconcileFallback(snapshot, removeMissing) {
391
+ const incoming = new Map();
392
+ snapshot.forEach(entry => {
393
+ const id = entry && entry.id;
394
+ if (id === undefined || id === null || id === '') {
395
+ throw new Error('Every event in a snapshot must have an id');
396
+ }
397
+ if (incoming.has(id)) {
398
+ throw new Error(`Duplicate event id in snapshot: ${id}`);
399
+ }
400
+ incoming.set(id, entry);
401
+ });
402
+
403
+ const result = { added: [], updated: [], removed: [], unchanged: [] };
404
+ const existingById = new Map(this.getEvents().map(event => [event.id, event]));
405
+
406
+ if (removeMissing) {
407
+ existingById.forEach((existing, id) => {
408
+ if (!incoming.has(id) && this.calendar.removeEvent(id)) {
409
+ result.removed.push(existing);
410
+ }
411
+ });
412
+ }
413
+
414
+ incoming.forEach((entry, id) => {
415
+ const existing = existingById.get(id);
416
+ const data = typeof entry.toObject === 'function' ? entry.toObject() : entry;
417
+ if (!existing) {
418
+ const event = this.calendar.addEvent(entry);
419
+ if (!event) throw new Error(`Failed to add event: ${id}`);
420
+ result.added.push(event);
421
+ } else if (existing === entry || this._isEquivalentFallback(existing, data)) {
422
+ result.unchanged.push(existing);
423
+ } else {
424
+ const event = this.calendar.updateEvent(id, data);
425
+ if (!event) throw new Error(`Failed to update event: ${id}`);
426
+ result.updated.push({ event, oldEvent: existing });
427
+ }
428
+ });
429
+
430
+ return result;
431
+ }
432
+
433
+ /**
434
+ * Field-wise comparison of a stored event against snapshot data.
435
+ * @param {CalendarEvent} existing
436
+ * @param {object} data
437
+ * @returns {boolean}
438
+ * @private
439
+ */
440
+ _isEquivalentFallback(existing, data) {
441
+ const toTime = value => {
442
+ if (value instanceof Date) return value.getTime();
443
+ if (value === undefined || value === null) return NaN;
444
+ return new Date(value).getTime();
445
+ };
446
+ return Object.keys(data).every(key => {
447
+ const stored = existing[key];
448
+ const incoming = data[key];
449
+ if (key === 'start' || key === 'end') return toTime(stored) === toTime(incoming);
450
+ if (stored === incoming) return true;
451
+ if (stored === undefined || stored === null || incoming === undefined || incoming === null) {
452
+ return false;
453
+ }
454
+ return JSON.stringify(stored) === JSON.stringify(incoming);
455
+ });
456
+ }
457
+
288
458
  /**
289
459
  * Force sync state.events from Core calendar
290
460
  * Use this if you've modified events directly on the Core calendar
@@ -351,6 +521,70 @@ class StateManager {
351
521
  return enriched;
352
522
  }
353
523
 
524
+ // Visible range
525
+
526
+ /**
527
+ * Get the window of dates the current view covers, including leading and
528
+ * trailing days from adjacent months in the month view.
529
+ *
530
+ * `end` is the last millisecond of the window (inclusive), so the pair can
531
+ * be passed straight to {@link StateManager#getEventsInRange}.
532
+ *
533
+ * @returns {VisibleRange}
534
+ */
535
+ getVisibleRange() {
536
+ const view = this.calendar.getView();
537
+ const date = this.calendar.getCurrentDate();
538
+
539
+ if (view !== 'day') {
540
+ const viewData = this.calendar.getViewData() || {};
541
+ if (viewData.startDate instanceof Date && viewData.endDate instanceof Date) {
542
+ const start = new Date(viewData.startDate);
543
+ let end = new Date(viewData.endDate);
544
+ // The list view reports an exclusive end; normalise it to inclusive
545
+ if (viewData.type === 'list') {
546
+ end = new Date(end.getTime() - 1);
547
+ }
548
+ return { start, end };
549
+ }
550
+ }
551
+
552
+ // Day view (or a view without an explicit window): midnight to midnight
553
+ const start = new Date(date);
554
+ start.setHours(0, 0, 0, 0);
555
+ const end = new Date(start);
556
+ end.setDate(end.getDate() + 1);
557
+ end.setMilliseconds(-1);
558
+ return { start, end };
559
+ }
560
+
561
+ /**
562
+ * @param {VisibleRange} range
563
+ * @returns {string}
564
+ * @private
565
+ */
566
+ _rangeKey(range) {
567
+ return `${range.start.getTime()}:${range.end.getTime()}`;
568
+ }
569
+
570
+ /**
571
+ * Single choke point for `range:changed`: recompute the visible window and
572
+ * emit only when it differs from the last one that was announced.
573
+ * Called after view, date and week-start changes, once their own bus events
574
+ * have been emitted, so listeners see navigation before the range update.
575
+ * @private
576
+ */
577
+ _syncVisibleRange() {
578
+ if (!this.calendar) return;
579
+ const range = this.getVisibleRange();
580
+ const key = this._rangeKey(range);
581
+ if (key === this._visibleRangeKey) return;
582
+ this._visibleRangeKey = key;
583
+ /** @type {VisibleRangeChange} */
584
+ const payload = { ...range, view: this.state.view, date: this.state.currentDate };
585
+ this.eventBus.emit('range:changed', payload);
586
+ }
587
+
354
588
  // Selection management
355
589
  selectEvent(event) {
356
590
  this.setState({ selectedEvent: event });
@@ -420,6 +654,7 @@ class StateManager {
420
654
  // Update calendar configuration if needed
421
655
  if (config.weekStartsOn !== undefined) {
422
656
  this.calendar.setWeekStartsOn(config.weekStartsOn);
657
+ this._syncVisibleRange();
423
658
  }
424
659
  if (config.locale !== undefined) {
425
660
  this.calendar.setLocale(config.locale);
package/src/index.js CHANGED
@@ -23,3 +23,4 @@ export { DayViewRenderer } from './renderers/DayViewRenderer.js';
23
23
 
24
24
  // Components
25
25
  export { ForceCalendar } from './components/ForceCalendar.js';
26
+ export { EventForm } from './components/EventForm.js';
@@ -2,6 +2,34 @@
2
2
  * StyleUtils - Styling utilities and theme management
3
3
  */
4
4
 
5
+ /**
6
+ * Named theme presets applied via the <forcecal-main theme="..."> attribute.
7
+ * Each maps design tokens to a platform's visual language.
8
+ */
9
+ export const THEME_PRESETS = {
10
+ // Salesforce Lightning Design System: looks native on the platform
11
+ slds: {
12
+ '--fc-primary-color': '#0176d3',
13
+ '--fc-primary-hover': '#014486',
14
+ '--fc-primary-light': '#eef4ff',
15
+ '--fc-accent-color': '#0b5cab',
16
+ '--fc-text-color': '#181818',
17
+ '--fc-text-secondary': '#706e6b',
18
+ '--fc-text-light': '#939393',
19
+ '--fc-border-color': '#e5e5e5',
20
+ '--fc-border-color-hover': '#c9c9c9',
21
+ '--fc-background': '#ffffff',
22
+ '--fc-background-alt': '#f3f3f3',
23
+ '--fc-background-hover': '#f3f3f3',
24
+ '--fc-background-active': '#d8e6fe',
25
+ '--fc-danger-color': '#ea001e',
26
+ '--fc-success-color': '#2e844a',
27
+ '--fc-border-radius': '0.25rem',
28
+ '--fc-border-radius-sm': '0.125rem',
29
+ '--fc-font-family': "'Salesforce Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
30
+ }
31
+ };
32
+
5
33
  export class StyleUtils {
6
34
  /**
7
35
  * Default theme colors