diff --git a/.workbench/inspiration.md b/.workbench/inspiration.md new file mode 100644 index 0000000..ffcae70 --- /dev/null +++ b/.workbench/inspiration.md @@ -0,0 +1,266 @@ +Selvfølgelig—her er en **opdateret, selvstændig `.md`-spec**, som **understøtter variable antal resources per team**, dynamisk kolonnebredde, ingen inline layout-styles, pipeline‐rendering i grupper, og CSS-controlling via custom properties. + +Kopier → gem som fx: +`grid-render-pipeline-dynamic-columns.md` + +--- + +````md +# Grid Render Pipeline — Dynamic Columns Spec + +Denne specifikation beskriver en generisk render-pipeline til at bygge et +dynamisk CSS Grid layout, hvor hver "gruppe" (teams, resources, dates) har sin +egen renderer og pipeline-styring. Layoutet understøtter **variable antal +resources pr. team** og beregner automatisk antal kolonner. Ingen inline-styles +til positionering anvendes. + +--- + +## ✨ Formål + +- Ét globalt CSS Grid. +- Variabelt antal resources pr. team → dynamisk antal kolonner. +- CSS-grid auto-placerer rækker. +- Ingen inline styling af layout (ingen `element.style.gridRow = ...`). +- CSS custom properties bruges til at definere dynamiske spænder. +- Renderere har ens interface og bindes i pipeline. +- `pipeline.run(ctx)` executer alle renderers i rækkefølge. +- Hver renderer kan hente sin egen data (API, async osv.). + +--- + +## 🧩 Data Model + +```ts +type DateString = string; + +interface Resource { + id: string; + name: string; + dates: DateString[]; +} + +interface Team { + id: string; + name: string; + resources: Resource[]; +} +```` + +--- + +## 🧠 Context + +```ts +interface RenderContext { + grid: HTMLElement; // root grid container + teams: Team[]; // data +} +``` + +`grid` er HTML-elementet med `display:grid`, og `teams` er data. + +--- + +## 🎨 CSS Layout + +Grid kolonner bestemmes dynamisk via CSS variablen `--total-cols`. + +```css +.grid { + display: grid; + grid-template-columns: repeat(var(--total-cols), minmax(0, 1fr)); + gap: 6px 10px; +} + +.cell { + font-size: 0.9rem; +} +``` + +### Teams (øverste række) + +Hver team-header spænder **antal resources for team'et**: + +```css +.team-header { + grid-column: span var(--team-cols, 1); + font-weight: 700; + border-bottom: 1px solid #ccc; + padding: 4px 2px; +} +``` + +### Resources (2. række) + +```css +.resource-cell { + padding: 4px 2px; + background: #f5f5f5; + border-radius: 4px; + text-align: center; + font-weight: 600; +} +``` + +### Dates (3. række) + +```css +.dates-cell { padding: 2px 0; } + +.dates-list { + display: flex; + flex-wrap: wrap; + gap: 4px; + justify-content: center; +} + +.date-pill { + padding: 3px 6px; + background: #e3e3e3; + border-radius: 4px; + font-size: 0.8rem; +} +``` + +--- + +## 🔧 Beregning af kolonner + +**Total cols = sum(resources.length for all teams)** + +```ts +const totalCols = ctx.teams.reduce((sum, t) => sum + t.resources.length, 0); +ctx.grid.style.setProperty('--total-cols', totalCols.toString()); +``` + +For hvert team defineres hvor mange kolonner det spænder: + +```ts +cell.style.setProperty('--team-cols', team.resources.length.toString()); +``` + +> Bemærk: vi bruger **kun CSS vars** til layoutparametre – ikke inline +> grid-row/grid-column. + +--- + +## ⚙ Renderer Interface + +```ts +interface Renderer { + id: string; + next: Renderer | null; + render(ctx: RenderContext): void; +} +``` + +### Factory + +```ts +function createRenderer(id: string, fn: (ctx: RenderContext) => void): Renderer { + return { + id, + next: null, + render(ctx) { + fn(ctx); + if (this.next) this.next.render(ctx); + } + }; +} +``` + +--- + +## 🧱 De tre render-lag (grupper) + +### Teams + +* Appender én `.team-header` per team. +* Sætter `--team-cols`. + +### Resources + +* Appender én `.resource-cell` per resource. +* Foregår i teams-orden → CSS auto-row sørger for næste række. + +### Dates + +* Appender én `.dates-cell` per resource. +* Hver celle indeholder flere `.date-pill`. + +Append-rækkefølge giver 3 rækker automatisk: + +1. teams, 2) resources, 3) dates. + +--- + +## 🔗 Pipeline + +```ts +function buildPipeline(renderers: Renderer[]) { + for (let i = 0; i < renderers.length - 1; i++) { + renderers[i].next = renderers[i + 1]; + } + const first = renderers[0] ?? null; + return { + run(ctx: RenderContext) { + if (first) first.render(ctx); + } + }; +} +``` + +### Brug + +```ts +const pipeline = buildPipeline([ + teamsRenderer, + resourcesRenderer, + datesRenderer +]); + +pipeline.run(ctx); +``` + +--- + +## 🚀 Kørsel + +```ts +// 1) beregn total kolonner +const totalCols = ctx.teams.reduce((sum, t) => sum + t.resources.length, 0); +ctx.grid.style.setProperty('--total-cols', totalCols); + +// 2) pipeline +pipeline.run(ctx); +``` + +CSS klarer resten. + +--- + +## 🧽 Principper + +* **Ingen inline style-positionering**. +* **CSS Grid** owner layout. +* **JS** owner data & rækkefølge. +* **Renderers** er udskiftelige og genbrugelige. +* **Append i grupper** = rækker automatisk. +* **CSS vars** styrer spans dynamisk. + +--- + +## ✔ TL;DR + +* Grid-cols bestemmes ud fra data. +* Team-header `span = resources.length`. +* Append rækkefølge = rækker. +* Renderere i pipeline. +* Ingen koordinater, ingen inline layout-styles. + +``` + +--- + +``` diff --git a/.workbench/plan-comparison.md b/.workbench/plan-comparison.md new file mode 100644 index 0000000..1077a8e --- /dev/null +++ b/.workbench/plan-comparison.md @@ -0,0 +1,246 @@ +# Plan Sammenligning: Spec vs Min Plan + +## 1. Grid Container + +| Spec | Min Plan | Kommentar | +|------|----------|-----------| +| Ét grid (`ctx.grid`) | To containers (`headerContainer` + `columnContainer`) | **Afvigelse:** Vi har 2 containers for at understøtte header drawer og sticky headers. Spec'en bruger ét grid hvor append-rækkefølge = rækker. | + +**Spørgsmål:** Er 2 containers ok, eller skal vi følge spec'en med ét grid? + +--- + +## 2. RenderContext + +| Spec | Min Plan | +|------|----------| +| `{ grid: HTMLElement, teams: Team[] }` | `{ headerContainer: HTMLElement, columnContainer: HTMLElement }` | + +**Spec:** +```typescript +interface RenderContext { + grid: HTMLElement; + teams: Team[]; +} +``` + +**Min plan:** +```typescript +interface RenderContext { + headerContainer: HTMLElement; + columnContainer: HTMLElement; +} +``` + +**Kommentar:** Spec'en har `teams` data i context. Min plan har ingen data i context - renderers henter selv. Er det korrekt at fjerne data fra context? + +--- + +## 3. Data Model + +| Spec | Min Plan | +|------|----------| +| Nested: `team.resources[]`, `resource.dates[]` | Flad med id-relationer, renderers henter selv | + +**Spec:** +```typescript +interface Team { + id: string; + name: string; + resources: Resource[]; // nested +} +``` + +**Min plan:** +```typescript +// Hardcoded i renderer +private resourcesByTeam = { + 'team1': ['res1', 'res2'], // kun ids + 'team2': ['res3'] +}; +``` + +**Kommentar:** Spec'en har nested data. Min plan bruger id-relationer og renderers slår selv op. Begge dele virker - min plan er mere fleksibel for store datasets. + +--- + +## 4. Renderer Interface + +| Spec | Min Plan | +|------|----------| +| `render(ctx): void` | `render(ids, next, context): void` | + +**Spec:** +```typescript +interface Renderer { + id: string; + next: Renderer | null; + render(ctx: RenderContext): void; +} +``` + +**Min plan:** +```typescript +interface IGroupingRenderer { + readonly type: string; + count?(ids: string[], next: NextFunction): number; + render(ids: string[], next: NextFunction, context: RenderContext): void; +} +``` + +**Kommentar:** +- Spec'en: Renderer har `next` som property, kalder selv `this.next.render(ctx)` +- Min plan: `next` kommer som parameter, kalder `next.render(ids)` + +Min plan sender ids eksplicit. Spec'en bruger nested data så ids er unødvendige. + +--- + +## 5. Pipeline / Builder + +| Spec | Min Plan | +|------|----------| +| `buildPipeline()` linker `renderer.next` | `RenderBuilder` med `buildChain()` | + +**Spec:** +```typescript +function buildPipeline(renderers: Renderer[]) { + for (let i = 0; i < renderers.length - 1; i++) { + renderers[i].next = renderers[i + 1]; + } + return { run(ctx) { first.render(ctx); } }; +} +``` + +**Min plan:** +```typescript +class RenderBuilder { + add(renderer): this { ... } + build(startIds): void { ... } + private buildChain(index): NextFunction { ... } +} +``` + +**Kommentar:** Samme koncept, forskellig implementering. Spec'en muterer renderers (`next` property). Min plan bruger closures (functional chain). + +--- + +## 6. Colspan Beregning + +| Spec | Min Plan | +|------|----------| +| Beregnes før render: `team.resources.length` | Beregnes via `next.count()` | + +**Spec:** +```typescript +// I renderer +cell.style.setProperty('--team-cols', team.resources.length.toString()); +``` + +**Min plan:** +```typescript +// I renderer +const colspan = next.count(resourceIds); +cell.style.setProperty('--team-cols', String(colspan)); +``` + +**Kommentar:** Spec'en ved colspan direkte fra nested data. Min plan kalder `next.count()` rekursivt for at beregne. Resultat er det samme. + +--- + +## 7. CSS Custom Properties + +| Spec | Min Plan | +|------|----------| +| `--total-cols`, `--team-cols` | `--grid-columns`, `--team-cols` | + +**Kommentar:** Næsten identisk. Begge bruger CSS vars til dynamisk colspan. + +--- + +## 8. Append Rækkefølge + +| Spec | Min Plan | +|------|----------| +| Alle teams → alle resources → alle dates | Per team: resources → dates | + +**Spec flow:** +``` +TeamRenderer: append team1, team2, team3 headers +ResourceRenderer: append res1, res2, res3, res4 headers +DateRenderer: append alle dates +``` + +**Min plan flow:** +``` +TeamRenderer: + append team1 header → next.render(['res1','res2']) + ResourceRenderer: append res1 → next.render(dates) + append res2 → next.render(dates) + append team2 header → next.render(['res3']) + ResourceRenderer: append res3 → next.render(dates) +``` + +**Kommentar:** Dette er en **væsentlig forskel**. + +Spec'en renderer alle teams først, så alle resources, så alle dates - CSS grid auto-row placerer dem. + +Min plan renderer nested: team1 → team1's resources → team1's dates → team2 → osv. + +**Spørgsmål:** Hvilken approach foretrækker du? Spec'ens "lag for lag" eller min "nested traversal"? + +--- + +## 9. Hvem kalder next? + +| Spec | Min Plan | +|------|----------| +| Renderer kalder `this.next.render(ctx)` efter egen render | Renderer kalder `next.render(ids)` per item | + +**Spec:** +```typescript +render(ctx) { + // render alle teams + for (const team of ctx.teams) { ... } + // derefter kald next + if (this.next) this.next.render(ctx); +} +``` + +**Min plan:** +```typescript +render(ids, next, context) { + for (const id of ids) { + // render ét team + next.render(childIds); // kald next PER team + } +} +``` + +**Kommentar:** Spec'en kalder next ÉN gang efter alle items. Min plan kalder next PER item. Dette hænger sammen med punkt 8. + +--- + +## Opsummering af Afvigelser + +| # | Emne | Status | +|---|------|--------| +| 1 | 2 containers vs 1 grid | **Accepteret** (header drawer) | +| 2 | Data i context | **Afvigelse** - fjernet | +| 3 | Nested vs flad data | **Accepteret** (id-relationer) | +| 4 | next som parameter vs property | **Afvigelse** - funktionel | +| 5 | Pipeline implementation | Lignende | +| 6 | Colspan beregning | Lignende | +| 7 | CSS vars | Identisk | +| 8 | Render rækkefølge | **Væsentlig afvigelse** | +| 9 | Hvornår next kaldes | **Væsentlig afvigelse** | + +--- + +## Åbne Spørgsmål + +1. **Render rækkefølge:** Skal vi følge spec'ens "lag for lag" approach, eller er "nested traversal" ok? + +2. **Context data:** Spec'en har `teams` i context. Skal vi have noget data i context, eller er det ok at renderers henter selv? + +3. **2 containers:** Er det ok at beholde 2 containers for header drawer support? diff --git a/src/v2/core/CalendarOrchestrator.ts b/src/v2/core/CalendarOrchestrator.ts index b3ac610..4c4e818 100644 --- a/src/v2/core/CalendarOrchestrator.ts +++ b/src/v2/core/CalendarOrchestrator.ts @@ -2,7 +2,7 @@ import { from } from 'ts-linq-light'; import { ViewConfig } from './ViewConfig'; import { IGroupingRenderer, RenderContext } from './IGroupingRenderer'; import { IGroupingStore } from './IGroupingStore'; -import { RenderBuilder, RenderData } from './RenderBuilder'; +import { RenderBuilder } from './RenderBuilder'; import { EventRenderer } from '../features/event/EventRenderer'; export class CalendarOrchestrator { @@ -37,15 +37,8 @@ export class CalendarOrchestrator { const types = from(viewConfig.groupings).select(g => g.type).toArray(); headerContainer.dataset.levels = types.join(' '); - // Hent alt data - const data: RenderData = { - teams: this.getItems('team', viewConfig), - resources: this.getItems('resource', viewConfig), - dates: this.getItems('date', viewConfig) - }; - // Byg renderer chain - const builder = new RenderBuilder(context, data); + const builder = new RenderBuilder(context); for (const grouping of viewConfig.groupings) { const renderer = this.getRenderer(grouping.type); diff --git a/src/v2/core/IGroupingRenderer.ts b/src/v2/core/IGroupingRenderer.ts index 39a784a..c1ec8dd 100644 --- a/src/v2/core/IGroupingRenderer.ts +++ b/src/v2/core/IGroupingRenderer.ts @@ -1,5 +1,5 @@ import { IEnumerable } from 'ts-linq-light'; -import { NextFunction, RenderData } from './RenderBuilder'; +import { NextFunction } from './RenderBuilder'; export interface RenderContext { headerContainer: HTMLElement; @@ -10,7 +10,6 @@ export interface IGroupingRenderer { readonly type: string; render( items: IEnumerable, - data: RenderData, next: NextFunction, context: RenderContext ): void; diff --git a/src/v2/core/RenderBuilder.ts b/src/v2/core/RenderBuilder.ts index e847547..2162627 100644 --- a/src/v2/core/RenderBuilder.ts +++ b/src/v2/core/RenderBuilder.ts @@ -6,12 +6,6 @@ export interface NextFunction { render(items: IEnumerable): void; } -export interface RenderData { - teams?: IEnumerable; - resources?: IEnumerable; - dates?: IEnumerable; -} - interface RenderLevel { renderer: IGroupingRenderer; items: IEnumerable; @@ -20,10 +14,7 @@ interface RenderLevel { export class RenderBuilder { private levels: RenderLevel[] = []; - constructor( - private context: RenderContext, - private data: RenderData - ) {} + constructor(private context: RenderContext) {} add(renderer: IGroupingRenderer, items: IEnumerable): this { this.levels.push({ renderer, items }); @@ -66,7 +57,7 @@ export class RenderBuilder { return total || from(items).count(); }, render: (items) => { - level.renderer.render(items, this.data, nextChain, this.context); + level.renderer.render(items, nextChain, this.context); } }; }