@chaaanito/event-resource-calendar 1.0.2 → 1.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 +64 -25
- package/jsconfig.json +12 -0
- package/package.json +9 -2
- package/src/event-resource.types.js +98 -0
- package/src/index.js +162 -436
- package/src/styles.css +171 -0
package/README.md
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
# EventResource
|
|
2
2
|
|
|
3
|
-
A lightweight, high-performance vanilla JavaScript resource calendar library. Features O(1) internal event mapping, extensible rich HTML layout renderers, holiday detection, and scroll freezing.
|
|
3
|
+
A lightweight, high-performance vanilla JavaScript resource calendar library. Features skeleton-first asynchronous rendering, O(1) internal event mapping, extensible rich HTML layout renderers, holiday detection, and scroll freezing.
|
|
4
4
|
|
|
5
5
|
# Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install event-resource-calendar
|
|
8
|
+
npm install @chaaanito/event-resource-calendar
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
# Quick Start
|
|
12
12
|
|
|
13
13
|
```javascript
|
|
14
|
-
import EventResource from "event-resource-calendar";
|
|
14
|
+
import EventResource from "@chaaanito/event-resource-calendar";
|
|
15
|
+
import "@chaaanito/event-resource-calendar/style.css"; // Required for grid structural styling
|
|
15
16
|
|
|
16
17
|
const calendar = new EventResource({
|
|
17
18
|
container: "#calendar-root",
|
|
@@ -50,27 +51,32 @@ Pass these properties into the `EventResource` constructor to customize your cal
|
|
|
50
51
|
|
|
51
52
|
| Property | Type | Default | Description |
|
|
52
53
|
| :---------------- | :------------- | :----------- | :-------------------------------------------------------------------- |
|
|
53
|
-
| **container** | `string\|Node` | _Required_ | CSS selector or DOM pointer mount node.
|
|
54
|
+
| **container** | `string\|Node` | _Required_ | CSS selector or explicit DOM pointer mount node. |
|
|
54
55
|
| **rooms** | `Array` | `[]` | Master source list establishing rows along the vertical plane. |
|
|
55
56
|
| **timeSlots** | `Array` | `[]` | Master source list defining columns mapped along the horizontal path. |
|
|
56
57
|
| **initialEvents** | `Array` | `[]` | In-memory event array populating coordinates on load. |
|
|
57
58
|
| **holidays** | `Array` | `[]` | Configuration identifying structural exceptions and global days. |
|
|
58
59
|
| **customButtons** | `Array` | `[]` | Extensible collections rendering specialized tool structures. |
|
|
59
|
-
| **showControls** | `boolean` | `false` | Visibility of structural management toolbars.
|
|
60
|
-
| **stickyHeaders** | `boolean` | `true` | Toggles CSS sticky double-axis tracking logic.
|
|
61
|
-
| **defaultView** | `string` | `'daily'` | Default presentation layout. Must be 'daily' or 'weekly'.
|
|
60
|
+
| **showControls** | `boolean` | `false` | Visibility of structural management toolbars (Datepicker, arrows). |
|
|
61
|
+
| **stickyHeaders** | `boolean` | `true` | Toggles CSS sticky double-axis tracking logic across layout headers. |
|
|
62
|
+
| **defaultView** | `string` | `'daily'` | Default presentation layout mode. Must be 'daily' or 'weekly'. |
|
|
62
63
|
| **defaultDate** | `Date\|string` | `new Date()` | Frame configuration locking starting lifecycle boundaries. |
|
|
63
64
|
|
|
64
65
|
### Callbacks & Renderers
|
|
65
66
|
|
|
66
|
-
| Property | Type | Description
|
|
67
|
-
| :----------------------- | :--------- |
|
|
68
|
-
| **onCellClick** | `Function` | Callback capturing clicks targeting empty coordinates.
|
|
69
|
-
| **onEventClick** | `Function` | Callback targeting allocated calendar card coordinates.
|
|
70
|
-
| **fetchEvents** | `Function` | Async method
|
|
71
|
-
| **
|
|
72
|
-
| **
|
|
73
|
-
| **
|
|
67
|
+
| Property | Type | Description |
|
|
68
|
+
| :----------------------- | :--------- | :--------------------------------------------------------------------------- |
|
|
69
|
+
| **onCellClick** | `Function` | Callback capturing clicks targeting empty coordinates. |
|
|
70
|
+
| **onEventClick** | `Function` | Callback targeting allocated calendar card coordinates. |
|
|
71
|
+
| **fetchEvents** | `Function` | Async method resolving to an array of `CalendarEvent` objects. |
|
|
72
|
+
| **fetchRooms** | `Function` | Async method resolving to an array of `CalendarRoom` objects dynamically. |
|
|
73
|
+
| **fetchTimeSlots** | `Function` | Async method resolving to an array of `TimeSlot` objects dynamically. |
|
|
74
|
+
| **fetchHolidays** | `Function` | Async method resolving to an array of `CalendarHoliday` objects dynamically. |
|
|
75
|
+
| **renderRoomHeader** | `Function` | HTML generator returning structural formatting strings for row slots. |
|
|
76
|
+
| **renderTimeSlotHeader** | `Function` | HTML generator returning structural formatting strings for column slots. |
|
|
77
|
+
| **renderEvent** | `Function` | HTML generator returning custom markup for individual event cards. |
|
|
78
|
+
|
|
79
|
+
---
|
|
74
80
|
|
|
75
81
|
## Data Models
|
|
76
82
|
|
|
@@ -92,7 +98,7 @@ Defines a timeline column structure partitioning the matrix grid workspace.
|
|
|
92
98
|
| :-------- | :--------------- | :---------------------------------------------------------- |
|
|
93
99
|
| **id** | `string\|number` | **Required.** Unique identifier for the chronological slot. |
|
|
94
100
|
| **label** | `string` | **Required.** Fallback timeline display text. |
|
|
95
|
-
| **[key]** | `any` | Optional
|
|
101
|
+
| **[key]** | `any` | Optional extensible properties (e.g., isLunchHour). |
|
|
96
102
|
|
|
97
103
|
### CalendarEvent
|
|
98
104
|
|
|
@@ -105,28 +111,61 @@ Represents an allocated timeline event mapped directly into a specific intersect
|
|
|
105
111
|
| **timeId** | `string\|number` | **Required.** Foreign key binding the item to a valid TimeSlot ID. |
|
|
106
112
|
| **title** | `string` | **Required.** Plain-text title injected inside the card element. |
|
|
107
113
|
| **color** | `string` | Optional CSS color value for the background card. Default: `#3b82f6`. |
|
|
108
|
-
| **[key]** | `any` | Optional custom meta data attributes.
|
|
114
|
+
| **[key]** | `any` | Optional custom meta data attributes parsed down to click payloads. |
|
|
115
|
+
|
|
116
|
+
### CalendarHoliday
|
|
117
|
+
|
|
118
|
+
Maps specific dates to holiday statuses, shifting backgrounds and appending context data.
|
|
119
|
+
|
|
120
|
+
| Property | Type | Description |
|
|
121
|
+
| :-------- | :--------------------- | :--------------------------------------------------------------------- |
|
|
122
|
+
| **date** | `string\|Date\|number` | **Required.** Parsable temporal timestamp mapping the milestone. |
|
|
123
|
+
| **name** | `string` | **Required.** Human-readable label injected into global notice badges. |
|
|
124
|
+
| **[key]** | `any` | Optional open-ended customer specific holiday data. |
|
|
125
|
+
|
|
126
|
+
---
|
|
109
127
|
|
|
110
128
|
## Interaction Payloads
|
|
111
129
|
|
|
112
|
-
When interacting with the grid, the library fires callbacks with contextual payloads.
|
|
130
|
+
When interacting with the grid, the library fires callbacks with comprehensive contextual payloads.
|
|
113
131
|
|
|
114
132
|
### ClickContextPayload (Empty Cell Click)
|
|
115
133
|
|
|
134
|
+
Shared universally across grid click response lifecycles.
|
|
135
|
+
|
|
116
136
|
| Property | Type | Description |
|
|
117
137
|
| :---------- | :------------- | :----------------------------------------------------------------- |
|
|
118
138
|
| **date** | `Date` | Chronological state baseline actively mounted inside the viewport. |
|
|
119
|
-
| **view** | `string` | Current structural mode index ('daily' or 'weekly').
|
|
120
|
-
| **holiday** | `Object\|null` | Associated holiday data object if applicable.
|
|
121
|
-
| **row** | `Object` | Track metadata coordinates (`index` and `data`).
|
|
122
|
-
| **col** | `Object` | Timeline metadata coordinates (`index` and `data`).
|
|
139
|
+
| **view** | `string` | Current structural mode index configuration ('daily' or 'weekly'). |
|
|
140
|
+
| **holiday** | `Object\|null` | Associated holiday data object if applicable to current date. |
|
|
141
|
+
| **row** | `Object` | Track metadata coordinates (`index` and `data` objects). |
|
|
142
|
+
| **col** | `Object` | Timeline metadata coordinates (`index` and `data` objects). |
|
|
123
143
|
| **cell** | `Object` | Target cell contents (`roomId`, `timeId`, and `events` array). |
|
|
124
144
|
|
|
125
145
|
### EventClickPayload (Event Card Click)
|
|
126
146
|
|
|
127
|
-
Inherits
|
|
147
|
+
Inherits all properties from `ClickContextPayload` and adds:
|
|
128
148
|
|
|
129
149
|
| Property | Type | Description |
|
|
130
150
|
| :-------------- | :----------- | :-------------------------------------------------------------------------- |
|
|
131
|
-
| **event** | `Object` | **Required.** The unique target event parameters
|
|
132
|
-
| **nativeEvent** | `MouseEvent` | **Required.** Raw browser click interaction data.
|
|
151
|
+
| **event** | `Object` | **Required.** The explicit, unique target event parameters clicked. |
|
|
152
|
+
| **nativeEvent** | `MouseEvent` | **Required.** Raw browser click interaction data used for element tracking. |
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Public API Methods
|
|
157
|
+
|
|
158
|
+
Once instantiated, the calendar instance exposes methods to control the grid programmatically.
|
|
159
|
+
|
|
160
|
+
| Method | Description |
|
|
161
|
+
| :------------------------- | :---------------------------------------------------------------------------------------------- |
|
|
162
|
+
| **`addEvent(event)`** | Injects a new event and performs an isolated, high-speed DOM append without layout thrashing. |
|
|
163
|
+
| **`removeEvent(eventId)`** | Purges a specific event by ID from memory and removes it from the UI instantly. |
|
|
164
|
+
| **`clearAllEvents()`** | Wipes all events from the board while preserving the layout skeleton rules. |
|
|
165
|
+
| **`setDate(newDate)`** | Jumps the calendar to a specific date. Automatically triggers `fetch` hooks if configured. |
|
|
166
|
+
| **`setView(newView)`** | Mutates the layout granularity between daily and weekly modes. |
|
|
167
|
+
| **`Maps(direction)`** | Steps the timeline forward or backward based on the current view multiplier. |
|
|
168
|
+
| **`forceRender()`** | Triggers a manual full data replenishment cycle, drawing the skeleton and resolving all asyncs. |
|
|
169
|
+
| **`destroy()`** | Unmounts the DOM, clears memory caches, and drops listener closures to prevent leaks. |
|
|
170
|
+
|
|
171
|
+
---
|
package/jsconfig.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaaanito/event-resource-calendar",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/chaaanito/event-resource-calendar.git"
|
|
@@ -11,7 +11,14 @@
|
|
|
11
11
|
"homepage": "https://github.com/chaaanito/event-resource-calendar#readme",
|
|
12
12
|
"type": "module",
|
|
13
13
|
"main": "src/index.js",
|
|
14
|
+
"types": "dist/types/index.d.ts",
|
|
15
|
+
"style": "src/styles.css",
|
|
14
16
|
"exports": {
|
|
15
|
-
".":
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/types/index.d.ts",
|
|
19
|
+
"import": "./src/index.js",
|
|
20
|
+
"default": "./src/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./style.css": "./src/styles.css"
|
|
16
23
|
}
|
|
17
24
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} CalendarRoom
|
|
3
|
+
* @description Defines a resource row within the calendar matrix grid layout.
|
|
4
|
+
* @property {string|number} id - REQUIRED: Unique identifier for the room or resource. Must be absolutely unique across all room rows.
|
|
5
|
+
* @property {string} name - REQUIRED: Fallback display title of the room/resource used if `renderRoomHeader` is omitted.
|
|
6
|
+
* @property {*} [key: string] - OPTIONAL: Any additional open-ended properties used for custom filtering or rich rendering templates.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} TimeSlot
|
|
11
|
+
* @description Defines a timeline column structure partitioning the matrix grid workspace.
|
|
12
|
+
* @property {string|number} id - REQUIRED: Unique identifier for the chronological slot. Must be unique across all columns.
|
|
13
|
+
* @property {string} label - REQUIRED: Fallback timeline display text used if `renderTimeSlotHeader` is omitted.
|
|
14
|
+
* @property {*} [key: string] - OPTIONAL: Any additional extensible properties used for custom filtering or rich rendering templates.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} CalendarEvent
|
|
19
|
+
* @description Represents an allocated timeline event mapped directly into a specific intersection cell.
|
|
20
|
+
* @property {string|number} id - REQUIRED: Unique identifier for the scheduled event element.
|
|
21
|
+
* @property {string|number} roomId - REQUIRED: Relational foreign key binding the item to a valid {@link CalendarRoom.id}.
|
|
22
|
+
* @property {string|number} timeId - REQUIRED: Relational foreign key binding the item to a valid {@link TimeSlot.id}.
|
|
23
|
+
* @property {string} title - REQUIRED: Plain-text title injected inside the DOM node element card.
|
|
24
|
+
* @property {string} [color='#3b82f6'] - OPTIONAL: Valid CSS color value (hex, rgb, hsl, keyword) for the background tracking card.
|
|
25
|
+
* @property {*} [key: string] - OPTIONAL: Custom meta data attributes parsed down to click event callback streams.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {Object} CalendarHoliday
|
|
30
|
+
* @description Maps specific dates to holiday statuses, shifting backgrounds and appending context data to interaction payloads.
|
|
31
|
+
* @property {string|Date|number} date - REQUIRED: Parsable temporal timestamp mapping the holiday milestone. Must be resolvable by `new Date()`.
|
|
32
|
+
* @property {string} name - REQUIRED: The human-readable label injected into global notice badges and cell descriptors.
|
|
33
|
+
* @property {*} [key: string] - OPTIONAL: Open-ended customer specific holiday data.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} CustomButton
|
|
38
|
+
* @description Injectable client control appended directly onto the right-hand toolbar grouping matrix.
|
|
39
|
+
* @property {string} label - REQUIRED: Text descriptor rendered inside the interactive control button element frame.
|
|
40
|
+
* @property {function(MouseEvent): void} onClick - REQUIRED: Action handler fired immediately upon client interactions.
|
|
41
|
+
* @property {string} [className] - OPTIONAL: Space-delimited functional CSS style modifiers for custom visual overrides.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {Object} ClickContextPayload
|
|
46
|
+
* @description Consolidated operational telemetry shared universally across grid click response lifecycles.
|
|
47
|
+
* @property {Date} date - Chronological state baseline actively mounted inside the viewport template frame.
|
|
48
|
+
* @property {'daily'|'weekly'} view - Current structural mode index configuration.
|
|
49
|
+
* @property {CalendarHoliday|null} holiday - Associated holiday data object if the current frame falls on a configured day milestone.
|
|
50
|
+
* @property {Object} row - Track metadata coordinates.
|
|
51
|
+
* @property {number} row.index - Vertical index array placement coordinate.
|
|
52
|
+
* @property {CalendarRoom} row.data - Complete root object data context passed down from initialization.
|
|
53
|
+
* @property {Object} col - Timeline metadata coordinates.
|
|
54
|
+
* @property {number} col.index - Horizontal layout coordinate tracking indexes.
|
|
55
|
+
* @property {TimeSlot} col.data - Complete root chronological object parameters.
|
|
56
|
+
* @property {Object} cell - Operational target cell contents.
|
|
57
|
+
* @property {string|number} cell.roomId - Unique cell row lookup index.
|
|
58
|
+
* @property {string|number} cell.timeId - Unique cell column chronological coordinate index.
|
|
59
|
+
* @property {CalendarEvent[]} cell.events - Contextual array containing all events currently occupying this grid location.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @typedef {Object} EventClickPayload
|
|
64
|
+
* @description Multi-layered payload shared exclusively with the `onEventClick` subscriber method.
|
|
65
|
+
* @extends ClickContextPayload
|
|
66
|
+
* @property {CalendarEvent} event - REQUIRED: The explicit, unique target event parameters bound to the clicked card element.
|
|
67
|
+
* @property {MouseEvent} nativeEvent - REQUIRED: Raw browser click interaction data used for analytical intercept positioning or element tracking.
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @typedef {Object} EventResourceOptions
|
|
72
|
+
* @description Input operational context map parsing standard parameters through class factories.
|
|
73
|
+
* @property {string|HTMLElement|Node} container - REQUIRED: CSS selector engine target or explicit DOM pointer mount node.
|
|
74
|
+
* @property {CalendarRoom[]} [rooms=[]] - OPTIONAL: Master source list establishing rows along the vertical plane matrix mapping layout.
|
|
75
|
+
* @property {TimeSlot[]} [timeSlots=[]] - OPTIONAL: Master source list defining columns mapped along the horizontal path lane.
|
|
76
|
+
* @property {CalendarEvent[]} [initialEvents=[]] - OPTIONAL: In-memory event array populating coordinates during setup initialization workflows.
|
|
77
|
+
* @property {CalendarHoliday[]} [holidays=[]] - OPTIONAL: Array configuration identifying structural exceptions and specialized global days.
|
|
78
|
+
* @property {CustomButton[]} [customButtons=[]] - OPTIONAL: Extensible collections rendering specialized tool structures within toolbars.
|
|
79
|
+
* @property {boolean} [showControls=false] - OPTIONAL: Flag managing initialization visibility of structural management toolbars.
|
|
80
|
+
* @property {boolean} [stickyHeaders=true] - OPTIONAL: Toggles CSS sticky double-axis tracking logic across layout headers on load.
|
|
81
|
+
* @property {'daily'|'weekly'} [defaultView='daily'] - OPTIONAL: Default presentation structure layout selection state. Must be 'daily' or 'weekly'.
|
|
82
|
+
* @property {Date|string|number} [defaultDate=new Date()] - OPTIONAL: Frame configuration locking starting lifecycle boundaries.
|
|
83
|
+
* @property {function(ClickContextPayload): void} [onCellClick] - OPTIONAL: Interaction callback capturing clicks targeting empty coordinates.
|
|
84
|
+
* @property {function(EventClickPayload): void} [onEventClick] - OPTIONAL: Interaction callback targeting allocated calendar card coordinates.
|
|
85
|
+
* @property {function(Date, 'daily'|'weekly'): Promise<CalendarEvent[]>} [fetchEvents] - OPTIONAL: Data fetching intercept. Async method returning structural item sets.
|
|
86
|
+
* @property {function(CalendarRoom): string} [renderRoomHeader] - OPTIONAL: HTML generator intercept returning structural formatting strings for row slots.
|
|
87
|
+
* @property {function(TimeSlot): string} [renderTimeSlotHeader] - OPTIONAL: HTML generator intercept returning structural formatting strings for column slots.
|
|
88
|
+
* @property {function(CalendarEvent): string} [renderEvent] - OPTIONAL: HTML generator intercept returning custom markup for individual event cards. Overrides default title text.
|
|
89
|
+
* @property {function(Date, 'daily'|'weekly'): Promise<CalendarEvent[]>} [fetchEvents] - OPTIONAL: Async method returning events.
|
|
90
|
+
* @property {function(Date, 'daily'|'weekly'): Promise<CalendarRoom[]>} [fetchRooms] - OPTIONAL: Async method returning rooms dynamically.
|
|
91
|
+
* @property {function(Date, 'daily'|'weekly'): Promise<TimeSlot[]>} [fetchTimeSlots] - OPTIONAL: Async method returning timeline columns dynamically.
|
|
92
|
+
* @property {function(Date, 'daily'|'weekly'): Promise<CalendarHoliday[]>} [fetchHolidays] - OPTIONAL: Async method returning holidays dynamically.
|
|
93
|
+
* * @property {function(CalendarRoom): string} [renderRoomHeader] - OPTIONAL: HTML generator intercept.
|
|
94
|
+
* @property {function(TimeSlot): string} [renderTimeSlotHeader] - OPTIONAL: HTML generator intercept.
|
|
95
|
+
* @property {function(CalendarEvent): string} [renderEvent] - OPTIONAL: HTML generator intercept.
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
export {};
|
package/src/index.js
CHANGED
|
@@ -1,114 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @typedef {
|
|
3
|
-
* @
|
|
4
|
-
* @
|
|
5
|
-
* @
|
|
6
|
-
* @
|
|
7
|
-
* @
|
|
8
|
-
* // Example of a valid CalendarRoom object
|
|
9
|
-
* { id: 'room-101', name: 'Conference Room A', capacity: 12, hasProjector: true }
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @typedef {Object} TimeSlot
|
|
14
|
-
* @description Defines a timeline column structure partitioning the matrix grid workspace.
|
|
15
|
-
* @property {string|number} id - REQUIRED: Unique identifier for the chronological slot. Must be unique across all columns.
|
|
16
|
-
* @property {string} label - REQUIRED: Fallback timeline display text used if `renderTimeSlotHeader` is omitted.
|
|
17
|
-
* @property {*} [key: string] - OPTIONAL: Any additional extensible properties used for custom filtering or rich rendering templates.
|
|
18
|
-
* @example
|
|
19
|
-
* // Example of a valid TimeSlot object
|
|
20
|
-
* { id: 'slot-0900', label: '09:00 AM', isLunchHour: false }
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @typedef {Object} CalendarEvent
|
|
25
|
-
* @description Represents an allocated timeline event mapped directly into a specific intersection cell.
|
|
26
|
-
* @property {string|number} id - REQUIRED: Unique identifier for the scheduled event element.
|
|
27
|
-
* @property {string|number} roomId - REQUIRED: Relational foreign key binding the item to a valid {@link CalendarRoom.id}.
|
|
28
|
-
* @property {string|number} timeId - REQUIRED: Relational foreign key binding the item to a valid {@link TimeSlot.id}.
|
|
29
|
-
* @property {string} title - REQUIRED: Plain-text title injected inside the DOM node element card.
|
|
30
|
-
* @property {string} [color='#3b82f6'] - OPTIONAL: Valid CSS color value (hex, rgb, hsl, keyword) for the background tracking card.
|
|
31
|
-
* @property {*} [key: string] - OPTIONAL: Custom meta data attributes parsed down to click event callback streams.
|
|
32
|
-
* @example
|
|
33
|
-
* // Example of a valid CalendarEvent object
|
|
34
|
-
* { id: 'evt-1', roomId: 'room-101', timeId: 'slot-0900', title: 'Q3 Planning Sync', color: '#10b981', attendees: 5 }
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* @typedef {Object} CalendarHoliday
|
|
39
|
-
* @description Maps specific dates to holiday statuses, shifting backgrounds and appending context data to interaction payloads.
|
|
40
|
-
* @property {string|Date|number} date - REQUIRED: Parsable temporal timestamp mapping the holiday milestone. Must be resolvable by `new Date()`.
|
|
41
|
-
* @property {string} name - REQUIRED: The human-readable label injected into global notice badges and cell descriptors.
|
|
42
|
-
* @property {*} [key: string] - OPTIONAL: Open-ended customer specific holiday data.
|
|
43
|
-
* @example
|
|
44
|
-
* // Example of a valid CalendarHoliday object
|
|
45
|
-
* { date: '2026-12-25', name: 'Christmas Day', isCompanyPaid: true }
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* @typedef {Object} CustomButton
|
|
50
|
-
* @description Injectable client control appended directly onto the right-hand toolbar grouping matrix.
|
|
51
|
-
* @property {string} label - REQUIRED: Text descriptor rendered inside the interactive control button element frame.
|
|
52
|
-
* @property {function(MouseEvent): void} onClick - REQUIRED: Action handler fired immediately upon client interactions.
|
|
53
|
-
* @property {string} [className] - OPTIONAL: Space-delimited functional CSS style modifiers for custom visual overrides.
|
|
54
|
-
* @example
|
|
55
|
-
* // Example of a valid CustomButton object
|
|
56
|
-
* { label: 'Export PDF', className: 'bg-red-500 text-white', onClick: (e) => console.log('Exporting...', e) }
|
|
57
|
-
*/
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* @typedef {Object} ClickContextPayload
|
|
61
|
-
* @description Consolidated operational telemetry shared universally across grid click response lifecycles.
|
|
62
|
-
* @property {Date} date - Chronological state baseline actively mounted inside the viewport template frame.
|
|
63
|
-
* @property {'daily'|'weekly'} view - Current structural mode index configuration.
|
|
64
|
-
* @property {CalendarHoliday|null} holiday - Associated holiday data object if the current frame falls on a configured day milestone.
|
|
65
|
-
* @property {Object} row - Track metadata coordinates.
|
|
66
|
-
* @property {number} row.index - Vertical index array placement coordinate.
|
|
67
|
-
* @property {CalendarRoom} row.data - Complete root object data context passed down from initialization.
|
|
68
|
-
* @property {Object} col - Timeline metadata coordinates.
|
|
69
|
-
* @property {number} col.index - Horizontal layout coordinate tracking indexes.
|
|
70
|
-
* @property {TimeSlot} col.data - Complete root chronological object parameters.
|
|
71
|
-
* @property {Object} cell - Operational target cell contents.
|
|
72
|
-
* @property {string|number} cell.roomId - Unique cell row lookup index.
|
|
73
|
-
* @property {string|number} cell.timeId - Unique cell column chronological coordinate index.
|
|
74
|
-
* @property {CalendarEvent[]} cell.events - Contextual array containing all events currently occupying this grid location.
|
|
75
|
-
*/
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* @typedef {Object} EventClickPayload
|
|
79
|
-
* @description Multi-layered payload shared exclusively with the `onEventClick` subscriber method.
|
|
80
|
-
* @extends ClickContextPayload
|
|
81
|
-
* @property {CalendarEvent} event - REQUIRED: The explicit, unique target event parameters bound to the clicked card element.
|
|
82
|
-
* @property {MouseEvent} nativeEvent - REQUIRED: Raw browser click interaction data used for analytical intercept positioning or element tracking.
|
|
83
|
-
*/
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* @typedef {Object} EventResourceOptions
|
|
87
|
-
* @description Input operational context map parsing standard parameters through class factories.
|
|
88
|
-
* @property {string|HTMLElement|Node} container - REQUIRED: CSS selector engine target or explicit DOM pointer mount node.
|
|
89
|
-
* @property {CalendarRoom[]} [rooms=[]] - OPTIONAL: Master source list establishing rows along the vertical plane matrix mapping layout.
|
|
90
|
-
* @property {TimeSlot[]} [timeSlots=[]] - OPTIONAL: Master source list defining columns mapped along the horizontal path lane.
|
|
91
|
-
* @property {CalendarEvent[]} [initialEvents=[]] - OPTIONAL: In-memory event array populating coordinates during setup initialization workflows.
|
|
92
|
-
* @property {CalendarHoliday[]} [holidays=[]] - OPTIONAL: Array configuration identifying structural exceptions and specialized global days.
|
|
93
|
-
* @property {CustomButton[]} [customButtons=[]] - OPTIONAL: Extensible collections rendering specialized tool structures within toolbars.
|
|
94
|
-
* @property {boolean} [showControls=false] - OPTIONAL: Flag managing initialization visibility of structural management toolbars.
|
|
95
|
-
* @property {boolean} [stickyHeaders=true] - OPTIONAL: Toggles CSS sticky double-axis tracking logic across layout headers on load.
|
|
96
|
-
* @property {'daily'|'weekly'} [defaultView='daily'] - OPTIONAL: Default presentation structure layout selection state. Must be 'daily' or 'weekly'.
|
|
97
|
-
* @property {Date|string|number} [defaultDate=new Date()] - OPTIONAL: Frame configuration locking starting lifecycle boundaries.
|
|
98
|
-
* @property {function(ClickContextPayload): void} [onCellClick] - OPTIONAL: Interaction callback capturing clicks targeting empty coordinates.
|
|
99
|
-
* @property {function(EventClickPayload): void} [onEventClick] - OPTIONAL: Interaction callback targeting allocated calendar card coordinates.
|
|
100
|
-
* @property {function(Date, 'daily'|'weekly'): Promise<CalendarEvent[]>} [fetchEvents] - OPTIONAL: Data fetching intercept. Async method returning structural item sets.
|
|
101
|
-
* @property {function(CalendarRoom): string} [renderRoomHeader] - OPTIONAL: HTML generator intercept returning structural formatting strings for row slots.
|
|
102
|
-
* @property {function(TimeSlot): string} [renderTimeSlotHeader] - OPTIONAL: HTML generator intercept returning structural formatting strings for column slots.
|
|
103
|
-
* @property {function(CalendarEvent): string} [renderEvent] - OPTIONAL: HTML generator intercept returning custom markup for individual event cards. Overrides default title text.
|
|
104
|
-
* @example
|
|
2
|
+
* @typedef {import('./event-resource.types.js').EventResourceOptions} EventResourceOptions
|
|
3
|
+
* @typedef {import('./event-resource.types.js').CalendarRoom} CalendarRoom
|
|
4
|
+
* @typedef {import('./event-resource.types.js').TimeSlot} TimeSlot
|
|
5
|
+
* @typedef {import('./event-resource.types.js').CalendarEvent} CalendarEvent
|
|
6
|
+
* @typedef {import('./event-resource.types.js').CalendarHoliday} CalendarHoliday
|
|
7
|
+
* @typedef {import('./event-resource.types.js').CustomButton} CustomButton
|
|
105
8
|
*/
|
|
106
9
|
|
|
107
10
|
/**
|
|
108
11
|
* EventResource
|
|
109
12
|
* A lightweight, high-performance vanilla JavaScript resource calendar library.
|
|
110
13
|
* Features O(1) internal event mapping, extensible rich HTML layout renderers, holiday detection, and scroll freezing.
|
|
111
|
-
* @class
|
|
112
14
|
*/
|
|
113
15
|
export default class EventResource {
|
|
114
16
|
/**
|
|
@@ -124,7 +26,7 @@ export default class EventResource {
|
|
|
124
26
|
* * // 2. Structural UI Toggles
|
|
125
27
|
* showControls: true,
|
|
126
28
|
* stickyHeaders: true,
|
|
127
|
-
* * // 3. Grid Definitions (
|
|
29
|
+
* * // 3. Grid Definitions (Static Fallbacks)
|
|
128
30
|
* rooms: [{ id: 'r1', name: 'Studio A', capacity: 10 }],
|
|
129
31
|
* timeSlots: [{ id: 't1', label: '09:00 AM' }],
|
|
130
32
|
* holidays: [{ date: '2026-12-25', name: 'Christmas Day' }],
|
|
@@ -139,38 +41,40 @@ export default class EventResource {
|
|
|
139
41
|
* * // 5. Toolbar Extensions
|
|
140
42
|
* customButtons: [{
|
|
141
43
|
* label: 'Export PDF',
|
|
142
|
-
* className: 'bg-red-500 text-white
|
|
143
|
-
* onClick: (e) => console.log('
|
|
44
|
+
* className: 'bg-red-500 text-white',
|
|
45
|
+
* onClick: (e) => console.log('Exporting...', e)
|
|
144
46
|
* }],
|
|
145
47
|
* * // 6. Interaction Event Hooks
|
|
146
|
-
* onCellClick: (payload) =>
|
|
147
|
-
*
|
|
48
|
+
* onCellClick: (payload) => console.log('Empty slot clicked!'),
|
|
49
|
+
* onEventClick: (payload) => alert(`Clicked: ${payload.event.title}`),
|
|
50
|
+
* * // 7. Rich HTML Generation Intercepts
|
|
51
|
+
* renderRoomHeader: (room) => `<div>${room.name}</div>`,
|
|
52
|
+
* renderTimeSlotHeader: (slot) => `<div>${slot.label}</div>`,
|
|
53
|
+
* renderEvent: (event) => `<div>${event.title}</div>`,
|
|
54
|
+
* * // 8. Async Lifecycle Management (Draws skeleton first, then populates data)
|
|
55
|
+
* fetchRooms: async (date, view) => {
|
|
56
|
+
* const res = await fetch(`/api/rooms?date=${date.toISOString()}&view=${view}`);
|
|
57
|
+
* return await res.json();
|
|
148
58
|
* },
|
|
149
|
-
*
|
|
150
|
-
*
|
|
59
|
+
* fetchTimeSlots: async (date, view) => {
|
|
60
|
+
* const res = await fetch(`/api/timeslots?view=${view}`);
|
|
61
|
+
* return await res.json();
|
|
62
|
+
* },
|
|
63
|
+
* fetchHolidays: async (date, view) => {
|
|
64
|
+
* const res = await fetch(`/api/holidays?year=${date.getFullYear()}`);
|
|
65
|
+
* return await res.json();
|
|
151
66
|
* },
|
|
152
|
-
* * // 7. Rich HTML Generation Intercepts
|
|
153
|
-
* renderRoomHeader: (room) => `<div class="p-2 border-b"><h3>${room.name}</h3><small>Cap: ${room.capacity}</small></div>`,
|
|
154
|
-
* renderTimeSlotHeader: (slot) => `<div class="text-center font-bold text-gray-700">${slot.label}</div>`,
|
|
155
|
-
* * // 8. Async Lifecycle Management
|
|
156
67
|
* fetchEvents: async (date, view) => {
|
|
157
68
|
* const res = await fetch(`/api/events?date=${date.toISOString()}&view=${view}`);
|
|
158
69
|
* return await res.json();
|
|
159
70
|
* }
|
|
160
71
|
* });
|
|
161
|
-
* renderEvent: (event) => `
|
|
162
|
-
* <div class="flex flex-col gap-1 p-1">
|
|
163
|
-
* <span class="font-bold text-xs truncate">${event.title}</span>
|
|
164
|
-
* <span class="text-[10px] opacity-75">${event.roomName || 'TBD'}</span>
|
|
165
|
-
* </div>
|
|
166
|
-
* `
|
|
167
72
|
*/
|
|
168
73
|
constructor(options) {
|
|
169
74
|
let resolvedContainer = null;
|
|
170
75
|
|
|
171
76
|
if (typeof options.container === "string") {
|
|
172
77
|
resolvedContainer = document.querySelector(options.container);
|
|
173
|
-
|
|
174
78
|
if (!resolvedContainer) {
|
|
175
79
|
resolvedContainer =
|
|
176
80
|
document.getElementById(options.container) ||
|
|
@@ -190,93 +94,54 @@ export default class EventResource {
|
|
|
190
94
|
|
|
191
95
|
if (!resolvedContainer) {
|
|
192
96
|
throw new Error(
|
|
193
|
-
"EventResource: A valid container
|
|
97
|
+
"EventResource: A valid container is required and must exist in the DOM.",
|
|
194
98
|
);
|
|
195
99
|
}
|
|
196
100
|
|
|
197
|
-
/**
|
|
198
|
-
* @type {HTMLElement}
|
|
199
|
-
* @description The verified root DOM node where the calendar is mounted.
|
|
200
|
-
*/
|
|
201
101
|
this.container = resolvedContainer;
|
|
202
102
|
|
|
203
|
-
//
|
|
204
|
-
/** @type {CalendarRoom[]} */
|
|
103
|
+
// Data Options
|
|
205
104
|
this.rooms = options.rooms || [];
|
|
206
|
-
/** @type {TimeSlot[]} */
|
|
207
105
|
this.timeSlots = options.timeSlots || [];
|
|
208
|
-
/** @type {CalendarEvent[]} */
|
|
209
106
|
this.events = options.initialEvents || [];
|
|
210
|
-
/** @type {CalendarHoliday[]} */
|
|
211
107
|
this.holidays = options.holidays || [];
|
|
212
|
-
/** @type {CustomButton[]} */
|
|
213
108
|
this.customButtons = options.customButtons || [];
|
|
214
109
|
|
|
215
|
-
//
|
|
216
|
-
/** @type {boolean} */
|
|
110
|
+
// UI Controls & State
|
|
217
111
|
this.showControls = options.showControls || false;
|
|
218
|
-
/** @type {boolean} */
|
|
219
112
|
this.stickyHeaders = options.stickyHeaders !== false;
|
|
220
|
-
/** @type {'daily'|'weekly'} */
|
|
221
113
|
this.currentView = options.defaultView || "daily";
|
|
222
|
-
/** @type {Date} */
|
|
223
114
|
this.currentDate = options.defaultDate
|
|
224
115
|
? new Date(options.defaultDate)
|
|
225
116
|
: new Date();
|
|
226
|
-
/** @type {boolean} */
|
|
227
117
|
this.isFetching = false;
|
|
228
118
|
|
|
229
|
-
//
|
|
230
|
-
/** @type {function|null} */
|
|
119
|
+
// Callbacks & Async Fetchers
|
|
231
120
|
this.onCellClick = options.onCellClick || null;
|
|
232
|
-
/** @type {function|null} */
|
|
233
121
|
this.onEventClick = options.onEventClick || null;
|
|
234
|
-
|
|
122
|
+
|
|
235
123
|
this.fetchEvents = options.fetchEvents || null;
|
|
236
|
-
|
|
124
|
+
this.fetchRooms = options.fetchRooms || null;
|
|
125
|
+
this.fetchTimeSlots = options.fetchTimeSlots || null;
|
|
126
|
+
this.fetchHolidays = options.fetchHolidays || null;
|
|
127
|
+
|
|
237
128
|
this.renderRoomHeader = options.renderRoomHeader || null;
|
|
238
|
-
/** @type {function|null} */
|
|
239
129
|
this.renderTimeSlotHeader = options.renderTimeSlotHeader || null;
|
|
240
|
-
/** @type {function|null} */
|
|
241
130
|
this.renderEvent = options.renderEvent || null;
|
|
242
131
|
|
|
243
|
-
// 5. Initialization
|
|
244
|
-
/**
|
|
245
|
-
* @type {Map<string, CalendarEvent[]>}
|
|
246
|
-
* @description Internal O(1) lookup map isolating events into cell clusters based on `roomId-timeId` keys.
|
|
247
|
-
* @private
|
|
248
|
-
*/
|
|
249
132
|
this.eventsMap = new Map();
|
|
250
|
-
this._injectStyles();
|
|
251
133
|
this._buildEventsMap();
|
|
134
|
+
|
|
252
135
|
this.forceRender();
|
|
253
136
|
}
|
|
254
137
|
|
|
255
138
|
// --- State & Date Management ---
|
|
256
139
|
|
|
257
|
-
/**
|
|
258
|
-
* Transforms a multi-format date payload into a strictly normalized `YYYY-MM-DD` string for standardized comparison operations.
|
|
259
|
-
* @param {Date|string|number} dateObj - REQUIRED: The temporal object, ISO string, or epoch timestamp to normalize.
|
|
260
|
-
* @returns {string} The normalized local date string, formatted strictly as "YYYY-MM-DD".
|
|
261
|
-
* @private
|
|
262
|
-
* @example
|
|
263
|
-
* // Returns "2026-06-24"
|
|
264
|
-
* this._getNormalizedDateString(new Date('2026-06-24T12:00:00Z'));
|
|
265
|
-
*/
|
|
266
140
|
_getNormalizedDateString = (dateObj) => {
|
|
267
141
|
const d = new Date(dateObj);
|
|
268
142
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
269
143
|
};
|
|
270
144
|
|
|
271
|
-
/**
|
|
272
|
-
* Scans the internal `holidays` array to determine if the provided date target matches a configured milestone.
|
|
273
|
-
* @param {Date|string|number} dateObj - REQUIRED: The temporal target to evaluate against known holidays.
|
|
274
|
-
* @returns {CalendarHoliday|null} Returns the exact matched holiday configuration object, or `null` if no match occurs.
|
|
275
|
-
* @private
|
|
276
|
-
* @example
|
|
277
|
-
* const holiday = this._getHolidayForDate('2026-12-25');
|
|
278
|
-
* if (holiday) console.log(holiday.name); // Logs "Christmas"
|
|
279
|
-
*/
|
|
280
145
|
_getHolidayForDate = (dateObj) => {
|
|
281
146
|
const targetDate = this._getNormalizedDateString(dateObj);
|
|
282
147
|
return (
|
|
@@ -286,16 +151,10 @@ export default class EventResource {
|
|
|
286
151
|
);
|
|
287
152
|
};
|
|
288
153
|
|
|
289
|
-
/**
|
|
290
|
-
* Wipes and rebuilds the highly-optimized internal collision map. Groups flat event arrays into isolated index blocks.
|
|
291
|
-
* Executes dynamically behind the scenes prior to every grid rendering phase to ensure data consistency.
|
|
292
|
-
* @returns {void}
|
|
293
|
-
* @private
|
|
294
|
-
*/
|
|
295
154
|
_buildEventsMap = () => {
|
|
296
155
|
this.eventsMap.clear();
|
|
297
156
|
for (const ev of this.events) {
|
|
298
|
-
const key = `${ev.roomId}
|
|
157
|
+
const key = `${ev.roomId}::${ev.timeId}`;
|
|
299
158
|
if (!this.eventsMap.has(key)) {
|
|
300
159
|
this.eventsMap.set(key, []);
|
|
301
160
|
}
|
|
@@ -304,11 +163,10 @@ export default class EventResource {
|
|
|
304
163
|
};
|
|
305
164
|
|
|
306
165
|
/**
|
|
307
|
-
* Forces a chronological state shift, rewiring internal date parameters and firing synchronous
|
|
166
|
+
* Forces a chronological state shift, rewiring internal date parameters and firing synchronous/asynchronous re-render loops.
|
|
308
167
|
* @param {Date|string|number} newDate - REQUIRED: The new calendar baseline target date parameter.
|
|
309
168
|
* @returns {Promise<void>} Resolves automatically when the async data refetch and complete re-render lifecycle are complete.
|
|
310
169
|
* @example
|
|
311
|
-
* // Jump directly to Halloween 2026
|
|
312
170
|
* await calendar.setDate('2026-10-31');
|
|
313
171
|
*/
|
|
314
172
|
setDate = async (newDate) => {
|
|
@@ -321,7 +179,6 @@ export default class EventResource {
|
|
|
321
179
|
* @param {'daily'|'weekly'} newView - REQUIRED: The strict target structural mode selection string.
|
|
322
180
|
* @returns {Promise<void>} Resolves when the refetch, DOM teardown, and structural framework update complete.
|
|
323
181
|
* @example
|
|
324
|
-
* // Swap calendar into weekly perspective mode
|
|
325
182
|
* await calendar.setView('weekly');
|
|
326
183
|
*/
|
|
327
184
|
setView = async (newView) => {
|
|
@@ -331,12 +188,10 @@ export default class EventResource {
|
|
|
331
188
|
};
|
|
332
189
|
|
|
333
190
|
/**
|
|
334
|
-
* Calculates timeline vector offsets based on the
|
|
335
|
-
* Moves timeline by exactly 1 day for 'daily' views, or exactly 7 days for 'weekly' views.
|
|
191
|
+
* Calculates timeline vector offsets based on the active view mode, steps the internal date parameter, and triggers reconciliations.
|
|
336
192
|
* @param {'prev'|'next'} direction - REQUIRED: The chronological vector direction keyword.
|
|
337
193
|
* @returns {Promise<void>} Resolves upon successful navigation and canvas redraw.
|
|
338
194
|
* @example
|
|
339
|
-
* // Step forward in time based on current view step sizes
|
|
340
195
|
* await calendar.navigate('next');
|
|
341
196
|
*/
|
|
342
197
|
navigate = async (direction) => {
|
|
@@ -352,74 +207,85 @@ export default class EventResource {
|
|
|
352
207
|
// --- Public API ---
|
|
353
208
|
|
|
354
209
|
/**
|
|
355
|
-
* Injects a raw configuration event into the application data state. Recompiles the collision map and forces a
|
|
210
|
+
* Injects a raw configuration event into the application data state. Recompiles the collision map and forces a high-speed DOM update.
|
|
356
211
|
* @param {CalendarEvent} newEvent - REQUIRED: A valid object data map matching configuration structural specs exactly.
|
|
357
212
|
* @returns {void}
|
|
358
213
|
* @example
|
|
359
|
-
*
|
|
360
|
-
* calendar.addEvent({
|
|
361
|
-
* id: 'evt-999',
|
|
362
|
-
* roomId: 'room-101',
|
|
363
|
-
* timeId: 'slot-0900',
|
|
364
|
-
* title: 'Ad-hoc Emergency Sync',
|
|
365
|
-
* color: '#ef4444'
|
|
366
|
-
* });
|
|
214
|
+
* calendar.addEvent({ id: 'evt-999', roomId: 'r1', timeId: 't1', title: 'Emergency Sync' });
|
|
367
215
|
*/
|
|
368
216
|
addEvent = (newEvent) => {
|
|
369
217
|
this.events.push(newEvent);
|
|
370
218
|
this._buildEventsMap();
|
|
371
|
-
this.
|
|
219
|
+
this._renderEvents();
|
|
372
220
|
};
|
|
373
221
|
|
|
374
222
|
/**
|
|
375
|
-
* Executes a hard delete across the internal event arrays based strictly on a uniquely matched id reference string.
|
|
376
|
-
* @param {string|number} eventId - REQUIRED: The exact, unique reference key index matching the target
|
|
223
|
+
* Executes a hard delete across the internal event arrays based strictly on a uniquely matched id reference string.
|
|
224
|
+
* @param {string|number} eventId - REQUIRED: The exact, unique reference key index matching the target CalendarEvent.id.
|
|
377
225
|
* @returns {void}
|
|
378
226
|
* @example
|
|
379
|
-
* // Purge a specific event from the DOM and memory
|
|
380
227
|
* calendar.removeEvent('evt-999');
|
|
381
228
|
*/
|
|
382
229
|
removeEvent = (eventId) => {
|
|
383
230
|
this.events = this.events.filter((e) => e.id !== eventId);
|
|
384
231
|
this._buildEventsMap();
|
|
385
|
-
this.
|
|
232
|
+
this._renderEvents();
|
|
386
233
|
};
|
|
387
234
|
|
|
388
235
|
/**
|
|
389
236
|
* Wipes out all transient operational event records cached in client memory structures while preserving column rules and grid row configurations.
|
|
390
|
-
* Excellent for resetting layouts between deep navigational transitions without destroying the core class instance.
|
|
391
237
|
* @returns {void}
|
|
392
238
|
* @example
|
|
393
|
-
* // Clean the board completely
|
|
394
239
|
* calendar.clearAllEvents();
|
|
395
240
|
*/
|
|
396
241
|
clearAllEvents = () => {
|
|
397
242
|
this.events = [];
|
|
398
243
|
this.eventsMap.clear();
|
|
399
|
-
this.
|
|
244
|
+
this._renderEvents();
|
|
400
245
|
};
|
|
401
246
|
|
|
402
247
|
/**
|
|
403
|
-
* Triggers a comprehensive data replenishment cycle. Evaluates external
|
|
404
|
-
* Implements internal `isFetching` lock mechanisms to protect against asynchronous data race conditions or parallel API execution.
|
|
248
|
+
* Triggers a comprehensive data replenishment cycle. Evaluates external fetch connectors, updates map caches, and rebuilds the visual DOM.
|
|
405
249
|
* @returns {Promise<void>} Resolves once all external data resolves and the DOM reconciliation concludes successfully.
|
|
406
250
|
* @example
|
|
407
|
-
* // Force a data refresh from the server
|
|
408
251
|
* await calendar.forceRender();
|
|
409
252
|
*/
|
|
410
253
|
forceRender = async () => {
|
|
411
254
|
if (this.isFetching) return;
|
|
412
255
|
|
|
413
|
-
|
|
256
|
+
this.render();
|
|
257
|
+
|
|
258
|
+
const hasAsyncSources =
|
|
259
|
+
typeof this.fetchEvents === "function" ||
|
|
260
|
+
typeof this.fetchRooms === "function" ||
|
|
261
|
+
typeof this.fetchTimeSlots === "function" ||
|
|
262
|
+
typeof this.fetchHolidays === "function";
|
|
263
|
+
|
|
264
|
+
if (hasAsyncSources) {
|
|
414
265
|
this.isFetching = true;
|
|
415
266
|
try {
|
|
416
|
-
const freshEvents
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
267
|
+
const [freshEvents, freshRooms, freshTimeSlots, freshHolidays] =
|
|
268
|
+
await Promise.all([
|
|
269
|
+
typeof this.fetchEvents === "function"
|
|
270
|
+
? this.fetchEvents(this.currentDate, this.currentView)
|
|
271
|
+
: Promise.resolve(this.events),
|
|
272
|
+
typeof this.fetchRooms === "function"
|
|
273
|
+
? this.fetchRooms(this.currentDate, this.currentView)
|
|
274
|
+
: Promise.resolve(this.rooms),
|
|
275
|
+
typeof this.fetchTimeSlots === "function"
|
|
276
|
+
? this.fetchTimeSlots(this.currentDate, this.currentView)
|
|
277
|
+
: Promise.resolve(this.timeSlots),
|
|
278
|
+
typeof this.fetchHolidays === "function"
|
|
279
|
+
? this.fetchHolidays(this.currentDate, this.currentView)
|
|
280
|
+
: Promise.resolve(this.holidays),
|
|
281
|
+
]);
|
|
282
|
+
|
|
420
283
|
this.events = freshEvents || [];
|
|
284
|
+
this.rooms = freshRooms || [];
|
|
285
|
+
this.timeSlots = freshTimeSlots || [];
|
|
286
|
+
this.holidays = freshHolidays || [];
|
|
421
287
|
} catch (error) {
|
|
422
|
-
console.error("EventResource: Failed to refetch
|
|
288
|
+
console.error("EventResource: Failed to refetch calendar data.", error);
|
|
423
289
|
} finally {
|
|
424
290
|
this.isFetching = false;
|
|
425
291
|
}
|
|
@@ -431,10 +297,9 @@ export default class EventResource {
|
|
|
431
297
|
|
|
432
298
|
/**
|
|
433
299
|
* Executes irreversible teardown routines protecting application host memory state cycles.
|
|
434
|
-
* Wipes parameters, drops listener closures, clears map caches, and securely unmounts UI sub-trees
|
|
300
|
+
* Wipes parameters, drops listener closures, clears map caches, and securely unmounts UI sub-trees.
|
|
435
301
|
* @returns {void}
|
|
436
302
|
* @example
|
|
437
|
-
* // Unmount calendar gracefully
|
|
438
303
|
* calendar.destroy();
|
|
439
304
|
* calendar = null;
|
|
440
305
|
*/
|
|
@@ -448,6 +313,9 @@ export default class EventResource {
|
|
|
448
313
|
this.onCellClick = null;
|
|
449
314
|
this.onEventClick = null;
|
|
450
315
|
this.fetchEvents = null;
|
|
316
|
+
this.fetchRooms = null;
|
|
317
|
+
this.fetchTimeSlots = null;
|
|
318
|
+
this.fetchHolidays = null;
|
|
451
319
|
this.renderRoomHeader = null;
|
|
452
320
|
this.renderTimeSlotHeader = null;
|
|
453
321
|
|
|
@@ -461,20 +329,10 @@ export default class EventResource {
|
|
|
461
329
|
|
|
462
330
|
// --- DOM Creation & Rendering ---
|
|
463
331
|
|
|
464
|
-
/**
|
|
465
|
-
* Generates and mounts the sophisticated navigation, date-selection, and view toggle toolbar components inside the DOM container.
|
|
466
|
-
* @param {HTMLElement} wrapper - REQUIRED: The main root layout frame container reference node where the toolbar will be injected.
|
|
467
|
-
* @returns {void}
|
|
468
|
-
* @private
|
|
469
|
-
* @example
|
|
470
|
-
* // Internal execution call structure
|
|
471
|
-
* this._renderToolbar(wrapperElementNode);
|
|
472
|
-
*/
|
|
473
332
|
_renderToolbar = (wrapper) => {
|
|
474
333
|
const toolbar = document.createElement("div");
|
|
475
334
|
toolbar.className = "er-toolbar";
|
|
476
335
|
|
|
477
|
-
// Left: Navigation Controls
|
|
478
336
|
const navGroup = document.createElement("div");
|
|
479
337
|
navGroup.className = "er-toolbar-group";
|
|
480
338
|
|
|
@@ -493,7 +351,6 @@ export default class EventResource {
|
|
|
493
351
|
btnNext.textContent = "▶";
|
|
494
352
|
btnNext.onclick = () => this.navigate("next");
|
|
495
353
|
|
|
496
|
-
// Date Picker HTML5 Input Integration
|
|
497
354
|
const datePicker = document.createElement("input");
|
|
498
355
|
datePicker.type = "date";
|
|
499
356
|
datePicker.className = "er-date-picker";
|
|
@@ -506,11 +363,9 @@ export default class EventResource {
|
|
|
506
363
|
|
|
507
364
|
navGroup.append(btnPrev, btnToday, btnNext, datePicker);
|
|
508
365
|
|
|
509
|
-
// Right: View Framework Mode Toggles & Freezing Management Elements
|
|
510
366
|
const viewGroup = document.createElement("div");
|
|
511
367
|
viewGroup.className = "er-toolbar-group";
|
|
512
368
|
|
|
513
|
-
// Append Client Extensible Custom Action Elements
|
|
514
369
|
this.customButtons.forEach((btnConfig) => {
|
|
515
370
|
const customBtn = document.createElement("button");
|
|
516
371
|
customBtn.className = `er-btn ${btnConfig.className || ""}`.trim();
|
|
@@ -530,7 +385,6 @@ export default class EventResource {
|
|
|
530
385
|
btnWeekly.onclick = () => this.setView("weekly");
|
|
531
386
|
|
|
532
387
|
viewGroup.append(btnDaily, btnWeekly);
|
|
533
|
-
|
|
534
388
|
toolbar.append(navGroup, viewGroup);
|
|
535
389
|
|
|
536
390
|
const btnFreeze = document.createElement("button");
|
|
@@ -542,7 +396,6 @@ export default class EventResource {
|
|
|
542
396
|
};
|
|
543
397
|
navGroup.appendChild(btnFreeze);
|
|
544
398
|
|
|
545
|
-
// Active Holiday Indicator Check
|
|
546
399
|
const currentHoliday = this._getHolidayForDate(this.currentDate);
|
|
547
400
|
if (currentHoliday) {
|
|
548
401
|
const holidayBadge = document.createElement("span");
|
|
@@ -555,15 +408,17 @@ export default class EventResource {
|
|
|
555
408
|
};
|
|
556
409
|
|
|
557
410
|
/**
|
|
558
|
-
*
|
|
559
|
-
* Builds coordinate grid intersections, mounts event cards, appends propagation intercepts, maps CSS grid templates dynamically, and finalizes DOM commitments.
|
|
411
|
+
* Manual render hook. Triggers a complete synchronization of the Skeleton UI and the Data Layer.
|
|
560
412
|
* @returns {void}
|
|
561
|
-
* @private
|
|
562
413
|
* @example
|
|
563
|
-
*
|
|
564
|
-
* this.render();
|
|
414
|
+
* calendar.render();
|
|
565
415
|
*/
|
|
566
416
|
render = () => {
|
|
417
|
+
this._renderSkeleton();
|
|
418
|
+
this._renderEvents();
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
_renderSkeleton = () => {
|
|
567
422
|
this.container.innerHTML = "";
|
|
568
423
|
|
|
569
424
|
const wrapper = document.createElement("div");
|
|
@@ -578,13 +433,12 @@ export default class EventResource {
|
|
|
578
433
|
|
|
579
434
|
const grid = document.createElement("div");
|
|
580
435
|
grid.className = `er-grid ${this.stickyHeaders ? "er-sticky" : ""}`.trim();
|
|
581
|
-
grid.style.gridTemplateColumns = `150px repeat(${this.timeSlots.length}, minmax(120px, 1fr))`;
|
|
436
|
+
grid.style.gridTemplateColumns = `150px repeat(${this.timeSlots.length || 1}, minmax(120px, 1fr))`;
|
|
582
437
|
|
|
583
438
|
const corner = document.createElement("div");
|
|
584
439
|
corner.className = "er-header-cell er-corner";
|
|
585
440
|
grid.appendChild(corner);
|
|
586
441
|
|
|
587
|
-
// Column Map Processing Loop
|
|
588
442
|
this.timeSlots.forEach((time) => {
|
|
589
443
|
const timeHeader = document.createElement("div");
|
|
590
444
|
timeHeader.className = "er-header-cell er-time-header";
|
|
@@ -600,7 +454,6 @@ export default class EventResource {
|
|
|
600
454
|
|
|
601
455
|
const activeHoliday = this._getHolidayForDate(this.currentDate);
|
|
602
456
|
|
|
603
|
-
// Row Matrix Layout Intercept Processing Loops
|
|
604
457
|
this.rooms.forEach((room, rowIndex) => {
|
|
605
458
|
const roomHeader = document.createElement("div");
|
|
606
459
|
roomHeader.className = "er-header-cell er-room-header";
|
|
@@ -617,228 +470,101 @@ export default class EventResource {
|
|
|
617
470
|
const cell = document.createElement("div");
|
|
618
471
|
cell.className = `er-grid-cell ${activeHoliday ? "er-holiday-cell" : ""}`;
|
|
619
472
|
|
|
620
|
-
|
|
473
|
+
cell.dataset.roomId = room.id;
|
|
474
|
+
cell.dataset.timeId = time.id;
|
|
621
475
|
|
|
622
476
|
cell.addEventListener("click", () => {
|
|
623
477
|
if (this.onCellClick) {
|
|
478
|
+
const currentEvents =
|
|
479
|
+
this.eventsMap.get(`${room.id}::${time.id}`) || [];
|
|
480
|
+
|
|
624
481
|
this.onCellClick({
|
|
625
482
|
date: this.currentDate,
|
|
626
483
|
view: this.currentView,
|
|
627
484
|
holiday: activeHoliday,
|
|
628
485
|
row: { index: rowIndex, data: room },
|
|
629
486
|
col: { index: colIndex, data: time },
|
|
630
|
-
cell: { roomId: room.id, timeId: time.id, events:
|
|
487
|
+
cell: { roomId: room.id, timeId: time.id, events: currentEvents },
|
|
631
488
|
});
|
|
632
489
|
}
|
|
633
490
|
});
|
|
634
491
|
|
|
635
|
-
cellEvents.forEach((ev) => {
|
|
636
|
-
const eventDiv = document.createElement("div");
|
|
637
|
-
eventDiv.className = "er-event";
|
|
638
|
-
|
|
639
|
-
// Apply base background color, allowing CSS/Tailwind classes inside the custom HTML to inherit or override it.
|
|
640
|
-
eventDiv.style.backgroundColor = ev.color || "#3b82f6";
|
|
641
|
-
|
|
642
|
-
// Intercept rendering if the custom hook exists
|
|
643
|
-
if (typeof this.renderEvent === "function") {
|
|
644
|
-
eventDiv.innerHTML = this.renderEvent(ev);
|
|
645
|
-
} else {
|
|
646
|
-
eventDiv.textContent = ev.title;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
eventDiv.addEventListener("click", (e) => {
|
|
650
|
-
e.stopPropagation();
|
|
651
|
-
|
|
652
|
-
if (this.onEventClick) {
|
|
653
|
-
this.onEventClick({
|
|
654
|
-
event: ev,
|
|
655
|
-
nativeEvent: e,
|
|
656
|
-
date: this.currentDate,
|
|
657
|
-
view: this.currentView,
|
|
658
|
-
holiday: activeHoliday,
|
|
659
|
-
row: { index: rowIndex, data: room },
|
|
660
|
-
col: { index: colIndex, data: time },
|
|
661
|
-
cell: { roomId: room.id, timeId: time.id, events: cellEvents },
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
cell.appendChild(eventDiv);
|
|
667
|
-
});
|
|
668
|
-
|
|
669
492
|
grid.appendChild(cell);
|
|
670
493
|
});
|
|
671
494
|
});
|
|
672
495
|
|
|
496
|
+
if (
|
|
497
|
+
this.rooms.length === 0 &&
|
|
498
|
+
this.timeSlots.length === 0 &&
|
|
499
|
+
this.isFetching
|
|
500
|
+
) {
|
|
501
|
+
const loadingIndicator = document.createElement("div");
|
|
502
|
+
loadingIndicator.style.padding = "20px";
|
|
503
|
+
loadingIndicator.style.color = "#6b7280";
|
|
504
|
+
loadingIndicator.style.textAlign = "center";
|
|
505
|
+
loadingIndicator.textContent = "Loading grid data...";
|
|
506
|
+
grid.appendChild(loadingIndicator);
|
|
507
|
+
}
|
|
508
|
+
|
|
673
509
|
gridWrapper.appendChild(grid);
|
|
674
510
|
wrapper.appendChild(gridWrapper);
|
|
675
511
|
this.container.appendChild(wrapper);
|
|
676
512
|
};
|
|
677
513
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
.
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
border-color: #c7d2fe;
|
|
734
|
-
}
|
|
735
|
-
.er-date-picker {
|
|
736
|
-
padding: 5px 10px;
|
|
737
|
-
border: 1px solid #d1d5db;
|
|
738
|
-
border-radius: 6px;
|
|
739
|
-
font-family: inherit;
|
|
740
|
-
font-size: 0.875rem;
|
|
741
|
-
color: #374151;
|
|
742
|
-
background: #fff;
|
|
743
|
-
cursor: pointer;
|
|
744
|
-
}
|
|
745
|
-
.er-holiday-badge {
|
|
746
|
-
font-size: 0.875rem;
|
|
747
|
-
font-weight: 600;
|
|
748
|
-
color: #059669;
|
|
749
|
-
background: #d1fae5;
|
|
750
|
-
padding: 4px 10px;
|
|
751
|
-
border-radius: 9999px;
|
|
752
|
-
margin-left: 8px;
|
|
753
|
-
}
|
|
754
|
-
.er-grid-wrapper {
|
|
755
|
-
overflow: auto;
|
|
756
|
-
max-height: 65vh;
|
|
757
|
-
width: 100%;
|
|
758
|
-
}
|
|
759
|
-
.er-grid {
|
|
760
|
-
display: grid;
|
|
761
|
-
grid-auto-rows: minmax(60px, auto);
|
|
762
|
-
}
|
|
763
|
-
.er-header-cell {
|
|
764
|
-
padding: 12px;
|
|
765
|
-
font-size: 0.875rem;
|
|
766
|
-
background: #f9fafb;
|
|
767
|
-
border-bottom: 1px solid #e5e7eb;
|
|
768
|
-
border-right: 1px solid #e5e7eb;
|
|
769
|
-
display: flex;
|
|
770
|
-
align-items: center;
|
|
771
|
-
box-sizing: border-box;
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
.er-grid.er-sticky .er-corner {
|
|
775
|
-
position: sticky;
|
|
776
|
-
top: 0;
|
|
777
|
-
left: 0;
|
|
778
|
-
z-index: 3;
|
|
779
|
-
background: #f9fafb;
|
|
780
|
-
}
|
|
781
|
-
.er-grid.er-sticky .er-time-header {
|
|
782
|
-
position: sticky;
|
|
783
|
-
top: 0;
|
|
784
|
-
z-index: 2;
|
|
785
|
-
background: #f9fafb;
|
|
786
|
-
justify-content: center;
|
|
787
|
-
color: #4b5563;
|
|
788
|
-
}
|
|
789
|
-
.er-grid.er-sticky .er-room-header {
|
|
790
|
-
position: sticky;
|
|
791
|
-
left: 0;
|
|
792
|
-
z-index: 2;
|
|
793
|
-
background: #f9fafb;
|
|
794
|
-
justify-content: flex-start;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
.er-grid:not(.er-sticky) .er-corner,
|
|
798
|
-
.er-grid:not(.er-sticky) .er-time-header,
|
|
799
|
-
.er-grid:not(.er-sticky) .er-room-header {
|
|
800
|
-
position: static;
|
|
801
|
-
background: #f9fafb;
|
|
802
|
-
}
|
|
803
|
-
.er-grid:not(.er-sticky) .er-time-header { justify-content: center; }
|
|
804
|
-
|
|
805
|
-
.er-grid-cell {
|
|
806
|
-
border-bottom: 1px solid #e5e7eb;
|
|
807
|
-
border-right: 1px solid #e5e7eb;
|
|
808
|
-
padding: 4px;
|
|
809
|
-
transition: background-color 0.2s;
|
|
810
|
-
cursor: pointer;
|
|
811
|
-
display: flex;
|
|
812
|
-
flex-direction: column;
|
|
813
|
-
gap: 4px;
|
|
814
|
-
box-sizing: border-box;
|
|
815
|
-
}
|
|
816
|
-
.er-grid-cell:hover { background-color: #f3f4f6; }
|
|
817
|
-
.er-holiday-cell { background-color: #fdfbf7; }
|
|
818
|
-
.er-holiday-cell:hover { background-color: #fef3c7; }
|
|
819
|
-
.er-event {
|
|
820
|
-
padding: 4px 8px;
|
|
821
|
-
border-radius: 4px;
|
|
822
|
-
color: white;
|
|
823
|
-
font-size: 0.75rem;
|
|
824
|
-
font-weight: 500;
|
|
825
|
-
white-space: nowrap;
|
|
826
|
-
overflow: hidden;
|
|
827
|
-
text-overflow: ellipsis;
|
|
828
|
-
cursor: pointer;
|
|
829
|
-
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
|
830
|
-
}
|
|
831
|
-
.er-event:hover { transform: scale(1.02); z-index: 10; }
|
|
832
|
-
|
|
833
|
-
.er-rich-wrapper {
|
|
834
|
-
display: flex;
|
|
835
|
-
flex-direction: column;
|
|
836
|
-
width: 100%;
|
|
837
|
-
gap: 2px;
|
|
838
|
-
}
|
|
839
|
-
.er-rich-title { font-weight: 600; color: #111827; }
|
|
840
|
-
.er-rich-subtitle { font-size: 0.75rem; color: #6b7280; font-weight: 400; }
|
|
841
|
-
`;
|
|
842
|
-
document.head.appendChild(style);
|
|
514
|
+
_renderEvents = () => {
|
|
515
|
+
const existingEvents = this.container.querySelectorAll(".er-event");
|
|
516
|
+
existingEvents.forEach((el) => el.remove());
|
|
517
|
+
|
|
518
|
+
const activeHoliday = this._getHolidayForDate(this.currentDate);
|
|
519
|
+
|
|
520
|
+
this.events.forEach((ev) => {
|
|
521
|
+
const cell = this.container.querySelector(
|
|
522
|
+
`[data-room-id="${ev.roomId}"][data-time-id="${ev.timeId}"]`,
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
if (!cell) return;
|
|
526
|
+
|
|
527
|
+
const eventDiv = document.createElement("div");
|
|
528
|
+
eventDiv.className = "er-event";
|
|
529
|
+
eventDiv.style.backgroundColor = ev.color || "#3b82f6";
|
|
530
|
+
|
|
531
|
+
if (typeof this.renderEvent === "function") {
|
|
532
|
+
eventDiv.innerHTML = this.renderEvent(ev);
|
|
533
|
+
} else {
|
|
534
|
+
eventDiv.textContent = ev.title;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
eventDiv.addEventListener("click", (e) => {
|
|
538
|
+
e.stopPropagation();
|
|
539
|
+
if (this.onEventClick) {
|
|
540
|
+
const sharedEvents =
|
|
541
|
+
this.eventsMap.get(`${ev.roomId}::${ev.timeId}`) || [];
|
|
542
|
+
|
|
543
|
+
const roomIndex = this.rooms.findIndex(
|
|
544
|
+
(r) => String(r.id) === String(ev.roomId),
|
|
545
|
+
);
|
|
546
|
+
const timeIndex = this.timeSlots.findIndex(
|
|
547
|
+
(t) => String(t.id) === String(ev.timeId),
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
this.onEventClick({
|
|
551
|
+
event: ev,
|
|
552
|
+
nativeEvent: e,
|
|
553
|
+
date: this.currentDate,
|
|
554
|
+
view: this.currentView,
|
|
555
|
+
holiday: activeHoliday,
|
|
556
|
+
row: { index: roomIndex, data: this.rooms[roomIndex] },
|
|
557
|
+
col: { index: timeIndex, data: this.timeSlots[timeIndex] },
|
|
558
|
+
cell: {
|
|
559
|
+
roomId: ev.roomId,
|
|
560
|
+
timeId: ev.timeId,
|
|
561
|
+
events: sharedEvents,
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
cell.appendChild(eventDiv);
|
|
568
|
+
});
|
|
843
569
|
};
|
|
844
570
|
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
.er-container {
|
|
2
|
+
font-family:
|
|
3
|
+
system-ui,
|
|
4
|
+
-apple-system,
|
|
5
|
+
sans-serif;
|
|
6
|
+
border: 1px solid #e5e7eb;
|
|
7
|
+
border-radius: 8px;
|
|
8
|
+
background: #fff;
|
|
9
|
+
width: 100%;
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
}
|
|
13
|
+
.er-toolbar {
|
|
14
|
+
display: flex;
|
|
15
|
+
justify-content: space-between;
|
|
16
|
+
align-items: center;
|
|
17
|
+
padding: 12px 16px;
|
|
18
|
+
border-bottom: 1px solid #e5e7eb;
|
|
19
|
+
background: #f9fafb;
|
|
20
|
+
border-radius: 8px 8px 0 0;
|
|
21
|
+
flex-wrap: wrap;
|
|
22
|
+
gap: 12px;
|
|
23
|
+
}
|
|
24
|
+
.er-toolbar-group {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
gap: 8px;
|
|
28
|
+
}
|
|
29
|
+
.er-btn {
|
|
30
|
+
padding: 6px 12px;
|
|
31
|
+
background: #fff;
|
|
32
|
+
border: 1px solid #d1d5db;
|
|
33
|
+
border-radius: 6px;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
font-size: 0.875rem;
|
|
36
|
+
font-weight: 500;
|
|
37
|
+
color: #374151;
|
|
38
|
+
transition: all 0.2s;
|
|
39
|
+
}
|
|
40
|
+
.er-btn:hover {
|
|
41
|
+
background: #f3f4f6;
|
|
42
|
+
}
|
|
43
|
+
.er-btn.active {
|
|
44
|
+
background: #e0e7ff;
|
|
45
|
+
color: #4f46e5;
|
|
46
|
+
border-color: #c7d2fe;
|
|
47
|
+
}
|
|
48
|
+
.er-date-picker {
|
|
49
|
+
padding: 5px 10px;
|
|
50
|
+
border: 1px solid #d1d5db;
|
|
51
|
+
border-radius: 6px;
|
|
52
|
+
font-family: inherit;
|
|
53
|
+
font-size: 0.875rem;
|
|
54
|
+
color: #374151;
|
|
55
|
+
background: #fff;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
}
|
|
58
|
+
.er-holiday-badge {
|
|
59
|
+
font-size: 0.875rem;
|
|
60
|
+
font-weight: 600;
|
|
61
|
+
color: #059669;
|
|
62
|
+
background: #d1fae5;
|
|
63
|
+
padding: 4px 10px;
|
|
64
|
+
border-radius: 9999px;
|
|
65
|
+
margin-left: 8px;
|
|
66
|
+
}
|
|
67
|
+
.er-grid-wrapper {
|
|
68
|
+
overflow: auto;
|
|
69
|
+
max-height: 65vh;
|
|
70
|
+
width: 100%;
|
|
71
|
+
}
|
|
72
|
+
.er-grid {
|
|
73
|
+
display: grid;
|
|
74
|
+
grid-auto-rows: minmax(60px, auto);
|
|
75
|
+
}
|
|
76
|
+
.er-header-cell {
|
|
77
|
+
padding: 12px;
|
|
78
|
+
font-size: 0.875rem;
|
|
79
|
+
background: #f9fafb;
|
|
80
|
+
border-bottom: 1px solid #e5e7eb;
|
|
81
|
+
border-right: 1px solid #e5e7eb;
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
box-sizing: border-box;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.er-grid.er-sticky .er-corner {
|
|
88
|
+
position: sticky;
|
|
89
|
+
top: 0;
|
|
90
|
+
left: 0;
|
|
91
|
+
z-index: 3;
|
|
92
|
+
background: #f9fafb;
|
|
93
|
+
}
|
|
94
|
+
.er-grid.er-sticky .er-time-header {
|
|
95
|
+
position: sticky;
|
|
96
|
+
top: 0;
|
|
97
|
+
z-index: 2;
|
|
98
|
+
background: #f9fafb;
|
|
99
|
+
justify-content: center;
|
|
100
|
+
color: #4b5563;
|
|
101
|
+
}
|
|
102
|
+
.er-grid.er-sticky .er-room-header {
|
|
103
|
+
position: sticky;
|
|
104
|
+
left: 0;
|
|
105
|
+
z-index: 2;
|
|
106
|
+
background: #f9fafb;
|
|
107
|
+
justify-content: flex-start;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.er-grid:not(.er-sticky) .er-corner,
|
|
111
|
+
.er-grid:not(.er-sticky) .er-time-header,
|
|
112
|
+
.er-grid:not(.er-sticky) .er-room-header {
|
|
113
|
+
position: static;
|
|
114
|
+
background: #f9fafb;
|
|
115
|
+
}
|
|
116
|
+
.er-grid:not(.er-sticky) .er-time-header {
|
|
117
|
+
justify-content: center;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.er-grid-cell {
|
|
121
|
+
border-bottom: 1px solid #e5e7eb;
|
|
122
|
+
border-right: 1px solid #e5e7eb;
|
|
123
|
+
padding: 4px;
|
|
124
|
+
transition: background-color 0.2s;
|
|
125
|
+
cursor: pointer;
|
|
126
|
+
display: flex;
|
|
127
|
+
flex-direction: column;
|
|
128
|
+
gap: 4px;
|
|
129
|
+
box-sizing: border-box;
|
|
130
|
+
}
|
|
131
|
+
.er-grid-cell:hover {
|
|
132
|
+
background-color: #f3f4f6;
|
|
133
|
+
}
|
|
134
|
+
.er-holiday-cell {
|
|
135
|
+
background-color: #fdfbf7;
|
|
136
|
+
}
|
|
137
|
+
.er-holiday-cell:hover {
|
|
138
|
+
background-color: #fef3c7;
|
|
139
|
+
}
|
|
140
|
+
.er-event {
|
|
141
|
+
padding: 4px 8px;
|
|
142
|
+
border-radius: 4px;
|
|
143
|
+
color: white;
|
|
144
|
+
font-size: 0.75rem;
|
|
145
|
+
font-weight: 500;
|
|
146
|
+
white-space: nowrap;
|
|
147
|
+
overflow: hidden;
|
|
148
|
+
text-overflow: ellipsis;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
151
|
+
}
|
|
152
|
+
.er-event:hover {
|
|
153
|
+
transform: scale(1.02);
|
|
154
|
+
z-index: 10;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.er-rich-wrapper {
|
|
158
|
+
display: flex;
|
|
159
|
+
flex-direction: column;
|
|
160
|
+
width: 100%;
|
|
161
|
+
gap: 2px;
|
|
162
|
+
}
|
|
163
|
+
.er-rich-title {
|
|
164
|
+
font-weight: 600;
|
|
165
|
+
color: #111827;
|
|
166
|
+
}
|
|
167
|
+
.er-rich-subtitle {
|
|
168
|
+
font-size: 0.75rem;
|
|
169
|
+
color: #6b7280;
|
|
170
|
+
font-weight: 400;
|
|
171
|
+
}
|