@ninthforge/scheduler 0.0.0-stage → 1.0.0-rc.1

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.
@@ -0,0 +1,585 @@
1
+ import * as React from 'react';
2
+
3
+ /**
4
+ * One action offered in a component's empty state, e.g. "Load sample data" on a Ninth Forge
5
+ * sample table. The host decides when to offer it and runs it; the component only shows it.
6
+ */
7
+ interface EmptyAction {
8
+ label: string;
9
+ onClick: () => void;
10
+ /** Running: the button gives way to `status` ("Loading sample data… 120 of 380"). */
11
+ busy?: boolean;
12
+ status?: string;
13
+ /** Shown under the button, e.g. what the action does. */
14
+ hint?: string;
15
+ /** The last attempt failed (the button stays, so the user can retry). */
16
+ error?: string;
17
+ }
18
+
19
+ /** The host props every Plan component takes for its watermark. */
20
+ interface WatermarkHostProps {
21
+ /**
22
+ * `false` draws no watermark at all (no link, no space). The licence is still checked; the
23
+ * host decides when, e.g. Power BI Desktop for authoring (DECISIONS 2026-10-31). Default true.
24
+ */
25
+ watermark?: boolean;
26
+ /** `text`: words only, no coloured mark (Power BI certification). Default `mark`. */
27
+ watermarkStyle?: 'mark' | 'text';
28
+ /** The host opens the link itself (Power BI's `launchUrl`): the watermark becomes a button. */
29
+ onWatermarkClick?: () => void;
30
+ }
31
+
32
+ /** Which days count as working days. `mon-fri` skips Saturdays and Sundays; `all` counts every day. */
33
+ type WorkingWeek = 'mon-fri' | 'all';
34
+
35
+ /**
36
+ * Time zones for instant (UTC millisecond) maths.
37
+ *
38
+ * A `Zone` answers one question: at this instant, how many minutes **east of UTC** is the
39
+ * wall clock? Perth is +480, New York −300 (winter) / −240 (summer). Everything that turns
40
+ * instants into dates, hours or pixels goes through a Zone — never through the machine's
41
+ * local getters, which are wrong whenever the user's Dataverse zone differs from the browser's.
42
+ */
43
+ type Zone = (utcMs: number) => number;
44
+ /** No offset, ever. Also the right zone for time-zone-independent ("floating") values. */
45
+ declare const UTC: Zone;
46
+ declare function fixedZone(minutesEast: number): Zone;
47
+ /** The browser's own zone. Only for demos; in Power Apps use `pcfZone`. */
48
+ declare function browserZone(): Zone;
49
+ /** An IANA zone such as `Europe/London`, via `Intl`. Used by the demo and the tests. */
50
+ declare function ianaZone(name: string): Zone;
51
+ /**
52
+ * The Power Apps user's zone, from `context.userSettings.getTimeZoneOffsetMinutes(date)`.
53
+ *
54
+ * Measured in Dev (DECISIONS 2026-10-25): it returns minutes **east** of UTC (Perth +480,
55
+ * Pacific −480 in January, −420 in July), so `sign` defaults to +1. It must always be given a
56
+ * date: without one the PCF API returned the standard offset during daylight time.
57
+ */
58
+ declare function pcfZone(getOffset: (date: Date) => number, sign?: 1 | -1): Zone;
59
+
60
+ /**
61
+ * Host hooks shared by the Plan components (Gantt, Timeline, capacity heatmap) when a host such
62
+ * as a Power BI visual wraps them. Same shapes as `@ninthforge/charts`' hooks. All optional:
63
+ * Power Apps passes none of them.
64
+ */
65
+ /** Passed with a selection: `multi` is true when Ctrl or Cmd was held (add to the selection). */
66
+ interface SelectInfo {
67
+ multi: boolean;
68
+ }
69
+ /** Where the pointer (or, from the keyboard, the item's anchor) is, in client coordinates. */
70
+ interface PointerInfo extends SelectInfo {
71
+ clientX: number;
72
+ clientY: number;
73
+ }
74
+ /** A host formatter, e.g. one built from a Power BI format string. `C` says what is formatted. */
75
+ type HostFormat<C> = (value: number, ctx: C) => string;
76
+
77
+ /**
78
+ * "Copy diagnostics": one click copies what support needs to reproduce a problem, and nothing
79
+ * else. Product and control version, licence state (never the key), host, browser, locale, time
80
+ * zone, the mapped property names and the row count. No data values and no personal data: the
81
+ * input types only take names and counts, and every string is trimmed to one short line.
82
+ *
83
+ * Framework-free. The drop-in menu item is `CopyDiagnosticsItem` in `@ninthforge/core/react`.
84
+ */
85
+ /** Where the control runs. PCF wrappers and Power BI visuals pass it when they know. */
86
+ type DiagnosticsHost = 'model-driven' | 'canvas' | 'canvas (Studio)' | 'custom page' | 'Power BI' | 'code app' | 'web page';
87
+ /**
88
+ * The licence as diagnostics reads it: a `LicenseResult` from `@ninthforge/license` fits. Only
89
+ * `status`, `payload.tier`, `payload.org` and `payload.exp` are read; the key itself never
90
+ * reaches this function, and the tenant ID and key id are left out.
91
+ */
92
+ interface DiagnosticsLicence {
93
+ status: string;
94
+ payload?: {
95
+ tier?: string;
96
+ org?: string;
97
+ exp?: string;
98
+ } | null;
99
+ }
100
+ interface DiagnosticsInput {
101
+ /** The product, e.g. `Plan`, `Charts`. */
102
+ product: string;
103
+ /** The control, e.g. `Gantt` or `nf_NinthForge.PlanGantt`. */
104
+ control: string;
105
+ /** The control or package version, e.g. `0.4.4`. */
106
+ version: string;
107
+ licence?: DiagnosticsLicence | null;
108
+ /** Overrides detection (Power BI visuals, code apps, or a PCF that knows its host). */
109
+ host?: DiagnosticsHost;
110
+ /**
111
+ * Mapped properties: `{ taskStart: 'nf_start' }` gives `taskStart → nf_start`, and a list
112
+ * gives the names alone. Names only, never values.
113
+ */
114
+ properties?: Readonly<Record<string, string | null | undefined>> | readonly string[];
115
+ /** Rows the control received. */
116
+ rows?: number;
117
+ /**
118
+ * A few product-specific settings, e.g. `{ 'Save mode': 'direct' }`. Settings only: never
119
+ * record values, user names or e-mail addresses.
120
+ */
121
+ extra?: Readonly<Record<string, string | number | boolean | null | undefined>>;
122
+ now?: Date;
123
+ }
124
+ /**
125
+ * What a host adds to a component's own diagnostics (the component fills product, version,
126
+ * licence and its row count): a PCF passes its control name, mapped properties and settings.
127
+ */
128
+ type DiagnosticsHostInfo = Partial<Pick<DiagnosticsInput, 'control' | 'version' | 'host' | 'properties' | 'rows' | 'extra'>>;
129
+
130
+ /**
131
+ * Text size: type roles and one scale for every component (the same roles in Power Apps and Power BI).
132
+ *
133
+ * Components size text by role, never by a pixel number, so one setting ("Text size", 75–200 %)
134
+ * makes every label, value and title bigger or smaller together. The same numbers reach CSS
135
+ * (`--nf-text-<role>`) and layout maths (`textSize(role, scale)`), so label fitting and row
136
+ * heights match what the browser draws.
137
+ */
138
+ /** What the text is for, not how big it is. */
139
+ type TextRole = 'caption' | 'body' | 'label' | 'value' | 'callout' | 'header' | 'title' | 'hero';
140
+ /** Per-role sizes at 100 %, in CSS pixels, that replace the defaults. The scale still applies. */
141
+ type TextSizes = Partial<Record<TextRole, number>>;
142
+
143
+ /** A component theme. Every key becomes a `--nf-<kebab-key>` CSS variable. */
144
+ interface ComponentTheme {
145
+ accent: string;
146
+ accentTint: string;
147
+ accentText: string;
148
+ surface: string;
149
+ surfaceAlt: string;
150
+ ink: string;
151
+ muted: string;
152
+ line: string;
153
+ lineSoft: string;
154
+ phase: string;
155
+ milestone: string;
156
+ baseline: string;
157
+ selected: string;
158
+ focus: string;
159
+ positive: string;
160
+ negative: string;
161
+ tooltipBg: string;
162
+ tooltipInk: string;
163
+ font: string;
164
+ monoFont: string;
165
+ }
166
+
167
+ /**
168
+ * Day: one day of hours. Week, Two weeks and Month: days across, bookings as bars within each
169
+ * day (Month is the calendar month). Agenda: a list by day, then resource, for 7 days from the
170
+ * date; the default below 600 px wide.
171
+ */
172
+ type SchedulerView = 'day' | 'week' | 'twoWeeks' | 'month' | 'agenda';
173
+ /** Scenario defaults (docs/specs/scheduler.md, "Scenarios"). `custom` sets nothing. */
174
+ type SchedulerPreset = 'people' | 'rooms' | 'vehicles' | 'equipment' | 'jobs' | 'custom';
175
+ /** What happens when a booking overlaps another on the same resource. */
176
+ type ConflictMode = 'allow' | 'warn' | 'block';
177
+ interface SchedResource {
178
+ id: string;
179
+ name: string;
180
+ /** Group by this (team, building, depot, category, area). */
181
+ group?: string;
182
+ /** Second line: role, rego, asset id. */
183
+ subtitle?: string;
184
+ /** Hours per day for utilisation (default from the preset, else 8). */
185
+ capacityHours?: number;
186
+ /** Rooms: seats, checked against booking attendees. */
187
+ seats?: number;
188
+ /** Working hours as decimal wall hours (9.5) or "09:30". Default from the preset. */
189
+ workStart?: number | string;
190
+ workEnd?: number | string;
191
+ /** Hex colour for the row marker. */
192
+ color?: string;
193
+ /** Image URL for the avatar (no network fetch by the component; the browser loads it). */
194
+ image?: string;
195
+ /** Sort order within the group; then by name. */
196
+ sort?: number;
197
+ }
198
+ interface SchedBooking {
199
+ id: string;
200
+ name: string;
201
+ /** An instant: UTC ms, or an ISO date-time with offset/Z. */
202
+ start: number | string;
203
+ end: number | string;
204
+ /** Empty = unassigned (shown in the Unassigned panel). */
205
+ resourceId: string | null;
206
+ /** Drives colour and the legend (e.g. Tentative / Confirmed / Done / Cancelled). */
207
+ status?: string;
208
+ /** Hex colour; overrides the status colour. */
209
+ color?: string;
210
+ allDay?: boolean;
211
+ subtitle?: string;
212
+ /** `unavailable` = leave, maintenance, closed: drawn hatched, counts against capacity. */
213
+ kind?: 'booking' | 'unavailable';
214
+ attendees?: number;
215
+ location?: string;
216
+ /** No editing for this booking (e.g. no write access). */
217
+ readOnly?: boolean;
218
+ }
219
+ interface SchedulerLabels {
220
+ /** "Person", "Room", "Vehicle", "Asset", "Technician", "Resource". */
221
+ resource: string;
222
+ resources: string;
223
+ /** "Shift", "Booking", "Hire", "Job". */
224
+ booking: string;
225
+ bookings: string;
226
+ /** What `group` means, for the Group by menu: "Team", "Building", "Depot", "Category", "Area". */
227
+ group: string;
228
+ }
229
+ /** The editable fields of a booking. Times in changes are ISO strings in UTC (`…Z`). */
230
+ interface SchedFields {
231
+ start: string;
232
+ end: string;
233
+ resourceId: string | null;
234
+ }
235
+ /**
236
+ * One edit to one booking: the new values of the fields that changed, the old values of the
237
+ * same fields, and what caused it. Hosts save it (return a promise to keep it optimistic;
238
+ * reject to put it back).
239
+ */
240
+ interface SchedChange extends Partial<SchedFields> {
241
+ id: string;
242
+ previous: Partial<SchedFields>;
243
+ source: 'drag' | 'keyboard' | 'assign' | 'undo' | 'redo';
244
+ }
245
+ /** Where the user asked for a new booking (drag across empty time, or Insert). */
246
+ interface CreateRequest {
247
+ resourceId: string;
248
+ /** ISO UTC. */
249
+ start: string;
250
+ end: string;
251
+ }
252
+ /** What users may change. `true`/omitted = everything the handlers allow. */
253
+ type SchedEditable = boolean | {
254
+ move?: boolean;
255
+ resize?: boolean;
256
+ create?: boolean;
257
+ assign?: boolean;
258
+ };
259
+ interface SchedulerProps extends WatermarkHostProps {
260
+ resources: SchedResource[];
261
+ bookings: SchedBooking[];
262
+ /** The host is still loading data: the empty state says so, and the board is marked busy. */
263
+ loading?: boolean;
264
+ /** Scenario defaults for labels, view, snapping, conflict mode and working hours. Default `custom`. */
265
+ preset?: SchedulerPreset;
266
+ title?: string;
267
+ /** Controlled view, or `defaultView` (preset default otherwise). */
268
+ view?: SchedulerView;
269
+ defaultView?: SchedulerView;
270
+ onViewChange?: (view: SchedulerView) => void;
271
+ /** The date shown (ISO `YYYY-MM-DD` in the scheduler's zone), controlled or default. Default: today. */
272
+ date?: string;
273
+ defaultDate?: string;
274
+ onDateChange?: (date: string) => void;
275
+ /**
276
+ * The instants on screen changed (view, date or working-hours toggle): `[start, end)` in UTC ms.
277
+ * Hosts load just this window (the PCF filters its dataset; canvas apps filter `Items`).
278
+ */
279
+ onRangeChange?: (range: {
280
+ start: number;
281
+ end: number;
282
+ }) => void;
283
+ /** Zone all times are shown in. Default: the browser's. In Power Apps the PCF passes the user's Dataverse zone. */
284
+ timeZone?: Zone;
285
+ /** "Now" for the now line (UTC ms). Default: the clock. Tests and demos pin it. */
286
+ now?: number;
287
+ workingWeek?: WorkingWeek;
288
+ /** Day view: show only working hours (default true; the toolbar toggles it). */
289
+ hideNonWorking?: boolean;
290
+ /** Snap for drags (minutes); default from the preset. */
291
+ snapMinutes?: number;
292
+ conflictMode?: ConflictMode;
293
+ /**
294
+ * Group resources by `group` (default true when any resource has one). Controlled when set;
295
+ * the toolbar's Group by menu reports changes through `onGroupByChange`.
296
+ */
297
+ groupBy?: boolean;
298
+ onGroupByChange?: (groupBy: boolean) => void;
299
+ /** Override the preset's words. */
300
+ labels?: Partial<SchedulerLabels>;
301
+ /** Status → hex colour. Unknown statuses get palette colours in order of appearance. */
302
+ statusColors?: Record<string, string>;
303
+ theme?: 'light' | 'dark' | Partial<ComponentTheme>;
304
+ accent?: string;
305
+ /** Ninth Forge licence key (a Plan key). Missing keys show a small watermark; nothing breaks. */
306
+ /** An action offered when there's nothing to show, e.g. "Load sample data" (PCF on a Ninth Forge sample table). */
307
+ emptyAction?: EmptyAction;
308
+ /** One or more keys; or a promise of them (the PCF reading its licence-key environment variable). */
309
+ licenseKey?: string | PromiseLike<string | undefined>;
310
+ /** Host tenant (PCF: context.orgSettings.tenantId). */
311
+ tenantId?: string;
312
+ /**
313
+ * Show the small "renew within N days" notice while the licence is in its 14-day grace
314
+ * period. The licensing spec shows it to makers only: the PCF passes `true` only for makers
315
+ * (Studio in canvas, an admin or customiser in model-driven). Default `true`; turn it off in a
316
+ * code app your end users see.
317
+ */
318
+ renewalNotice?: boolean;
319
+ /**
320
+ * Text size, 1 = 100 % (0.75–2). Every label, value and title scales together; rows, lanes
321
+ * and the resource column grow with it. The PCF's "Text size" input (percent) sets it.
322
+ */
323
+ textScale?: number;
324
+ /** Per-role sizes at 100 % in px, e.g. `{ body: 14 }`; the scale still applies. */
325
+ textSizes?: TextSizes;
326
+ /**
327
+ * `false` hides the Export menu (print, PNG and Copy diagnostics), e.g. in Power BI, whose
328
+ * sandbox blocks downloads, new windows and usually the clipboard. Default true.
329
+ */
330
+ exports?: boolean;
331
+ /**
332
+ * "Copy diagnostics" in the Export menu: copies what support needs (version, licence state,
333
+ * host, browser, locale, time zone, mapped property names, row count; never data values or
334
+ * the key). A function adds what the host knows, e.g. the PCF's control name and mapped
335
+ * properties. `false` hides the item. Default: shown.
336
+ */
337
+ diagnostics?: false | (() => DiagnosticsHostInfo);
338
+ height?: number | string;
339
+ className?: string;
340
+ /** Text direction (default: inherited). The resource column mirrors; time runs left to right. */
341
+ dir?: 'ltr' | 'rtl';
342
+ showToolbar?: boolean;
343
+ /** Show the Unassigned panel (default: preset; `jobs` shows it open). */
344
+ showUnassigned?: boolean;
345
+ /** Every edit (move, resize, assign, undo). Return a promise for an optimistic save. */
346
+ onBookingChange?: (change: SchedChange) => void | Promise<void>;
347
+ /** Drag across empty time on a row (or Insert): the host opens its form / creates the row. */
348
+ onCreate?: (request: CreateRequest) => void;
349
+ /** Switch editing off, or parts of it. Default: on wherever a handler is given. */
350
+ editable?: SchedEditable;
351
+ /** Selection (single booking). */
352
+ onSelect?: (id: string | null) => void;
353
+ /** Enter or double-click on a booking. */
354
+ onOpen?: (booking: SchedBooking) => void;
355
+ }
356
+
357
+ declare function Scheduler(props: SchedulerProps): React.JSX.Element;
358
+
359
+ interface PresetDefaults {
360
+ labels: SchedulerLabels;
361
+ view: SchedulerView;
362
+ snapMinutes: number;
363
+ conflictMode: ConflictMode;
364
+ /** Decimal wall hours. */
365
+ workStart: number;
366
+ workEnd: number;
367
+ /** What capacity means in the row header: hours booked, or seats (rooms). */
368
+ capacity: 'hours' | 'seats';
369
+ capacityHours: number;
370
+ /** Unassigned panel open by default. */
371
+ showUnassigned: boolean;
372
+ }
373
+ /**
374
+ * One engine, five scenarios (docs/specs/scheduler.md, "Scenarios", Matt 2026-10-25).
375
+ * A preset only sets defaults; every prop still overrides it.
376
+ */
377
+ declare const PRESETS: Record<SchedulerPreset, PresetDefaults>;
378
+ declare const VIEW_LABEL: Record<SchedulerView, string>;
379
+
380
+ interface Scenario {
381
+ resources: SchedResource[];
382
+ bookings: SchedBooking[];
383
+ }
384
+ /** Sample resources and bookings for a preset, ±3 weeks around `anchor` (ISO date), wall times in `zone`. */
385
+ declare function sampleScenario(preset: SchedulerPreset, anchor: string, zone: Zone): Scenario;
386
+ /**
387
+ * The scale target (docs/specs/scheduler.md, "Scale"): `resources` people in 10 teams, each with
388
+ * `perResource` bookings inside the two weeks from the Monday of `anchor`'s week (so a Two weeks
389
+ * view shows resources × perResource bookings), plus some overlaps, leave and unassigned jobs.
390
+ */
391
+ declare function scaleScenario(anchor: string, zone: Zone, resources?: number, perResource?: number): Scenario;
392
+
393
+ interface Item {
394
+ booking: SchedBooking;
395
+ id: string;
396
+ start: number;
397
+ /** Never before start; zero-length bookings get 15 minutes so they can be seen and grabbed. */
398
+ end: number;
399
+ resourceId: string | null;
400
+ kind: 'booking' | 'unavailable';
401
+ cancelled: boolean;
402
+ }
403
+ declare function normalise(bookings: readonly SchedBooking[]): Item[];
404
+ /** First day shown and how many: Day = that day; Week / Two weeks start on the Monday. */
405
+ declare function viewRange(date: string, view: SchedulerView): {
406
+ startDay: number;
407
+ days: number;
408
+ };
409
+ /**
410
+ * Stack overlapping items into lanes (greedy, by start then longest first): each item gets the
411
+ * first lane free at its start. Returns lane per item id and how many lanes the row needs.
412
+ */
413
+ declare function assignLanes(items: readonly Item[]): {
414
+ lane: Map<string, number>;
415
+ lanes: number;
416
+ };
417
+ /** Ids of items that overlap another item on the same resource. Cancelled bookings never conflict. */
418
+ declare function conflicts(items: readonly Item[]): Set<string>;
419
+ interface GroupRow {
420
+ kind: 'group';
421
+ key: string;
422
+ label: string;
423
+ count: number;
424
+ collapsed: boolean;
425
+ top: number;
426
+ height: number;
427
+ }
428
+ interface ResourceRow {
429
+ kind: 'resource';
430
+ key: string;
431
+ resource: SchedResource;
432
+ /** Items overlapping the visible range, with their lanes. */
433
+ items: Item[];
434
+ lane: Map<string, number>;
435
+ lanes: number;
436
+ conflicts: Set<string>;
437
+ top: number;
438
+ height: number;
439
+ /** Position among resource rows (for aria-rowindex and keyboard). */
440
+ index: number;
441
+ }
442
+ type Row = GroupRow | ResourceRow;
443
+ /**
444
+ * Rows for the board: optional group headers, then resources with their bookings in the
445
+ * range [from, to). Heights grow with the number of overlapping lanes.
446
+ */
447
+ declare function buildRows(resources: readonly SchedResource[], items: readonly Item[], opts: {
448
+ from: number;
449
+ to: number;
450
+ groupBy: boolean;
451
+ collapsed?: ReadonlySet<string>;
452
+ filter?: string;
453
+ textScale?: number;
454
+ }): Row[];
455
+ /** Today's ISO date in a zone. */
456
+ declare function todayIn(zone: Zone, now?: number): string;
457
+
458
+ /**
459
+ * NF Capacity: utilisation per resource (or team) per week, for a heatmap. Hosts that already
460
+ * have totals (a Power BI model) pass `CapacityRow`s straight to `<CapacityHeatmap>`; Power Apps
461
+ * and code apps can build them from bookings with `weeklyUtilisation`.
462
+ */
463
+ interface CapacityCell {
464
+ /** The week's Monday, ISO `YYYY-MM-DD`. */
465
+ week: string;
466
+ /** Hours booked in working time. */
467
+ booked: number;
468
+ /** Hours available that week. */
469
+ capacity: number;
470
+ }
471
+ interface CapacityRow {
472
+ id: string;
473
+ /** A resource or a team. */
474
+ name: string;
475
+ group?: string;
476
+ cells: CapacityCell[];
477
+ }
478
+ /** The five steps on the board: under 60%, 60–79%, 80–94%, 95–100%, over 100%. */
479
+ type CapacityStep = 0 | 1 | 2 | 3 | 4;
480
+ declare const STEP_LABELS: readonly ["under 60%", "60–79%", "80–94%", "95–100%", "over 100%"];
481
+ /** Utilisation as a whole percentage, or `null` when nothing is available (capacity 0). */
482
+ declare function utilisationPct(c: Pick<CapacityCell, 'booked' | 'capacity'>): number | null;
483
+ declare function capacityStep(c: Pick<CapacityCell, 'booked' | 'capacity'>): CapacityStep;
484
+ /** The key a cell is selected and highlighted by: row id + U+001F + week. A row id alone means the whole row. */
485
+ declare function cellKey(rowId: string, week: string): string;
486
+ /** `count` Mondays from the week containing `fromIso`. */
487
+ declare function weekStarts(fromIso: string, count: number): string[];
488
+ interface WeeklyOptions {
489
+ zone: Zone;
490
+ /** `mon-fri` (default): capacity counts five days a week. */
491
+ week?: WorkingWeek;
492
+ /** Defaults for resources that don't set their own (decimal hours or "09:00"). */
493
+ workStart?: number | string;
494
+ workEnd?: number | string;
495
+ /** Hours per day (default 8). */
496
+ capacityHours?: number;
497
+ }
498
+ /**
499
+ * Booked vs available hours per resource per week, using the Scheduler's capacity rules:
500
+ * bookings clipped to each working day, cancelled ones ignored, leave counting up to a full day.
501
+ */
502
+ declare function weeklyUtilisation(resources: readonly SchedResource[], bookings: readonly SchedBooking[], weeks: readonly string[], o: WeeklyOptions): CapacityRow[];
503
+ /** Rows summed by `group` (resources → teams), in first-seen order. Rows without a group keep their name. */
504
+ declare function rollUp(rows: readonly CapacityRow[]): CapacityRow[];
505
+ /** Every week that appears in the rows, sorted. */
506
+ declare function weeksOf(rows: readonly CapacityRow[]): string[];
507
+ /** A week in full, for tooltips and screen readers: "16 Nov 2026". */
508
+ declare function weekName(week: string): string;
509
+ /** Column labels like the board: "2 Nov", then "9", "16", …, and the month again when it changes. */
510
+ declare function weekLabels(weeks: readonly string[]): string[];
511
+ /**
512
+ * A message title naming who is overbooked: "Front-end squad overbooked in Nov", with "and 2
513
+ * others" when more rows are over, or "No one over capacity".
514
+ */
515
+ declare function capacityHeadline(rows: readonly CapacityRow[], noun?: string): string;
516
+ interface StepColor {
517
+ background: string;
518
+ text: string;
519
+ }
520
+ /**
521
+ * The five step colours from the accent: three tints, a deep shade at 95–100%, and the Negative
522
+ * token over 100%. Text on each reaches 4.5:1. `override` (a host's five colours) wins.
523
+ */
524
+ declare function stepColors(accent: string, surface: string, override?: readonly string[]): StepColor[];
525
+
526
+ interface CapacityFormatContext {
527
+ field: 'utilisation' | 'hours';
528
+ use: 'label' | 'tooltip';
529
+ }
530
+ interface CapacityColors {
531
+ /** Five colours, lowest step first (under 60% … over 100%). */
532
+ steps: string[];
533
+ foreground: string;
534
+ background: string;
535
+ }
536
+ /** What a hook receives: the row, and the cell when the pointer is on one. */
537
+ interface CapacityTarget {
538
+ row: CapacityRow;
539
+ cell: CapacityCell;
540
+ /** `cellKey(row.id, cell.week)`. */
541
+ key: string;
542
+ }
543
+ interface CapacityHeatmapProps extends WatermarkHostProps {
544
+ /** Resources or teams, with booked and available hours per week (`weeklyUtilisation`, `rollUp`, or a host's totals). */
545
+ rows: CapacityRow[];
546
+ /** Column order. Default: every week in the rows, sorted. */
547
+ weeks?: string[];
548
+ /** Default: a message naming the most overbooked row ("Front-end squad overbooked in Nov"). */
549
+ title?: string;
550
+ /** Column labels. Default "2 Nov", "9", "16", … */
551
+ weekLabel?: (weekIso: string, index: number) => string;
552
+ /** Header over the names column. */
553
+ rowHeader?: string;
554
+ theme?: 'light' | 'dark' | Partial<ComponentTheme>;
555
+ accent?: string;
556
+ /** A Plan key (or several). Missing keys show a small watermark; nothing breaks. */
557
+ licenseKey?: string | PromiseLike<string | undefined>;
558
+ tenantId?: string;
559
+ height?: number | string;
560
+ className?: string;
561
+ dir?: 'ltr' | 'rtl';
562
+ /** Hide the legend (the host draws its own). */
563
+ showLegend?: boolean;
564
+ /** `none`: no built-in tooltip (the host shows its own from `onHover`). */
565
+ tooltip?: 'built-in' | 'none';
566
+ onHover?: (target: CapacityTarget | null, at: PointerInfo) => void;
567
+ /** Click, Enter or Space on a cell. `null` when Escape clears. */
568
+ onSelect?: (target: CapacityTarget | null, info: SelectInfo) => void;
569
+ /** Cell keys (`cellKey`) or row ids kept at full strength; the rest dim. */
570
+ highlighted?: readonly string[];
571
+ onContextMenu?: (target: CapacityTarget, at: PointerInfo) => void;
572
+ formatValue?: HostFormat<CapacityFormatContext>;
573
+ colors?: Partial<CapacityColors>;
574
+ animate?: boolean;
575
+ }
576
+ /**
577
+ * NF Capacity: who is booked how much, week by week. A resource (or team) × week heatmap with
578
+ * % labels, five steps from the accent, and over 100% in the Negative colour.
579
+ */
580
+ declare function CapacityHeatmap(props: CapacityHeatmapProps): React.JSX.Element;
581
+
582
+ /** This package's version, for Copy diagnostics. `version.test.ts` keeps it equal to package.json. */
583
+ declare const VERSION = "1.0.0-rc.1";
584
+
585
+ export { type CapacityCell, type CapacityColors, type CapacityFormatContext, CapacityHeatmap, type CapacityHeatmapProps, type CapacityRow, type CapacityStep, type CapacityTarget, type ConflictMode, type CreateRequest, PRESETS, type PresetDefaults, STEP_LABELS, type Scenario, type SchedBooking, type SchedChange, type SchedEditable, type SchedFields, type SchedResource, Scheduler, type SchedulerLabels, type SchedulerPreset, type SchedulerProps, type SchedulerView, UTC, VERSION, VIEW_LABEL, type WeeklyOptions, type Zone, assignLanes, browserZone, buildRows, capacityHeadline, capacityStep, cellKey, conflicts, fixedZone, ianaZone, normalise, pcfZone, rollUp, sampleScenario, scaleScenario, stepColors, todayIn, utilisationPct, viewRange, weekLabels, weekName, weekStarts, weeklyUtilisation, weeksOf };