@ziix/calendar 0.1.0 → 0.1.2

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 CHANGED
@@ -1,13 +1,15 @@
1
1
  # @ziix/calendar
2
2
 
3
- A framework-agnostic resource & time-grid calendar — a from-scratch, **license-free**
4
- replacement for the FullCalendar views DMS uses (`timeGridDay`, `resourceTimeGridDay`,
5
- `resourceTimeline`). No premium scheduler licence, no React/Vue/Preact dependency: a
6
- plain imperative class you drive through a ref from any framework.
3
+ By [ziix.eu](https://ziix.eu) · [npm](https://www.npmjs.com/package/@ziix/calendar)
7
4
 
8
- > **Status.** All three views (`day`, `resource-day`, `timeline`) and the full interaction
9
- > engine (drag/resize/select) are implemented and tested. Remaining work is polish, the DMS
10
- > migration and the npm release — see the roadmap below.
5
+ A framework-agnostic resource & time-grid calendar with three views **day**,
6
+ **resource-day** (resources as columns) and **timeline** (resources as rows) plus
7
+ drag/resize/select, resource grouping and timezone-correct rendering. No framework
8
+ dependency: a plain imperative class you drive through a ref from React, Preact, Vue,
9
+ Svelte or vanilla JS.
10
+
11
+ > **Status.** All three views and the full interaction engine (drag/resize/select) are
12
+ > implemented and tested, and the package is published on npm.
11
13
 
12
14
  ## Install
13
15
 
@@ -25,7 +27,7 @@ import '@ziix/calendar/styles.css'
25
27
 
26
28
  const cal = new Calendar(document.getElementById('calendar'), {
27
29
  view: 'day',
28
- timezone: 'Europe/Copenhagen', // events are placed in the shop's clock, not the browser's
30
+ timezone: 'Europe/Copenhagen', // events are placed in this timezone, not the browser's
29
31
  date: '2026-06-09',
30
32
  height: 780,
31
33
  slot: { duration: 15, min: '06:00', max: '19:00', labelInterval: 60 },
@@ -58,9 +60,8 @@ events: async ({ start, end }) => {
58
60
 
59
61
  ## Timeline view (resources as rows)
60
62
 
61
- This is the `resourceTimeline` replacement: a sticky resource area on the left and a
62
- horizontally-scrolling time grid on the right. Pass `resources` (array or function) and
63
- set `view: 'timeline'`.
63
+ A sticky resource area on the left and a horizontally-scrolling time grid on the right.
64
+ Pass `resources` (array or function) and set `view: 'timeline'`.
64
65
 
65
66
  ```js
66
67
  const cal = new Calendar(el, {
@@ -92,7 +93,7 @@ const cal = new Calendar(el, {
92
93
  { id: 1, title: 'Service', start: '...', end: '...', resourceId: 'E1' },
93
94
  ],
94
95
 
95
- // custom label for the default resource column (FullCalendar's resourceLabelContent)
96
+ // custom label for the default resource column
96
97
  renderResource: (resource) => `<strong>${resource.title}</strong>`,
97
98
 
98
99
  onEventClick: ({ event }) => openOrder(event.extendedProps.orderId),
@@ -105,8 +106,8 @@ grows to fit them. An event without a matching `resourceId` is not shown in the
105
106
 
106
107
  ### Resource-day view (resources as columns)
107
108
 
108
- `view: 'resource-day'` is the `resourceTimeGridDay` equivalent: the same vertical time axis
109
- as the day view, but with one column per resource under a sticky, grouped header. Takes the
109
+ `view: 'resource-day'` uses the same vertical time axis as the day view, but with one
110
+ column per resource under a sticky, grouped header. Takes the
110
111
  same `resources` / `resourceGroupField` / `renderResource` options as the timeline. With
111
112
  `editable`, dragging an event sideways moves it to another resource column.
112
113
 
@@ -181,7 +182,7 @@ const cal = new Calendar(el, {
181
182
 
182
183
  // false ⇒ a drop/resize that would overlap another event on the same resource is
183
184
  // rejected and snaps back. May be a function evaluated per drop.
184
- eventOverlap: () => shop.settings.calendar_overlap,
185
+ eventOverlap: () => settings.allowOverlap,
185
186
 
186
187
  // gate which ranges may be selected (e.g. only employee or rental-car rows)
187
188
  selectAllow: ({ resource }) => !!resource && /^[EC]/.test(resource.id),
@@ -233,8 +234,7 @@ working menu implementation.
233
234
 
234
235
  ## Imperative API
235
236
 
236
- The calendar is a plain class you drive through a ref — mirrors the surface FullCalendar
237
- consumers rely on:
237
+ The calendar is a plain class you drive through a ref:
238
238
 
239
239
  | Method | Purpose |
240
240
  | --- | --- |
@@ -252,13 +252,12 @@ consumers rely on:
252
252
 
253
253
  ### Real-time updates
254
254
 
255
- Drive incremental updates from a websocket without a full refetch (DMS uses Laravel Echo):
255
+ Drive incremental updates from a websocket without a full refetch:
256
256
 
257
257
  ```js
258
- echo.private(`shop.${id}`)
259
- .listen('.eventCreated', (e) => cal.addEvent(e.event))
260
- .listen('.eventUpdated', () => cal.refetchEvents())
261
- .listen('.eventRemoved', (e) => cal.getEventById(e.id)?.remove())
258
+ socket.on('event:created', (e) => cal.addEvent(e.event))
259
+ socket.on('event:updated', () => cal.refetchEvents())
260
+ socket.on('event:removed', (e) => cal.getEventById(e.id)?.remove())
262
261
  ```
263
262
 
264
263
  ### Using it from Preact / React
@@ -278,7 +277,7 @@ export function CalendarView({ shopId }) {
278
277
  useEffect(() => {
279
278
  const cal = new Calendar(elRef.current, {
280
279
  view: 'timeline',
281
- timezone: window.ziix.timezone,
280
+ timezone: 'Europe/Copenhagen',
282
281
  events: ({ start, end }) => fetchEvents(shopId, start, end),
283
282
  resources: () => fetchResources(shopId),
284
283
  onEventClick: ({ event }) => openOrder(event),
@@ -297,8 +296,7 @@ export function CalendarView({ shopId }) {
297
296
  The calendar renders almost no text of its own — column headers, resource labels and
298
297
  event content all come from **your** render hooks, so they're already in your language.
299
298
  The only built-in strings are the toolbar buttons, and the date in the title. Both are
300
- driven by the `locale` option — pass your app's translations there (DMS feeds its
301
- `trans()` values straight in):
299
+ driven by the `locale` option — pass your app's translations there:
302
300
 
303
301
  ```js
304
302
  const cal = new Calendar(el, {
@@ -306,8 +304,8 @@ const cal = new Calendar(el, {
306
304
  code: 'da',
307
305
  intl: 'da-DK', // BCP-47 tag used by Intl to format the title date
308
306
  firstDay: 1,
309
- buttons: { today: trans('e.today'), prev: '‹', next: '›' },
310
- ariaLabels: { today: trans('e.today'), prev: trans('e.prev'), next: trans('e.next') },
307
+ buttons: { today: t('today'), prev: '‹', next: '›' },
308
+ ariaLabels: { today: t('today'), prev: t('prev'), next: t('next') },
311
309
  },
312
310
  })
313
311
  ```
@@ -343,17 +341,22 @@ npm run typecheck # tsc --noEmit
343
341
  npm run build # dist/ziix-calendar.js + .css + index.d.ts
344
342
  ```
345
343
 
346
- ## Roadmap
344
+ ## Features
347
345
 
348
- | Fase | Scope | State |
349
- | --- | --- | --- |
350
- | 0 | Core: Calendar class, stores, datelib, toolbar, navigation, theming | ✅ |
351
- | 1 | `day` view: time axis, overlap packing, now indicator, event hooks | ✅ |
352
- | 3 | `timeline` view (resources as rows, horizontal axis, grouping, custom resource columns, event stacking) | ✅ |
353
- | 4 | Interaction engine: drag-move, resize, drag-select, overlap/allow gating | ✅ |
354
- | 2 | `resource-day` view (resources as columns, grouped header, cross-column move) | ✅ |
355
- | 5 | Locale pack, deep-link highlight, a11y polish | ⏳ |
346
+ - **Three views** `day`, `resource-day` (resources as columns) and `timeline`
347
+ (resources as rows, horizontal axis) with grouped resources and custom resource columns
348
+ - **Interaction** drag-move (incl. across resources), resize, drag-select, with
349
+ overlap and selection gating
350
+ - **Timezone-correct** rendering via dayjs; events placed in the configured timezone
351
+ - **Imperative API** drive it from any framework through a ref
352
+ - **Real-time friendly** add/update/remove events incrementally from a websocket
353
+ - **Themeable** via `--zc-*` custom properties; **translatable** via the `locale` option
354
+ - **Typed** — ships TypeScript declarations; one peer dependency (`dayjs`)
356
355
 
357
356
  ## License
358
357
 
359
358
  MIT
359
+
360
+ ---
361
+
362
+ Built by [ziix](https://ziix.eu).
package/dist/index.d.ts CHANGED
@@ -29,7 +29,7 @@ export declare class Calendar {
29
29
  get activeStart(): Dayjs;
30
30
  /** End of the currently-shown view range. */
31
31
  get activeEnd(): Dayjs;
32
- /** The active view's type and date window (parity with FullCalendar's `view`). */
32
+ /** The active view's type and date window. */
33
33
  getView(): {
34
34
  type: ViewType;
35
35
  activeStart: Dayjs;
@@ -57,6 +57,14 @@ export declare class Calendar {
57
57
  getEvents(): CalEvent[];
58
58
  getResources(): CalResource[];
59
59
  getResourceById(id: string | number): ResourceHandle | null;
60
+ private resourceRenderScheduled;
61
+ /**
62
+ * Coalesce resource-area re-renders: many setExtendedProp calls in a row (e.g.
63
+ * pushing work hours / punch-ins for every resource) collapse into a single
64
+ * cell update on the next microtask, and event bars are never rebuilt — so the
65
+ * timeline doesn't flicker.
66
+ */
67
+ private scheduleResourceRender;
60
68
  private resourceHandle;
61
69
  private handle;
62
70
  /** Build the inner body of an event, honouring the `renderEvent` hook. */
@@ -204,7 +212,7 @@ export declare interface EventContextMenuInfo {
204
212
  jsEvent: MouseEvent;
205
213
  }
206
214
 
207
- /** Handle returned by addEvent/getEventById, mirroring the imperative API hosts rely on. */
215
+ /** Handle returned by addEvent/getEventById for imperative updates from outside. */
208
216
  export declare interface EventHandle {
209
217
  id: string;
210
218
  event: CalEvent;
@@ -308,7 +316,7 @@ export declare interface PackedEvent {
308
316
  /**
309
317
  * Pack events that share a single column (resource/day) into side-by-side
310
318
  * sub-columns so overlapping events never cover each other — the classic
311
- * interval-graph greedy colouring FullCalendar uses.
319
+ * interval-graph greedy colouring.
312
320
  *
313
321
  * Events are expected to already belong to the same column; callers filter by
314
322
  * resource first.
@@ -367,7 +375,7 @@ export declare class ResourceStore {
367
375
  * Resources in display order. With `resourceOrder: 'id'` they are sorted by a
368
376
  * natural id comparison (so 'E2' precedes 'E10'); with a numeric order field
369
377
  * they sort by it; otherwise the original input order is preserved (sort is
370
- * stable), matching FullCalendar's default.
378
+ * stable).
371
379
  */
372
380
  ordered(): CalResource[];
373
381
  /**
@@ -1 +1 @@
1
- .zc{--zc-border: #e2e2e2;--zc-bg: #ffffff;--zc-muted-bg: #f6f6f6;--zc-fg: #1a1a1a;--zc-muted-fg: #6b7280;--zc-today-bg: #f0f6ff;--zc-now: #ef4444;--zc-btn-bg: transparent;--zc-btn-fg: var(--zc-fg);--zc-btn-border: var(--zc-border);--zc-btn-hover-bg: var(--zc-muted-bg);--zc-btn-active-bg: #e7efff;--zc-event-bg: #c7dbff;--zc-event-border: #a9c6ff;--zc-event-fg: #1e3a5f;--zc-nonbusiness: oklch(0 0 0 / .045);--zc-radius: 6px;--zc-font: inherit;display:flex;flex-direction:column;box-sizing:border-box;font-family:var(--zc-font);color:var(--zc-fg);background:var(--zc-bg)}.zc *,.zc *:before,.zc *:after{box-sizing:border-box}.zc-toolbar{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.5rem .75rem;flex:0 0 auto}.zc-toolbar-section{display:flex;align-items:center;gap:.375rem}.zc-btn-group{display:inline-flex}.zc-btn-group .zc-btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0;margin-left:-1px}.zc-btn-group .zc-btn:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.zc-toolbar-center{flex:1 1 auto;justify-content:center}.zc-title{margin:0;font-size:1rem;font-weight:600;text-transform:capitalize}.zc-btn{-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer;font:inherit;font-size:.875rem;line-height:1.2;padding:.375rem .75rem;border-radius:var(--zc-radius);border:1px solid var(--zc-btn-border);background:var(--zc-btn-bg);color:var(--zc-btn-fg)}.zc-btn:hover{background:var(--zc-btn-hover-bg)}.zc-btn:active{background:var(--zc-btn-active-bg)}.zc-body{flex:1 1 auto;overflow:auto;position:relative;border-top:1px solid var(--zc-border)}.zc-timegrid{display:flex;align-items:stretch;position:relative;min-height:100%}.zc-axis{position:relative;flex:0 0 56px;border-right:1px solid var(--zc-border)}.zc-axis-label{position:absolute;right:6px;transform:translateY(-50%);font-size:.6875rem;color:var(--zc-muted-fg);white-space:nowrap}.zc-col{position:relative;flex:1 1 auto}.zc-slot-line{position:absolute;left:0;right:0;border-top:1px solid var(--zc-border);opacity:.4}.zc-slot-line.zc-slot-major{opacity:1}.zc-event{position:absolute;overflow:hidden;border-radius:var(--zc-radius);border:1px solid var(--zc-event-border);background:var(--zc-event-bg);color:var(--zc-event-fg);font-size:.75rem;cursor:pointer}.zc-event-main{padding:2px 4px;height:100%}.zc-event-default{display:flex;flex-direction:column;gap:1px;line-height:1.2}.zc-event-time{font-variant-numeric:tabular-nums;opacity:.85}.zc-event-title{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.zc-timeline{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:stretch}.zc-tl-resource-area{display:flex;flex-direction:column;overflow:hidden;border-right:1px solid var(--zc-border);background:var(--zc-bg)}.zc-tl-resource-head,.zc-tl-resource-row{display:flex;align-items:stretch;flex:0 0 auto}.zc-tl-resource-head{border-bottom:1px solid var(--zc-border);background:var(--zc-muted-bg);font-weight:600;font-size:.75rem}.zc-tl-col-head{display:flex;align-items:center;padding:0 .5rem;overflow:hidden;white-space:nowrap}.zc-tl-resource-body{flex:1 1 auto;overflow:hidden}.zc-tl-resource-row{border-bottom:1px solid var(--zc-border)}.zc-tl-col-cell{display:flex;flex-direction:column;justify-content:center;padding:.25rem .5rem;overflow:hidden;font-size:.8125rem;min-width:0}.zc-tl-group-row{display:flex;align-items:center;padding:0 .5rem;background:var(--zc-muted-bg);color:var(--zc-muted-fg);font-size:.6875rem;font-weight:700;text-transform:uppercase;letter-spacing:.03em}.zc-tl-time-area{display:flex;flex-direction:column;flex:1 1 auto;min-width:0;overflow:hidden}.zc-tl-time-head{flex:0 0 auto;overflow:hidden;position:relative;border-bottom:1px solid var(--zc-border);background:var(--zc-muted-bg)}.zc-tl-axis{position:relative;height:100%}.zc-tl-axis-label{position:absolute;top:0;bottom:0;display:flex;align-items:center;padding-left:4px;font-size:.6875rem;color:var(--zc-muted-fg);white-space:nowrap;border-left:1px solid var(--zc-border)}.zc-tl-time-body{flex:1 1 auto;overflow:auto;position:relative}.zc-tl-time-canvas{position:relative;min-height:100%}.zc-tl-overlay{position:absolute;top:0;right:0;bottom:0;left:0;pointer-events:none;z-index:0}.zc-tl-vline{position:absolute;top:0;bottom:0;border-left:1px solid var(--zc-border);opacity:.6}.zc-tl-now{position:absolute;top:0;bottom:0;border-left:2px solid var(--zc-now);z-index:3}.zc-tl-rows{position:relative;z-index:1}.zc-tl-row{position:relative;border-bottom:1px solid var(--zc-border)}.zc-tl-group-spacer{background:var(--zc-muted-bg);opacity:.5}.zc-tl-event{display:flex;align-items:stretch}.zc-tl-event .zc-event-main{padding:3px 6px}.zc-rg{position:absolute;top:0;right:0;bottom:0;left:0;overflow-y:auto;overflow-x:hidden}.zc-rg-head{position:sticky;top:0;z-index:5;display:flex;align-items:stretch;background:var(--zc-muted-bg);border-bottom:1px solid var(--zc-border)}.zc-rg-corner{flex:0 0 56px;width:56px;border-right:1px solid var(--zc-border)}.zc-rg-head-cols{flex:1 1 auto;display:flex;flex-direction:column;min-width:0}.zc-rg-group-row,.zc-rg-label-row{display:flex;align-items:stretch}.zc-rg-group-band{display:flex;align-items:center;justify-content:center;padding:2px 4px;border-left:1px solid var(--zc-border);font-size:.625rem;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:var(--zc-muted-fg);overflow:hidden;white-space:nowrap}.zc-rg-label{flex:1 1 0;display:flex;align-items:center;justify-content:center;padding:4px 6px;border-left:1px solid var(--zc-border);font-size:.8125rem;font-weight:600;min-width:0;overflow:hidden;text-align:center}.zc-rg-canvas{display:flex;align-items:stretch}.zc-rg-cols{position:relative;flex:1 1 auto;display:flex;align-items:stretch;min-width:0}.zc-rg-col{position:relative;flex:1 1 0;border-left:1px solid var(--zc-border);min-width:0}.zc-rg-col>.zc-select-box{left:2px;right:2px}.zc-rg-now{z-index:4}.zc-body.zc-closed .zc-col,.zc-body.zc-closed .zc-tl-time-canvas,.zc-body.zc-closed .zc-rg-cols{background:var(--zc-nonbusiness)}.zc-event.zc-dragging{z-index:6;opacity:.9;box-shadow:0 2px 8px #0000002e}.zc-resize-handle{position:absolute;z-index:2}.zc-resize-s{left:0;right:0;bottom:0;height:6px;cursor:ns-resize}.zc-resize-e,.zc-resize-w{top:0;bottom:0;width:6px;cursor:ew-resize}.zc-resize-e{right:0}.zc-resize-w{left:0}.zc-select-box{position:absolute;z-index:5;background:var(--zc-event-bg);border:1px solid var(--zc-event-border);opacity:.45;pointer-events:none;border-radius:var(--zc-radius)}.zc-col>.zc-select-box{left:2px;right:2px}.zc-tl-select{top:2px;bottom:2px}.zc-tl-row.zc-drop-target{background:var(--zc-today-bg)}.zc-now-indicator{position:absolute;left:0;right:0;height:0;border-top:2px solid var(--zc-now);z-index:4;pointer-events:none}.zc-now-indicator:before{content:"";position:absolute;left:-1px;top:-4px;width:7px;height:7px;border-radius:50%;background:var(--zc-now)}
1
+ .zc{--zc-border: #e2e2e2;--zc-bg: #ffffff;--zc-muted-bg: #f6f6f6;--zc-fg: #1a1a1a;--zc-muted-fg: #6b7280;--zc-today-bg: #f0f6ff;--zc-now: #ef4444;--zc-btn-bg: transparent;--zc-btn-fg: var(--zc-fg);--zc-btn-border: var(--zc-border);--zc-btn-hover-bg: var(--zc-muted-bg);--zc-btn-active-bg: #e7efff;--zc-event-bg: #c7dbff;--zc-event-border: #a9c6ff;--zc-event-fg: #1e3a5f;--zc-nonbusiness: oklch(0 0 0 / .045);--zc-radius: 6px;--zc-font: inherit;display:flex;flex-direction:column;box-sizing:border-box;font-family:var(--zc-font);color:var(--zc-fg);background:var(--zc-bg)}.zc *,.zc *:before,.zc *:after{box-sizing:border-box}.zc-toolbar{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.5rem .75rem;flex:0 0 auto}.zc-toolbar-section{display:flex;align-items:center;gap:.375rem}.zc-btn-group{display:inline-flex}.zc-btn-group .zc-btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0;margin-left:-1px}.zc-btn-group .zc-btn:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.zc-toolbar-center{flex:1 1 auto;justify-content:center}.zc-title{margin:0;font-size:1rem;font-weight:600;text-transform:capitalize}.zc-btn{-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer;font:inherit;font-size:.875rem;line-height:1.2;padding:.375rem .75rem;border-radius:var(--zc-radius);border:1px solid var(--zc-btn-border);background:var(--zc-btn-bg);color:var(--zc-btn-fg)}.zc-btn:hover{background:var(--zc-btn-hover-bg)}.zc-btn:active{background:var(--zc-btn-active-bg)}.zc-body{flex:1 1 auto;overflow:auto;position:relative;border-top:1px solid var(--zc-border)}.zc-timegrid{display:flex;align-items:stretch;position:relative;min-height:100%}.zc-axis{position:relative;flex:0 0 56px;border-right:1px solid var(--zc-border)}.zc-axis-label{position:absolute;right:6px;transform:translateY(-50%);font-size:.6875rem;color:var(--zc-muted-fg);white-space:nowrap}.zc-col{position:relative;flex:1 1 auto}.zc-slot-line{position:absolute;left:0;right:0;border-top:1px solid var(--zc-border);opacity:.4}.zc-slot-line.zc-slot-major{opacity:1}.zc-event{position:absolute;overflow:hidden;border-radius:var(--zc-radius);border:1px solid var(--zc-event-border);background:var(--zc-event-bg);color:var(--zc-event-fg);font-size:.75rem;cursor:pointer}.zc-event-main{padding:2px 4px;height:100%}.zc-event-default{display:flex;flex-direction:column;gap:1px;line-height:1.2}.zc-event-time{font-variant-numeric:tabular-nums;opacity:.85}.zc-event-title{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.zc-timeline{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:stretch}.zc-tl-resource-area{display:flex;flex-direction:column;overflow:hidden;border-right:1px solid var(--zc-border);background:var(--zc-bg)}.zc-tl-resource-head,.zc-tl-resource-row{display:flex;align-items:stretch;flex:0 0 auto}.zc-tl-resource-head{border-bottom:1px solid var(--zc-border);background:var(--zc-muted-bg);font-weight:600;font-size:.75rem}.zc-tl-col-head{display:flex;align-items:center;padding:0 .5rem;overflow:hidden;white-space:nowrap}.zc-tl-resource-body{flex:1 1 auto;overflow:hidden}.zc-tl-resource-row{border-bottom:1px solid var(--zc-border)}.zc-tl-col-cell{display:flex;flex-direction:column;justify-content:center;gap:2px;padding:.5rem;overflow:hidden;font-size:.8125rem;min-width:0}.zc-tl-group-row{display:flex;align-items:center;padding:0 .5rem;background:var(--zc-muted-bg);color:var(--zc-muted-fg);font-size:.6875rem;font-weight:700;text-transform:uppercase;letter-spacing:.03em}.zc-tl-time-area{display:flex;flex-direction:column;flex:1 1 auto;min-width:0;overflow:hidden}.zc-tl-time-head{flex:0 0 auto;overflow:hidden;position:relative;border-bottom:1px solid var(--zc-border);background:var(--zc-muted-bg)}.zc-tl-axis{position:relative;height:100%}.zc-tl-axis-label{position:absolute;top:0;bottom:0;display:flex;align-items:center;padding-left:4px;font-size:.6875rem;color:var(--zc-muted-fg);white-space:nowrap;border-left:1px solid var(--zc-border)}.zc-tl-time-body{flex:1 1 auto;overflow:auto;position:relative}.zc-tl-time-canvas{position:relative;min-height:100%}.zc-tl-overlay{position:absolute;top:0;right:0;bottom:0;left:0;pointer-events:none;z-index:0}.zc-tl-vline{position:absolute;top:0;bottom:0;border-left:1px solid var(--zc-border);opacity:.6}.zc-tl-now{position:absolute;top:0;bottom:0;border-left:2px solid var(--zc-now);z-index:3}.zc-tl-rows{position:relative;z-index:1}.zc-tl-row{position:relative;border-bottom:1px solid var(--zc-border)}.zc-tl-group-spacer{background:var(--zc-muted-bg);opacity:.5}.zc-tl-event{display:flex;align-items:stretch}.zc-tl-event .zc-event-main{padding:3px 6px}.zc-rg{position:absolute;top:0;right:0;bottom:0;left:0;overflow-y:auto;overflow-x:hidden}.zc-rg-head{position:sticky;top:0;z-index:5;display:flex;align-items:stretch;background:var(--zc-muted-bg);border-bottom:1px solid var(--zc-border)}.zc-rg-corner{flex:0 0 56px;width:56px;border-right:1px solid var(--zc-border)}.zc-rg-head-cols{flex:1 1 auto;display:flex;flex-direction:column;min-width:0}.zc-rg-group-row,.zc-rg-label-row{display:flex;align-items:stretch}.zc-rg-group-band{display:flex;align-items:center;justify-content:center;padding:2px 4px;border-left:1px solid var(--zc-border);font-size:.625rem;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:var(--zc-muted-fg);overflow:hidden;white-space:nowrap}.zc-rg-label{flex:1 1 0;display:flex;align-items:center;justify-content:center;padding:4px 6px;border-left:1px solid var(--zc-border);font-size:.8125rem;font-weight:600;min-width:0;overflow:hidden;text-align:center}.zc-rg-canvas{display:flex;align-items:stretch}.zc-rg-cols{position:relative;flex:1 1 auto;display:flex;align-items:stretch;min-width:0}.zc-rg-col{position:relative;flex:1 1 0;border-left:1px solid var(--zc-border);min-width:0}.zc-rg-col>.zc-select-box{left:2px;right:2px}.zc-rg-now{z-index:4}.zc-body.zc-closed .zc-col,.zc-body.zc-closed .zc-tl-time-canvas,.zc-body.zc-closed .zc-rg-cols{background:var(--zc-nonbusiness)}.zc-event.zc-dragging{z-index:6;opacity:.9;box-shadow:0 2px 8px #0000002e}.zc-resize-handle{position:absolute;z-index:2}.zc-resize-s{left:0;right:0;bottom:0;height:6px;cursor:ns-resize}.zc-resize-e,.zc-resize-w{top:0;bottom:0;width:6px;cursor:ew-resize}.zc-resize-e{right:0}.zc-resize-w{left:0}.zc-select-box{position:absolute;z-index:5;background:var(--zc-event-bg);border:1px solid var(--zc-event-border);opacity:.45;pointer-events:none;border-radius:var(--zc-radius)}.zc-col>.zc-select-box{left:2px;right:2px}.zc-tl-select{top:2px;bottom:2px}.zc-tl-row.zc-drop-target,.zc-rg-col.zc-drop-target{background:var(--zc-today-bg);outline:2px dashed var(--zc-event-border);outline-offset:-2px}.zc-now-indicator{position:absolute;left:0;right:0;height:0;border-top:2px solid var(--zc-now);z-index:4;pointer-events:none}.zc-now-indicator:before{content:"";position:absolute;left:-1px;top:-4px;width:7px;height:7px;border-radius:50%;background:var(--zc-now)}