Adds header drawer and event drag interactions
Introduces HeaderDrawerRenderer and HeaderDrawerLayoutEngine to support dragging events into an all-day header drawer Enables dynamic event placement and conversion between timed and all-day events through new drag interactions Implements flexible layout calculation for header items with column and row management Extends DragDropManager to handle header zone interactions Adds new event types for header drag events
This commit is contained in:
parent
026d83eb32
commit
6723658fd9
11 changed files with 850 additions and 4 deletions
408
docs/V2-ARCHITECTURE.md
Normal file
408
docs/V2-ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,408 @@
|
||||||
|
# Calendar V2 Architecture
|
||||||
|
|
||||||
|
## Oversigt
|
||||||
|
|
||||||
|
Calendar V2 er bygget med en event-driven arkitektur og et fleksibelt grouping-system der tillader forskellige kalendervisninger (dag, uge, ressource, team).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Event System
|
||||||
|
|
||||||
|
### CoreEvents Katalog
|
||||||
|
|
||||||
|
Alle events er defineret i `src/v2/constants/CoreEvents.ts`.
|
||||||
|
|
||||||
|
#### Drag-Drop Events
|
||||||
|
|
||||||
|
| Event | Payload | Emitter | Subscribers |
|
||||||
|
|-------|---------|---------|-------------|
|
||||||
|
| `event:drag-start` | `IDragStartPayload` | DragDropManager | EdgeScrollManager |
|
||||||
|
| `event:drag-move` | `IDragMovePayload` | DragDropManager | EventRenderer |
|
||||||
|
| `event:drag-end` | `IDragEndPayload` | DragDropManager | EdgeScrollManager |
|
||||||
|
| `event:drag-cancel` | `IDragCancelPayload` | DragDropManager | EdgeScrollManager |
|
||||||
|
| `event:drag-column-change` | `IDragColumnChangePayload` | DragDropManager | EventRenderer |
|
||||||
|
|
||||||
|
#### Resize Events
|
||||||
|
|
||||||
|
| Event | Payload | Emitter | Subscribers |
|
||||||
|
|-------|---------|---------|-------------|
|
||||||
|
| `event:resize-start` | `IResizeStartPayload` | ResizeManager | - |
|
||||||
|
| `event:resize-end` | `IResizeEndPayload` | ResizeManager | - |
|
||||||
|
|
||||||
|
#### Edge Scroll Events
|
||||||
|
|
||||||
|
| Event | Payload | Emitter | Subscribers |
|
||||||
|
|-------|---------|---------|-------------|
|
||||||
|
| `edge-scroll:tick` | `{ scrollDelta: number }` | EdgeScrollManager | DragDropManager |
|
||||||
|
| `edge-scroll:started` | `{}` | EdgeScrollManager | - |
|
||||||
|
| `edge-scroll:stopped` | `{}` | EdgeScrollManager | - |
|
||||||
|
|
||||||
|
#### Lifecycle Events
|
||||||
|
|
||||||
|
| Event | Purpose |
|
||||||
|
|-------|---------|
|
||||||
|
| `core:initialized` | DI container klar |
|
||||||
|
| `core:ready` | Kalender fuldt initialiseret |
|
||||||
|
| `core:destroyed` | Cleanup færdig |
|
||||||
|
|
||||||
|
#### Data Events
|
||||||
|
|
||||||
|
| Event | Purpose |
|
||||||
|
|-------|---------|
|
||||||
|
| `data:loading` | Data fetch startet |
|
||||||
|
| `data:loaded` | Data fetch færdig |
|
||||||
|
| `data:error` | Data fetch fejlet |
|
||||||
|
| `entity:saved` | Entity gemt i IndexedDB |
|
||||||
|
| `entity:deleted` | Entity slettet fra IndexedDB |
|
||||||
|
|
||||||
|
#### View Events
|
||||||
|
|
||||||
|
| Event | Purpose |
|
||||||
|
|-------|---------|
|
||||||
|
| `view:changed` | View/grouping ændret |
|
||||||
|
| `view:rendered` | View rendering færdig |
|
||||||
|
| `events:rendered` | Events renderet til DOM |
|
||||||
|
| `grid:rendered` | Time grid renderet |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Event Flows
|
||||||
|
|
||||||
|
### Drag-Drop Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
pointerdown på event
|
||||||
|
└── DragDropManager.handlePointerDown()
|
||||||
|
└── Gem mouseDownPosition, capture pointer
|
||||||
|
|
||||||
|
pointermove (>5px)
|
||||||
|
└── DragDropManager.initializeDrag()
|
||||||
|
├── Opret ghost clone (opacity 0.3)
|
||||||
|
├── Marker original med .dragging
|
||||||
|
├── EMIT: event:drag-start
|
||||||
|
│ └── EdgeScrollManager starter scrollTick loop
|
||||||
|
└── Start animateDrag() RAF loop
|
||||||
|
|
||||||
|
pointermove (under drag)
|
||||||
|
├── DragDropManager.updateDragTarget()
|
||||||
|
│ ├── Detect kolonneændring
|
||||||
|
│ │ └── EMIT: event:drag-column-change
|
||||||
|
│ │ └── EventRenderer flytter element til ny kolonne
|
||||||
|
│ └── Beregn targetY for smooth interpolation
|
||||||
|
│
|
||||||
|
└── DragDropManager.animateDrag() (RAF)
|
||||||
|
├── Interpoler currentY mod targetY (factor 0.3)
|
||||||
|
├── Opdater element.style.top
|
||||||
|
└── EMIT: event:drag-move
|
||||||
|
└── EventRenderer.updateDragTimestamp()
|
||||||
|
└── Beregn snapped tid og opdater visning
|
||||||
|
|
||||||
|
EdgeScrollManager.scrollTick() (parallel RAF)
|
||||||
|
├── Beregn velocity baseret på museafstand til kanter
|
||||||
|
│ - Inner zone (0-50px): 640 px/sek
|
||||||
|
│ - Outer zone (50-100px): 140 px/sek
|
||||||
|
├── Scroll viewport: scrollTop += scrollDelta
|
||||||
|
└── EMIT: edge-scroll:tick
|
||||||
|
└── DragDropManager kompenserer element position
|
||||||
|
|
||||||
|
pointerup
|
||||||
|
└── DragDropManager.handlePointerUp()
|
||||||
|
├── Snap currentY til grid (15-min intervaller)
|
||||||
|
├── Fjern ghost element
|
||||||
|
├── EMIT: event:drag-end
|
||||||
|
│ └── EdgeScrollManager stopper scroll
|
||||||
|
└── Persist ændringer til IndexedDB
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Grouping System
|
||||||
|
|
||||||
|
### ViewConfig
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ViewConfig {
|
||||||
|
templateId: string; // 'day' | 'simple' | 'resource' | 'team'
|
||||||
|
groupings: GroupingConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupingConfig {
|
||||||
|
type: string; // 'date' | 'resource' | 'team'
|
||||||
|
values: string[]; // IDs der skal vises
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sådan bestemmer groupings strukturen
|
||||||
|
|
||||||
|
1. **Kolonneantal**: Produkt af alle grouping dimensioner
|
||||||
|
- Eksempel: `5 datoer × 2 ressourcer = 10 kolonner`
|
||||||
|
|
||||||
|
2. **Header layout**: CSS grid bruger `data-levels` til at stakke headers
|
||||||
|
- `data-levels="date"` → 1 header række
|
||||||
|
- `data-levels="resource date"` → 2 header rækker
|
||||||
|
- `data-levels="team resource date"` → 3 header rækker
|
||||||
|
|
||||||
|
3. **Renderer selektion**: Grouping types matches til tilgængelige renderers
|
||||||
|
|
||||||
|
### Konfigurationseksempler
|
||||||
|
|
||||||
|
#### Simple Date View (3 dage, ingen ressourcer)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
templateId: 'simple',
|
||||||
|
groupings: [
|
||||||
|
{ type: 'date', values: ['2024-01-15', '2024-01-16', '2024-01-17'] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resultat:** 3 kolonner, 1 header række
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─── Date1 ───┬─── Date2 ───┬─── Date3 ───┐
|
||||||
|
│ │ │ │
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Resource View (2 ressourcer, 3 dage)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
templateId: 'resource',
|
||||||
|
groupings: [
|
||||||
|
{ type: 'resource', values: ['EMP001', 'EMP002'] },
|
||||||
|
{ type: 'date', values: ['2024-01-15', '2024-01-16', '2024-01-17'] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resultat:** 6 kolonner, 2 header rækker
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───── Resource1 (span 3) ─────┬───── Resource2 (span 3) ─────┐
|
||||||
|
├─── D1 ───┬─── D2 ───┬─── D3 ─┼─── D1 ───┬─── D2 ───┬─── D3 ─┤
|
||||||
|
│ │ │ │ │ │ │
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Team View (2 teams, 2 ressourcer, 3 dage)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
templateId: 'team',
|
||||||
|
groupings: [
|
||||||
|
{ type: 'team', values: ['team1', 'team2'] },
|
||||||
|
{ type: 'resource', values: ['res1', 'res2'] },
|
||||||
|
{ type: 'date', values: ['2024-01-15', '2024-01-16', '2024-01-17'] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resultat:** 12 kolonner, 3 header rækker
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────── Team1 (span 6) ───────────┬─────────── Team2 (span 6) ───────────┐
|
||||||
|
├───── Res1 (span 3) ──┬── Res2 (3) ───┼───── Res1 (span 3) ──┬── Res2 (3) ───┤
|
||||||
|
├── D1 ─┬── D2 ─┬── D3 ┼── D1 ─┬── D2 ─┼── D1 ─┬── D2 ─┬── D3 ┼── D1 ─┬── D2 ─┤
|
||||||
|
│ │ │ │ │ │ │ │ │ │ │
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Header Rendering
|
||||||
|
|
||||||
|
### data-levels Attribut
|
||||||
|
|
||||||
|
`swp-calendar-header` modtager `data-levels` som bestemmer header row layout:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// I CalendarOrchestrator.render()
|
||||||
|
const levels = viewConfig.groupings.map(g => g.type).join(' ');
|
||||||
|
headerContainer.dataset.levels = levels; // "resource date" eller "team resource date"
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS Grid Styling
|
||||||
|
|
||||||
|
```css
|
||||||
|
swp-calendar-header[data-levels="date"] > swp-day-header {
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-calendar-header[data-levels="resource date"] {
|
||||||
|
> swp-resource-header { grid-row: 1; }
|
||||||
|
> swp-day-header { grid-row: 2; }
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-calendar-header[data-levels="team resource date"] {
|
||||||
|
> swp-team-header { grid-row: 1; }
|
||||||
|
> swp-resource-header { grid-row: 2; }
|
||||||
|
> swp-day-header { grid-row: 3; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rendering Pipeline
|
||||||
|
|
||||||
|
1. Renderers eksekveres i den rækkefølge de står i `viewConfig.groupings`
|
||||||
|
2. Hver renderer APPENDER sine headers til `headerContainer`
|
||||||
|
3. CSS bruger `data-levels` + element type til at positionere i korrekt række
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Renderers
|
||||||
|
|
||||||
|
### DateRenderer
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class DateRenderer implements IRenderer {
|
||||||
|
readonly type = 'date';
|
||||||
|
|
||||||
|
render(context: IRenderContext): void {
|
||||||
|
// For HVER ressource (eller én gang hvis ingen):
|
||||||
|
// For HVER dato i filter['date']:
|
||||||
|
// Opret swp-day-header + swp-day-column
|
||||||
|
// Sæt dataset.date & dataset.resourceId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ResourceRenderer
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class ResourceRenderer implements IRenderer {
|
||||||
|
readonly type = 'resource';
|
||||||
|
|
||||||
|
async render(context: IRenderContext): Promise<void> {
|
||||||
|
// Load IResource[] fra ResourceService
|
||||||
|
// For HVER ressource:
|
||||||
|
// Opret swp-resource-header
|
||||||
|
// Sæt grid-column: span ${dateCount}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TeamRenderer
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class TeamRenderer implements IRenderer {
|
||||||
|
readonly type = 'team';
|
||||||
|
|
||||||
|
render(context: IRenderContext): void {
|
||||||
|
// For HVERT team:
|
||||||
|
// Tæl ressourcer der tilhører team
|
||||||
|
// Opret swp-team-header
|
||||||
|
// Sæt grid-column: span ${colspan}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Grid Konfiguration
|
||||||
|
|
||||||
|
### IGridConfig
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface IGridConfig {
|
||||||
|
hourHeight: number; // pixels per time (fx 60)
|
||||||
|
dayStartHour: number; // fx 6 (06:00)
|
||||||
|
dayEndHour: number; // fx 18 (18:00)
|
||||||
|
snapInterval: number; // minutter (fx 15)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Position Beregning
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function calculateEventPosition(start: Date, end: Date, config: IGridConfig): EventPosition {
|
||||||
|
const startMinutes = start.getHours() * 60 + start.getMinutes();
|
||||||
|
const endMinutes = end.getHours() * 60 + end.getMinutes();
|
||||||
|
|
||||||
|
const dayStartMinutes = config.dayStartHour * 60;
|
||||||
|
const minuteHeight = config.hourHeight / 60;
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: (startMinutes - dayStartMinutes) * minuteHeight,
|
||||||
|
height: (endMinutes - startMinutes) * minuteHeight
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Eksempel:** Event 09:00-10:00 med dayStartHour=6, hourHeight=60
|
||||||
|
- startMinutes = 540, dayStartMinutes = 360
|
||||||
|
- top = (540 - 360) × 1 = **180px**
|
||||||
|
- height = (600 - 540) × 1 = **60px**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Z-Index Stack
|
||||||
|
|
||||||
|
```
|
||||||
|
z-index: 10 ← Events (interaktive, draggable)
|
||||||
|
z-index: 5 ← Unavailable zones (visuel, pointer-events: none)
|
||||||
|
z-index: 2 ← Time linjer (baggrund)
|
||||||
|
z-index: 1 ← Kvarter linjer (baggrund)
|
||||||
|
z-index: 0 ← Grid baggrund
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Komponenter
|
||||||
|
|
||||||
|
| Komponent | Ansvar | Fil |
|
||||||
|
|-----------|--------|-----|
|
||||||
|
| **CalendarOrchestrator** | Orkestrerer renderer pipeline | `core/CalendarOrchestrator.ts` |
|
||||||
|
| **DateRenderer** | Opretter dag-headers og kolonner | `features/date/DateRenderer.ts` |
|
||||||
|
| **ResourceRenderer** | Opretter ressource-headers | `features/resource/ResourceRenderer.ts` |
|
||||||
|
| **TeamRenderer** | Opretter team-headers | `features/team/TeamRenderer.ts` |
|
||||||
|
| **EventRenderer** | Renderer events med positioner | `features/event/EventRenderer.ts` |
|
||||||
|
| **ScheduleRenderer** | Renderer unavailable zoner | `features/schedule/ScheduleRenderer.ts` |
|
||||||
|
| **DragDropManager** | Håndterer drag-drop | `managers/DragDropManager.ts` |
|
||||||
|
| **ResizeManager** | Håndterer event resize | `managers/ResizeManager.ts` |
|
||||||
|
| **EdgeScrollManager** | Auto-scroll ved kanter | `managers/EdgeScrollManager.ts` |
|
||||||
|
| **ScrollManager** | Synkroniserer scroll | `core/ScrollManager.ts` |
|
||||||
|
| **EventBus** | Central event dispatcher | `core/EventBus.ts` |
|
||||||
|
| **DateService** | Dato formatering og beregning | `core/DateService.ts` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Filer
|
||||||
|
|
||||||
|
```
|
||||||
|
src/v2/
|
||||||
|
├── constants/
|
||||||
|
│ └── CoreEvents.ts # Alle event konstanter
|
||||||
|
├── core/
|
||||||
|
│ ├── CalendarOrchestrator.ts
|
||||||
|
│ ├── DateService.ts
|
||||||
|
│ ├── EventBus.ts
|
||||||
|
│ ├── IGridConfig.ts
|
||||||
|
│ ├── IGroupingRenderer.ts
|
||||||
|
│ ├── RenderBuilder.ts
|
||||||
|
│ ├── ScrollManager.ts
|
||||||
|
│ └── ViewConfig.ts
|
||||||
|
├── features/
|
||||||
|
│ ├── date/
|
||||||
|
│ │ └── DateRenderer.ts
|
||||||
|
│ ├── event/
|
||||||
|
│ │ └── EventRenderer.ts
|
||||||
|
│ ├── resource/
|
||||||
|
│ │ └── ResourceRenderer.ts
|
||||||
|
│ ├── schedule/
|
||||||
|
│ │ └── ScheduleRenderer.ts
|
||||||
|
│ └── team/
|
||||||
|
│ └── TeamRenderer.ts
|
||||||
|
├── managers/
|
||||||
|
│ ├── DragDropManager.ts
|
||||||
|
│ └── EdgeScrollManager.ts
|
||||||
|
├── storage/
|
||||||
|
│ ├── BaseEntityService.ts
|
||||||
|
│ ├── IndexedDBContext.ts
|
||||||
|
│ └── events/
|
||||||
|
│ ├── EventService.ts
|
||||||
|
│ └── EventStore.ts
|
||||||
|
├── types/
|
||||||
|
│ ├── CalendarTypes.ts
|
||||||
|
│ ├── DragTypes.ts
|
||||||
|
│ └── ScheduleTypes.ts
|
||||||
|
└── utils/
|
||||||
|
└── PositionUtils.ts
|
||||||
|
```
|
||||||
|
|
@ -44,6 +44,7 @@ import { DataSeeder } from './workers/DataSeeder';
|
||||||
// Features
|
// Features
|
||||||
import { EventRenderer } from './features/event/EventRenderer';
|
import { EventRenderer } from './features/event/EventRenderer';
|
||||||
import { ScheduleRenderer } from './features/schedule/ScheduleRenderer';
|
import { ScheduleRenderer } from './features/schedule/ScheduleRenderer';
|
||||||
|
import { HeaderDrawerRenderer } from './features/headerdrawer/HeaderDrawerRenderer';
|
||||||
|
|
||||||
// Schedule
|
// Schedule
|
||||||
import { ScheduleOverrideStore } from './storage/schedules/ScheduleOverrideStore';
|
import { ScheduleOverrideStore } from './storage/schedules/ScheduleOverrideStore';
|
||||||
|
|
@ -135,6 +136,7 @@ export function createV2Container(): Container {
|
||||||
// Features
|
// Features
|
||||||
builder.registerType(EventRenderer).as<EventRenderer>();
|
builder.registerType(EventRenderer).as<EventRenderer>();
|
||||||
builder.registerType(ScheduleRenderer).as<ScheduleRenderer>();
|
builder.registerType(ScheduleRenderer).as<ScheduleRenderer>();
|
||||||
|
builder.registerType(HeaderDrawerRenderer).as<HeaderDrawerRenderer>();
|
||||||
|
|
||||||
// Renderers - registreres som Renderer (array injection til CalendarOrchestrator)
|
// Renderers - registreres som Renderer (array injection til CalendarOrchestrator)
|
||||||
builder.registerType(DateRenderer).as<IRenderer>();
|
builder.registerType(DateRenderer).as<IRenderer>();
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,11 @@ export const CoreEvents = {
|
||||||
EVENT_DRAG_CANCEL: 'event:drag-cancel',
|
EVENT_DRAG_CANCEL: 'event:drag-cancel',
|
||||||
EVENT_DRAG_COLUMN_CHANGE: 'event:drag-column-change',
|
EVENT_DRAG_COLUMN_CHANGE: 'event:drag-column-change',
|
||||||
|
|
||||||
|
// Header drag (timed → header conversion)
|
||||||
|
EVENT_DRAG_ENTER_HEADER: 'event:drag-enter-header',
|
||||||
|
EVENT_DRAG_MOVE_HEADER: 'event:drag-move-header',
|
||||||
|
EVENT_DRAG_LEAVE_HEADER: 'event:drag-leave-header',
|
||||||
|
|
||||||
// Event resize
|
// Event resize
|
||||||
EVENT_RESIZE_START: 'event:resize-start',
|
EVENT_RESIZE_START: 'event:resize-start',
|
||||||
EVENT_RESIZE_END: 'event:resize-end',
|
EVENT_RESIZE_END: 'event:resize-end',
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { ViewConfig } from '../core/ViewConfig';
|
||||||
import { DragDropManager } from '../managers/DragDropManager';
|
import { DragDropManager } from '../managers/DragDropManager';
|
||||||
import { EdgeScrollManager } from '../managers/EdgeScrollManager';
|
import { EdgeScrollManager } from '../managers/EdgeScrollManager';
|
||||||
import { ResizeManager } from '../managers/ResizeManager';
|
import { ResizeManager } from '../managers/ResizeManager';
|
||||||
|
import { HeaderDrawerRenderer } from '../features/headerdrawer/HeaderDrawerRenderer';
|
||||||
|
|
||||||
export class DemoApp {
|
export class DemoApp {
|
||||||
private animator!: NavigationAnimator;
|
private animator!: NavigationAnimator;
|
||||||
|
|
@ -27,7 +28,8 @@ export class DemoApp {
|
||||||
private dataSeeder: DataSeeder,
|
private dataSeeder: DataSeeder,
|
||||||
private dragDropManager: DragDropManager,
|
private dragDropManager: DragDropManager,
|
||||||
private edgeScrollManager: EdgeScrollManager,
|
private edgeScrollManager: EdgeScrollManager,
|
||||||
private resizeManager: ResizeManager
|
private resizeManager: ResizeManager,
|
||||||
|
private headerDrawerRenderer: HeaderDrawerRenderer
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
|
|
|
||||||
135
src/v2/features/headerdrawer/HeaderDrawerLayoutEngine.ts
Normal file
135
src/v2/features/headerdrawer/HeaderDrawerLayoutEngine.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
/**
|
||||||
|
* HeaderDrawerLayoutEngine - Calculates row placement for header items
|
||||||
|
*
|
||||||
|
* Prevents visual overlap by assigning items to different rows when
|
||||||
|
* they occupy the same columns. Uses a track-based algorithm similar
|
||||||
|
* to V1's AllDayLayoutEngine.
|
||||||
|
*
|
||||||
|
* Each row can hold multiple items as long as they don't overlap in columns.
|
||||||
|
* When an item spans columns that are already occupied, it's placed in the
|
||||||
|
* next available row.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface IHeaderItemLayout {
|
||||||
|
itemId: string;
|
||||||
|
gridArea: string; // "row / col-start / row+1 / col-end"
|
||||||
|
startColumn: number;
|
||||||
|
endColumn: number;
|
||||||
|
row: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IHeaderItemInput {
|
||||||
|
id: string;
|
||||||
|
columnStart: number; // 0-based column index
|
||||||
|
columnEnd: number; // 0-based end column (inclusive)
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HeaderDrawerLayoutEngine {
|
||||||
|
private tracks: boolean[][] = [];
|
||||||
|
private columnCount: number;
|
||||||
|
|
||||||
|
constructor(columnCount: number) {
|
||||||
|
this.columnCount = columnCount;
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset tracks for new layout calculation
|
||||||
|
*/
|
||||||
|
reset(): void {
|
||||||
|
this.tracks = [new Array(this.columnCount).fill(false)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate layout for all items
|
||||||
|
* Items should be sorted by start column for optimal packing
|
||||||
|
*/
|
||||||
|
calculateLayout(items: IHeaderItemInput[]): IHeaderItemLayout[] {
|
||||||
|
this.reset();
|
||||||
|
const layouts: IHeaderItemLayout[] = [];
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const row = this.findAvailableRow(item.columnStart, item.columnEnd);
|
||||||
|
|
||||||
|
// Mark columns as occupied in this row
|
||||||
|
for (let col = item.columnStart; col <= item.columnEnd; col++) {
|
||||||
|
this.tracks[row][col] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// gridArea format: "row / col-start / row+1 / col-end"
|
||||||
|
// CSS grid uses 1-based indices
|
||||||
|
layouts.push({
|
||||||
|
itemId: item.id,
|
||||||
|
gridArea: `${row + 1} / ${item.columnStart + 1} / ${row + 2} / ${item.columnEnd + 2}`,
|
||||||
|
startColumn: item.columnStart,
|
||||||
|
endColumn: item.columnEnd,
|
||||||
|
row: row + 1 // 1-based for CSS
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return layouts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate layout for a single new item
|
||||||
|
* Useful for real-time drag operations
|
||||||
|
*/
|
||||||
|
calculateSingleLayout(item: IHeaderItemInput): IHeaderItemLayout {
|
||||||
|
const row = this.findAvailableRow(item.columnStart, item.columnEnd);
|
||||||
|
|
||||||
|
// Mark columns as occupied
|
||||||
|
for (let col = item.columnStart; col <= item.columnEnd; col++) {
|
||||||
|
this.tracks[row][col] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
itemId: item.id,
|
||||||
|
gridArea: `${row + 1} / ${item.columnStart + 1} / ${row + 2} / ${item.columnEnd + 2}`,
|
||||||
|
startColumn: item.columnStart,
|
||||||
|
endColumn: item.columnEnd,
|
||||||
|
row: row + 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the first row where all columns in range are available
|
||||||
|
*/
|
||||||
|
private findAvailableRow(startCol: number, endCol: number): number {
|
||||||
|
for (let row = 0; row < this.tracks.length; row++) {
|
||||||
|
if (this.isRowAvailable(row, startCol, endCol)) {
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new row if all existing rows are occupied
|
||||||
|
this.tracks.push(new Array(this.columnCount).fill(false));
|
||||||
|
return this.tracks.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if columns in range are all available in given row
|
||||||
|
*/
|
||||||
|
private isRowAvailable(row: number, startCol: number, endCol: number): boolean {
|
||||||
|
for (let col = startCol; col <= endCol; col++) {
|
||||||
|
if (this.tracks[row][col]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of rows currently in use
|
||||||
|
*/
|
||||||
|
getRowCount(): number {
|
||||||
|
return this.tracks.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update column count (e.g., when view changes)
|
||||||
|
*/
|
||||||
|
setColumnCount(count: number): void {
|
||||||
|
this.columnCount = count;
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
151
src/v2/features/headerdrawer/HeaderDrawerRenderer.ts
Normal file
151
src/v2/features/headerdrawer/HeaderDrawerRenderer.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
import { IEventBus } from '../../types/CalendarTypes';
|
||||||
|
import { IGridConfig } from '../../core/IGridConfig';
|
||||||
|
import { CoreEvents } from '../../constants/CoreEvents';
|
||||||
|
import {
|
||||||
|
IDragEnterHeaderPayload,
|
||||||
|
IDragMoveHeaderPayload,
|
||||||
|
IDragLeaveHeaderPayload
|
||||||
|
} from '../../types/DragTypes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HeaderDrawerRenderer - Handles rendering of items in the header drawer
|
||||||
|
*
|
||||||
|
* Listens to drag events from DragDropManager and creates/manages
|
||||||
|
* swp-header-item elements in the header drawer.
|
||||||
|
*
|
||||||
|
* Uses subgrid for column alignment with parent swp-calendar-header.
|
||||||
|
* Position items via gridArea for explicit row/column placement.
|
||||||
|
*/
|
||||||
|
export class HeaderDrawerRenderer {
|
||||||
|
private currentItem: HTMLElement | null = null;
|
||||||
|
private container: HTMLElement | null = null;
|
||||||
|
private sourceElement: HTMLElement | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private eventBus: IEventBus,
|
||||||
|
private gridConfig: IGridConfig
|
||||||
|
) {
|
||||||
|
this.setupListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup event listeners for drag events
|
||||||
|
*/
|
||||||
|
private setupListeners(): void {
|
||||||
|
this.eventBus.on(CoreEvents.EVENT_DRAG_ENTER_HEADER, (e) => {
|
||||||
|
const payload = (e as CustomEvent<IDragEnterHeaderPayload>).detail;
|
||||||
|
this.handleDragEnter(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventBus.on(CoreEvents.EVENT_DRAG_MOVE_HEADER, (e) => {
|
||||||
|
const payload = (e as CustomEvent<IDragMoveHeaderPayload>).detail;
|
||||||
|
this.handleDragMove(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventBus.on(CoreEvents.EVENT_DRAG_LEAVE_HEADER, (e) => {
|
||||||
|
const payload = (e as CustomEvent<IDragLeaveHeaderPayload>).detail;
|
||||||
|
this.handleDragLeave(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventBus.on(CoreEvents.EVENT_DRAG_END, () => {
|
||||||
|
this.handleDragEnd();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventBus.on(CoreEvents.EVENT_DRAG_CANCEL, () => {
|
||||||
|
this.cleanup();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle drag entering header zone - create preview item
|
||||||
|
*/
|
||||||
|
private handleDragEnter(payload: IDragEnterHeaderPayload): void {
|
||||||
|
this.container = document.querySelector('swp-header-drawer');
|
||||||
|
if (!this.container) return;
|
||||||
|
|
||||||
|
// Store reference to source element
|
||||||
|
this.sourceElement = payload.element;
|
||||||
|
|
||||||
|
// Create header item
|
||||||
|
const item = document.createElement('swp-header-item');
|
||||||
|
item.dataset.id = payload.eventId;
|
||||||
|
item.dataset.itemType = payload.itemType;
|
||||||
|
item.dataset.date = payload.sourceDate;
|
||||||
|
item.dataset.duration = String(payload.duration);
|
||||||
|
item.textContent = payload.title;
|
||||||
|
|
||||||
|
// Apply color class if present
|
||||||
|
if (payload.colorClass) {
|
||||||
|
item.classList.add(payload.colorClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add dragging state
|
||||||
|
item.classList.add('dragging');
|
||||||
|
|
||||||
|
// Initial placement (duration determines column span)
|
||||||
|
// gridArea format: "row / col-start / row+1 / col-end"
|
||||||
|
const col = payload.sourceColumnIndex + 1;
|
||||||
|
const endCol = col + payload.duration;
|
||||||
|
item.style.gridArea = `1 / ${col} / 2 / ${endCol}`;
|
||||||
|
|
||||||
|
this.container.appendChild(item);
|
||||||
|
this.currentItem = item;
|
||||||
|
|
||||||
|
// Hide original element while in header
|
||||||
|
payload.element.style.visibility = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle drag moving within header - update column position
|
||||||
|
*/
|
||||||
|
private handleDragMove(payload: IDragMoveHeaderPayload): void {
|
||||||
|
if (!this.currentItem) return;
|
||||||
|
|
||||||
|
// Update column position (duration=1 for now)
|
||||||
|
const col = payload.columnIndex + 1;
|
||||||
|
const duration = parseInt(this.currentItem.dataset.duration || '1', 10);
|
||||||
|
const endCol = col + duration;
|
||||||
|
|
||||||
|
this.currentItem.style.gridArea = `1 / ${col} / 2 / ${endCol}`;
|
||||||
|
this.currentItem.dataset.date = payload.dateKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle drag leaving header - remove preview and restore source
|
||||||
|
*/
|
||||||
|
private handleDragLeave(_payload: IDragLeaveHeaderPayload): void {
|
||||||
|
this.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle drag end - finalize the item (it stays in header)
|
||||||
|
*/
|
||||||
|
private handleDragEnd(): void {
|
||||||
|
if (!this.currentItem) return;
|
||||||
|
|
||||||
|
// Remove dragging state
|
||||||
|
this.currentItem.classList.remove('dragging');
|
||||||
|
|
||||||
|
// Item stays - it's now permanent
|
||||||
|
// TODO: Emit event to persist allDay=true change
|
||||||
|
|
||||||
|
// Clear references but leave item in DOM
|
||||||
|
this.currentItem = null;
|
||||||
|
this.sourceElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup preview item and restore source visibility
|
||||||
|
*/
|
||||||
|
private cleanup(): void {
|
||||||
|
// Remove preview item
|
||||||
|
this.currentItem?.remove();
|
||||||
|
this.currentItem = null;
|
||||||
|
|
||||||
|
// Restore source element visibility
|
||||||
|
if (this.sourceElement) {
|
||||||
|
this.sourceElement.style.visibility = '';
|
||||||
|
this.sourceElement = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/v2/features/headerdrawer/index.ts
Normal file
2
src/v2/features/headerdrawer/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { HeaderDrawerRenderer } from './HeaderDrawerRenderer';
|
||||||
|
export { HeaderDrawerLayoutEngine, type IHeaderItemLayout, type IHeaderItemInput } from './HeaderDrawerLayoutEngine';
|
||||||
|
|
@ -8,7 +8,10 @@ import {
|
||||||
IDragMovePayload,
|
IDragMovePayload,
|
||||||
IDragEndPayload,
|
IDragEndPayload,
|
||||||
IDragCancelPayload,
|
IDragCancelPayload,
|
||||||
IDragColumnChangePayload
|
IDragColumnChangePayload,
|
||||||
|
IDragEnterHeaderPayload,
|
||||||
|
IDragMoveHeaderPayload,
|
||||||
|
IDragLeaveHeaderPayload
|
||||||
} from '../types/DragTypes';
|
} from '../types/DragTypes';
|
||||||
|
|
||||||
interface DragState {
|
interface DragState {
|
||||||
|
|
@ -39,6 +42,7 @@ export class DragDropManager {
|
||||||
private pendingElement: HTMLElement | null = null;
|
private pendingElement: HTMLElement | null = null;
|
||||||
private pendingMouseOffset: IMousePosition | null = null;
|
private pendingMouseOffset: IMousePosition | null = null;
|
||||||
private container: HTMLElement | null = null;
|
private container: HTMLElement | null = null;
|
||||||
|
private inHeader = false;
|
||||||
|
|
||||||
private readonly DRAG_THRESHOLD = 5;
|
private readonly DRAG_THRESHOLD = 5;
|
||||||
private readonly INTERPOLATION_FACTOR = 0.3;
|
private readonly INTERPOLATION_FACTOR = 0.3;
|
||||||
|
|
@ -159,6 +163,7 @@ export class DragDropManager {
|
||||||
// Cleanup
|
// Cleanup
|
||||||
this.dragState.element.classList.remove('dragging');
|
this.dragState.element.classList.remove('dragging');
|
||||||
this.dragState = null;
|
this.dragState = null;
|
||||||
|
this.inHeader = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
private initializeDrag(element: HTMLElement, mouseOffset: IMousePosition, e: PointerEvent): void {
|
private initializeDrag(element: HTMLElement, mouseOffset: IMousePosition, e: PointerEvent): void {
|
||||||
|
|
@ -217,6 +222,12 @@ export class DragDropManager {
|
||||||
private updateDragTarget(e: PointerEvent): void {
|
private updateDragTarget(e: PointerEvent): void {
|
||||||
if (!this.dragState) return;
|
if (!this.dragState) return;
|
||||||
|
|
||||||
|
// Check header zone first
|
||||||
|
this.checkHeaderZone(e);
|
||||||
|
|
||||||
|
// Skip normal grid handling if in header
|
||||||
|
if (this.inHeader) return;
|
||||||
|
|
||||||
// Check for column change
|
// Check for column change
|
||||||
const columnAtPoint = this.getColumnAtPoint(e.clientX);
|
const columnAtPoint = this.getColumnAtPoint(e.clientX);
|
||||||
if (columnAtPoint && columnAtPoint !== this.dragState.currentColumn) {
|
if (columnAtPoint && columnAtPoint !== this.dragState.currentColumn) {
|
||||||
|
|
@ -244,6 +255,74 @@ export class DragDropManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if pointer is in header zone and emit appropriate events
|
||||||
|
*/
|
||||||
|
private checkHeaderZone(e: PointerEvent): void {
|
||||||
|
if (!this.dragState) return;
|
||||||
|
|
||||||
|
const headerViewport = document.querySelector('swp-header-viewport');
|
||||||
|
if (!headerViewport) return;
|
||||||
|
|
||||||
|
const rect = headerViewport.getBoundingClientRect();
|
||||||
|
const isInHeader = e.clientY < rect.bottom;
|
||||||
|
|
||||||
|
if (isInHeader && !this.inHeader) {
|
||||||
|
// Entered header
|
||||||
|
this.inHeader = true;
|
||||||
|
|
||||||
|
const payload: IDragEnterHeaderPayload = {
|
||||||
|
eventId: this.dragState.eventId,
|
||||||
|
element: this.dragState.element,
|
||||||
|
sourceColumnIndex: this.getColumnIndex(this.dragState.columnElement),
|
||||||
|
sourceDate: this.dragState.columnElement.dataset.date || '',
|
||||||
|
title: this.dragState.element.querySelector('swp-event-title')?.textContent || '',
|
||||||
|
colorClass: [...this.dragState.element.classList].find(c => c.startsWith('is-')),
|
||||||
|
itemType: 'event',
|
||||||
|
duration: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventBus.emit(CoreEvents.EVENT_DRAG_ENTER_HEADER, payload);
|
||||||
|
} else if (!isInHeader && this.inHeader) {
|
||||||
|
// Left header
|
||||||
|
this.inHeader = false;
|
||||||
|
|
||||||
|
const payload: IDragLeaveHeaderPayload = {
|
||||||
|
eventId: this.dragState.eventId
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventBus.emit(CoreEvents.EVENT_DRAG_LEAVE_HEADER, payload);
|
||||||
|
} else if (isInHeader) {
|
||||||
|
// Moving within header
|
||||||
|
const column = this.getColumnAtX(e.clientX);
|
||||||
|
if (column) {
|
||||||
|
const payload: IDragMoveHeaderPayload = {
|
||||||
|
eventId: this.dragState.eventId,
|
||||||
|
columnIndex: this.getColumnIndex(column),
|
||||||
|
dateKey: column.dataset.date || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventBus.emit(CoreEvents.EVENT_DRAG_MOVE_HEADER, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get column index (0-based) for a column element
|
||||||
|
*/
|
||||||
|
private getColumnIndex(column: HTMLElement): number {
|
||||||
|
if (!this.container) return 0;
|
||||||
|
const columns = Array.from(this.container.querySelectorAll('swp-day-column'));
|
||||||
|
return columns.indexOf(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get column at X coordinate (alias for getColumnAtPoint)
|
||||||
|
*/
|
||||||
|
private getColumnAtX(clientX: number): HTMLElement | null {
|
||||||
|
return this.getColumnAtPoint(clientX);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find column element at given X coordinate
|
* Find column element at given X coordinate
|
||||||
*/
|
*/
|
||||||
|
|
@ -323,5 +402,6 @@ export class DragDropManager {
|
||||||
this.eventBus.emit(CoreEvents.EVENT_DRAG_CANCEL, payload);
|
this.eventBus.emit(CoreEvents.EVENT_DRAG_CANCEL, payload);
|
||||||
|
|
||||||
this.dragState = null;
|
this.dragState = null;
|
||||||
|
this.inHeader = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,3 +45,25 @@ export interface IDragColumnChangePayload {
|
||||||
newColumn: HTMLElement;
|
newColumn: HTMLElement;
|
||||||
currentY: number;
|
currentY: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Header drag payloads
|
||||||
|
export interface IDragEnterHeaderPayload {
|
||||||
|
eventId: string;
|
||||||
|
element: HTMLElement; // Original dragged element
|
||||||
|
sourceColumnIndex: number;
|
||||||
|
sourceDate: string;
|
||||||
|
title: string;
|
||||||
|
colorClass?: string;
|
||||||
|
itemType: 'event' | 'reminder';
|
||||||
|
duration: number; // Antal dage (default 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDragMoveHeaderPayload {
|
||||||
|
eventId: string;
|
||||||
|
columnIndex: number;
|
||||||
|
dateKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDragLeaveHeaderPayload {
|
||||||
|
eventId: string;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -346,3 +346,38 @@ swp-allday-container swp-event.transitioning {
|
||||||
.is-amber { --b-primary: var(--b-color-amber); }
|
.is-amber { --b-primary: var(--b-color-amber); }
|
||||||
.is-orange { --b-primary: var(--b-color-orange); }
|
.is-orange { --b-primary: var(--b-color-orange); }
|
||||||
.is-deep-orange { --b-primary: var(--b-color-deep-orange); }
|
.is-deep-orange { --b-primary: var(--b-color-deep-orange); }
|
||||||
|
|
||||||
|
/* Header drawer items */
|
||||||
|
swp-header-item {
|
||||||
|
--b-text: var(--color-text);
|
||||||
|
|
||||||
|
/* Positioneres via style.gridArea */
|
||||||
|
height: 22px;
|
||||||
|
margin: 2px 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: background-color 200ms ease;
|
||||||
|
|
||||||
|
/* Color system fra swp-event */
|
||||||
|
background-color: color-mix(in srgb, var(--b-primary) 10%, var(--b-mix));
|
||||||
|
color: var(--b-text);
|
||||||
|
border-left: 4px solid var(--b-primary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: color-mix(in srgb, var(--b-primary) 15%, var(--b-mix));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dragging state */
|
||||||
|
&.dragging {
|
||||||
|
opacity: 0.7;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,8 +73,12 @@ swp-header-spacer {
|
||||||
}
|
}
|
||||||
|
|
||||||
swp-header-drawer {
|
swp-header-drawer {
|
||||||
display: block;
|
display: grid;
|
||||||
height: 0;
|
grid-template-columns: repeat(var(--grid-columns), minmax(var(--day-column-min-width), 1fr));
|
||||||
|
min-width: calc(var(--grid-columns) * var(--day-column-min-width));
|
||||||
|
grid-auto-rows: 28px;
|
||||||
|
gap: 2px 0;
|
||||||
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--color-background-alt);
|
background: var(--color-background-alt);
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue