@heyramzi/ui 2.13.0 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,19 +10,33 @@ export interface Smoothing {
10
10
  kind?: SeriesKind;
11
11
  }
12
12
 
13
+ /** Raw days, a trailing average, or a level read once a week. */
13
14
  export type SmoothMode = "raw" | "rolling" | "weekly";
14
15
 
16
+ export interface Smoothed<TData> {
17
+ points: TData[];
18
+ mode: SmoothMode;
19
+ /** How many days a rolling point averages: 7 up to a quarter, 28 past it. */
20
+ window: number;
21
+ }
22
+
15
23
  /** The tooltip's name for a drawn point, beside its date. */
16
- export const ROLLING_LABEL = "7-day average";
24
+ export const averageLabel = (days: number) => `${days}-day average`;
25
+ export const ROLLING_LABEL = averageLabel(7);
17
26
  export const weekOf = (start: string) => `Week of ${start}`;
27
+ export const averageTo = (end: string, days = 7) => `${averageLabel(days)} to ${end}`;
28
+ /** What a point the collector never measured reads as, where a number would claim a 0. */
29
+ export const NO_DATA = "No data";
18
30
 
19
31
  const DAY = 86_400_000;
20
- const WINDOW = 7;
32
+ const WEEK = 7;
33
+ /** A year at 7 days still drew one-day spikes as humps; 28 days keeps the growth and drops the spikes. */
34
+ const LONG_WINDOW = 28;
21
35
 
22
36
  // =========================
23
37
  // FUNCTIONS
24
38
  // =========================
25
- /** About 30 to 60 marks across a chart: raw to a week, a 7-day average to a quarter, whole weeks past it. */
39
+ /** About 30 to 60 marks across a chart: raw to a fortnight, a daily average to a quarter, weekly past it. */
26
40
  export function smoothMode(rangeDays: number): SmoothMode {
27
41
  if (rangeDays <= 14) return "raw";
28
42
  if (rangeDays <= 120) return "rolling";
@@ -33,6 +47,13 @@ function timeOf(value: unknown): number {
33
47
  return value instanceof Date ? value.getTime() : new Date(String(value)).getTime();
34
48
  }
35
49
 
50
+ /** Null, undefined or not a number: a day nobody measured. An absent row still counts as 0. */
51
+ function measured(value: unknown): number | null {
52
+ if (value == null) return null;
53
+ const n = Number(value);
54
+ return Number.isFinite(n) ? n : null;
55
+ }
56
+
36
57
  /** The bucket's start in the same form the series uses, so the axis and the formatter see no change. */
37
58
  function sameForm(sample: unknown, time: number): unknown {
38
59
  if (sample instanceof Date) return new Date(time);
@@ -40,66 +61,100 @@ function sameForm(sample: unknown, time: number): unknown {
40
61
  return String(sample).length === 10 ? iso.slice(0, 10) : iso;
41
62
  }
42
63
 
64
+ const daysBetween = (from: number, to: number) => Math.round((to - from) / DAY);
65
+
66
+ /**
67
+ * A trailing mean over `window` days, drawn on the days `keep` picks.
68
+ *
69
+ * A missing value is skipped, never averaged in as 0, and a window with fewer measured days than
70
+ * missing ones draws a gap: one partial day after a 12-day outage is not a week. Days before a
71
+ * series' first measured value are not tracked yet, so they neither draw nor count as missing.
72
+ */
73
+ function trailingMean<TData extends Record<string, unknown>>(
74
+ data: TData[],
75
+ times: number[],
76
+ yKeys: string[],
77
+ window: number,
78
+ keep: (i: number) => boolean,
79
+ ): TData[] {
80
+ const firstSeen = Object.fromEntries(
81
+ yKeys.map((k) => [k, times[data.findIndex((d) => measured(d[k]) !== null)] ?? Infinity]),
82
+ );
83
+ const out: TData[] = [];
84
+ data.forEach((d, i) => {
85
+ if (!keep(i)) return;
86
+ const row: Record<string, unknown> = { ...d };
87
+ for (const k of yKeys) {
88
+ if (times[i] < firstSeen[k]) {
89
+ row[k] = null;
90
+ continue;
91
+ }
92
+ let sum = 0;
93
+ let missing = 0;
94
+ for (let j = i; j >= 0 && times[i] - times[j] < window * DAY; j--) {
95
+ const value = measured(data[j][k]);
96
+ if (value === null) missing += times[j] >= firstSeen[k] ? 1 : 0;
97
+ else sum += value;
98
+ }
99
+ // Divided by the tracked days in the window, so a row absent from a sparse series counts as 0.
100
+ const days = Math.min(window, daysBetween(Math.max(times[0], firstSeen[k]), times[i]) + 1);
101
+ const seen = days - missing;
102
+ row[k] = seen > 0 && seen * 2 >= days ? sum / seen : null;
103
+ }
104
+ out.push(row as TData);
105
+ });
106
+ return out;
107
+ }
108
+
43
109
  /**
44
- * The line a chart draws: a trailing 7-day mean, or 7-day buckets counted back from the last day.
45
- * Totals stay the caller's raw sums; this only moves the drawn points.
110
+ * The line a chart draws. Up to a quarter, a trailing 7-day mean per day. Past it, a trailing
111
+ * 28-day mean read once a week from the last day, so the axis stays in per-day units at every
112
+ * range; a level reads its last value each week instead. A missing value (null) is a gap in the
113
+ * line, never a 0. Totals stay the caller's raw sums; this only moves the drawn points.
46
114
  */
47
115
  export function smoothSeries<TData extends Record<string, unknown>>(
48
116
  data: TData[],
49
117
  xKey: keyof TData & string,
50
118
  yKeys: (keyof TData & string)[],
51
119
  { rangeDays, kind = "count" }: Smoothing,
52
- ): { points: TData[]; mode: SmoothMode } {
120
+ ): Smoothed<TData> {
53
121
  const mode = smoothMode(rangeDays);
54
- if (mode === "raw" || data.length < WINDOW) return { points: data, mode: "raw" };
122
+ if (mode === "raw" || data.length < WEEK) return { points: data, mode: "raw", window: 1 };
55
123
  const times = data.map((d) => timeOf(d[xKey]));
124
+ const last = times[times.length - 1];
56
125
 
57
- if (mode === "rolling") {
58
- // A level has no daily noise to average, and a mean draws slopes between its steps.
59
- if (kind === "level") return { points: data, mode: "raw" };
60
- let from = 0;
61
- const sums = Object.fromEntries(yKeys.map((k) => [k, 0])) as Record<string, number>;
62
- const points = data.map((d, i) => {
63
- for (const k of yKeys) sums[k] += Number(d[k] ?? 0);
64
- while (times[i] - times[from] >= WINDOW * DAY) {
65
- for (const k of yKeys) sums[k] -= Number(data[from][k] ?? 0);
66
- from += 1;
67
- }
68
- // Divided by the days in the window, so a missing day counts as zero, not skipped.
69
- const days = Math.min(WINDOW, Math.round((times[i] - times[0]) / DAY) + 1);
70
- const row: Record<string, unknown> = { ...d };
71
- for (const k of yKeys) row[k] = sums[k] / days;
72
- return row as TData;
73
- });
74
- return { points, mode };
126
+ if (kind === "count") {
127
+ const window = mode === "rolling" ? WEEK : LONG_WINDOW;
128
+ const keep =
129
+ mode === "rolling" ? () => true : (i: number) => daysBetween(times[i], last) % WEEK === 0;
130
+ return { points: trailingMean(data, times, yKeys, window, keep), mode: "rolling", window };
75
131
  }
76
132
 
77
- // Weeks end on the last day, so only the oldest can be partial, and a partial week is dropped:
78
- // a 3-day sum would draw a dip that never happened.
79
- const last = times[times.length - 1];
133
+ // A level has no daily noise to average, and a mean draws slopes between its steps.
134
+ if (mode === "rolling") return { points: data, mode: "raw", window: 1 };
135
+
136
+ // Weeks end on the last day, so only the oldest can be partial, and a partial week is dropped.
80
137
  const buckets = new Map<number, TData[]>();
81
138
  data.forEach((d, i) => {
82
- const index = Math.floor(Math.round((last - times[i]) / DAY) / WINDOW);
139
+ const index = Math.floor(daysBetween(times[i], last) / WEEK);
83
140
  buckets.set(index, [...(buckets.get(index) ?? []), d]);
84
141
  });
85
142
  const oldest = Math.max(...buckets.keys());
86
- const oldestStart = last - (oldest * WINDOW + WINDOW - 1) * DAY;
143
+ const oldestStart = last - (oldest * WEEK + WEEK - 1) * DAY;
87
144
  if (times[0] > oldestStart && buckets.size > 1) buckets.delete(oldest);
88
145
 
89
146
  const points = [...buckets.entries()]
90
147
  .toSorted(([a], [b]) => b - a)
91
148
  .map(([index, rows]) => {
92
149
  const row: Record<string, unknown> = { ...rows[rows.length - 1] };
93
- row[xKey] = sameForm(data[0][xKey], last - (index * WINDOW + WINDOW - 1) * DAY);
150
+ row[xKey] = sameForm(data[0][xKey], last - (index * WEEK + WEEK - 1) * DAY);
94
151
  for (const k of yKeys) {
95
- row[k] =
96
- kind === "level"
97
- ? Number(rows[rows.length - 1][k] ?? 0)
98
- : rows.reduce((sum, r) => sum + Number(r[k] ?? 0), 0);
152
+ const readings = rows.map((r) => measured(r[k])).filter((v) => v !== null);
153
+ row[k] = readings.length > 0 ? readings[readings.length - 1] : null;
99
154
  }
100
155
  return row as TData;
101
156
  });
102
- return { points, mode };
157
+ return { points, mode, window: WEEK };
103
158
  }
104
159
 
105
160
  /** Labels at least `spacing` pixels apart, the way a time axis reads at any width. */
@@ -6,7 +6,14 @@
6
6
  import { Area, Chart, Highlight, LinearGradient, Points, Spline, Svg, Tooltip } from "layerchart";
7
7
  import { curveMonotoneX } from "d3-shape";
8
8
  import Reveal from "./reveal.svelte";
9
- import { ROLLING_LABEL, smoothSeries, weekOf, type Smoothing } from "./smoothing.js";
9
+ import {
10
+ NO_DATA,
11
+ averageLabel,
12
+ averageTo,
13
+ smoothSeries,
14
+ weekOf,
15
+ type Smoothing,
16
+ } from "./smoothing.js";
10
17
 
11
18
  // =========================
12
19
  // TYPES
@@ -71,7 +78,7 @@
71
78
  active = d;
72
79
  });
73
80
  const x = formatX(d[xKey]);
74
- return mode === "weekly" ? weekOf(x) : x;
81
+ return mode === "weekly" ? weekOf(x) : mode === "rolling" ? averageTo(x, smoothed.window) : x;
75
82
  }
76
83
 
77
84
  // =========================
@@ -81,13 +88,14 @@
81
88
  const smoothed = $derived(
82
89
  smoothing
83
90
  ? smoothSeries(input, xKey, [yKey], smoothing)
84
- : { points: input, mode: "raw" as const },
91
+ : { points: input, mode: "raw" as const, window: 1 },
85
92
  );
86
93
  const data = $derived(smoothed.points);
87
94
  const mode = $derived(smoothed.mode);
88
- const valueLabel = $derived(mode === "rolling" ? ROLLING_LABEL : String(yKey));
95
+ const valueLabel = $derived(mode === "rolling" ? averageLabel(smoothed.window) : String(yKey));
89
96
  const curve = $derived(smooth ? curveMonotoneX : undefined);
90
- const last = $derived(data.length > 0 ? data[data.length - 1] : null);
97
+ // The newest measured point: a gap at the end has no height for the dot to sit at.
98
+ const last = $derived(data.findLast((d) => d[yKey] != null) ?? null);
91
99
  const display = $derived(active ?? last);
92
100
  const dot = $derived(endCap && !bare);
93
101
  // Bare runs to the edges: the fill reaches the bottom of the box and the line keeps three pixels of head so its stroke is not cut at the series
@@ -144,7 +152,10 @@
144
152
  {#snippet children({ data: d })}
145
153
  <Tooltip.Header value={trackActive(d as TData)} />
146
154
  <Tooltip.List>
147
- <Tooltip.Item label={valueLabel} value={formatValue(Number(d[yKey]))} />
155
+ <Tooltip.Item
156
+ label={valueLabel}
157
+ value={d[yKey] == null ? NO_DATA : formatValue(Number(d[yKey]))}
158
+ />
148
159
  </Tooltip.List>
149
160
  {/snippet}
150
161
  </Tooltip.Root>
@@ -81,15 +81,15 @@ isn't a drop target (an Overdue lane, say).
81
81
  | `manualTiebreak` | `(a: T, b: T) => number` | Settles two cards with the same manual key, or none. |
82
82
  | `getLabel` | `(item: T) => string` | The screen-reader name of a card. Defaults to a keyboard hint. |
83
83
  | `onSelect` | `(id: string) => void` | Enter or Space on a focused card. |
84
- | `columnFooter` | `Snippet<[Column, KanbanColumnContext]>` | Under a column's cards: a "Show all" or an add button. |
84
+ | `columnFooter` | `Snippet<[Column, KanbanColumnContext]>` | Under a column's cards and its fold button, e.g. an add button. |
85
85
  | `wrapColumn` | `Snippet<[Column, Snippet<[props]>]>` | Wraps a whole column, for a context menu. Render the inner snippet once with the props you are handed. |
86
- | `limit` | `(columnId: string) => number \| undefined` | Cards a column shows before the rest fold. `expanded` and `onExpandedChange` lift it per column. |
86
+ | `limit` | `number \| ((columnId: string) => number \| undefined)` | Cards a column shows before the rest fold, 5 by default; `Infinity` for none. A "Show N more" button opens the fold and "Show less" shuts it, both animated. `moreLabel`, `lessLabel`, `expanded` and `onExpandedChange` go with it. A card moved into a shut column stays in view. |
87
87
  | `hidden` | `string[]` | Columns folded into a rail on the right. Fold and unfold animate, and a card dropped on a rail entry brings the column back. `onHiddenChange`, `hiddenLabel`, `showAllLabel` and `hiddenItem` go with it. |
88
88
 
89
89
  `columns` can be your own type, as long as it extends `KanbanColumn`: the
90
90
  header, footer and wrap snippets get it back as you passed it, so a column can
91
91
  carry a status or a stage for your own markup. The header and footer context is
92
- `{ count, active, shown, capped, hide, expand }`.
92
+ `{ count, active, shown, capped, hide, expand, collapse }`.
93
93
 
94
94
  `onMove` gives `{ columnId, boardOrder?, beforeId?, afterId? }`. On a manual
95
95
  drop `beforeId` and `afterId` name the cards above and below the new slot (null
@@ -0,0 +1,265 @@
1
+ import { untrack } from "svelte";
2
+ import { SvelteSet } from "svelte/reactivity";
3
+ import { prefersReducedMotion } from "svelte/motion";
4
+ import { cubicOut } from "svelte/easing";
5
+ import { durationMs } from "../lib/motion.js";
6
+ import { createKanbanDnd } from "./use-kanban-dnd.svelte.js";
7
+ import { createKanbanRailState } from "./kanban-rail-state.svelte.js";
8
+ import { createCardKeyDown } from "./kanban-keyboard.js";
9
+ import type {
10
+ KanbanColumn as KanbanColumnDef,
11
+ KanbanColumnContext,
12
+ KanbanMove,
13
+ KanbanSortKey,
14
+ } from "./kanban-types.js";
15
+
16
+ /** The board props the state reads, with the board's defaults already applied. */
17
+ export type KanbanBoardInput<T, C extends KanbanColumnDef> = {
18
+ columns: C[];
19
+ items: T[];
20
+ getId: (item: T) => string;
21
+ getColumn: (item: T) => string;
22
+ onMove: (id: string, move: KanbanMove) => void | Promise<void>;
23
+ boardOrder?: (item: T) => string | null | undefined;
24
+ manualTiebreak?: (a: T, b: T) => number;
25
+ sortKeys: KanbanSortKey<T>[];
26
+ sortKey: string;
27
+ onSelect?: (id: string) => void;
28
+ limit: number | ((columnId: string) => number | undefined);
29
+ expanded?: string[];
30
+ onExpandedChange?: (ids: string[]) => void;
31
+ hidden?: string[];
32
+ onHiddenChange?: (ids: string[]) => void;
33
+ dropAtTop?: (columnId: string) => boolean;
34
+ isDroppable?: (columnId: string) => boolean;
35
+ };
36
+
37
+ /** One column as the board renders it: every card, the head over the fold and the tail in it. */
38
+ export type KanbanColumnView<T, C extends KanbanColumnDef> = {
39
+ column: C;
40
+ all: T[];
41
+ head: T[];
42
+ tail: T[];
43
+ foldable: boolean;
44
+ open: boolean;
45
+ capped: boolean;
46
+ };
47
+
48
+ /**
49
+ * A board's state: sort, the per-column fold and cap, and the drag wiring. The hidden rail
50
+ * lives in `rail`, the keyboard move in `onCardKeyDown`.
51
+ * Call it during component init, since it registers an effect. `input` is read on every use.
52
+ */
53
+ export function createKanbanBoardState<T, C extends KanbanColumnDef>(
54
+ input: () => KanbanBoardInput<T, C>,
55
+ ) {
56
+ // The drop slot takes the held card's height, so the column doesn't jump when it lands.
57
+ let slotHeight = $state(0);
58
+ let slotMotion = $derived({
59
+ duration: prefersReducedMotion.current ? 0 : durationMs("quick"),
60
+ easing: cubicOut,
61
+ });
62
+
63
+ let internalExpanded = $state<string[]>([]);
64
+ let expandedIds = $derived(input().expanded ?? internalExpanded);
65
+
66
+ function compare(a: T, b: T): number {
67
+ const { sortKey, manualTiebreak, boardOrder, sortKeys } = input();
68
+ if (sortKey === "manual") {
69
+ const tie = manualTiebreak?.(a, b) ?? 0;
70
+ if (!boardOrder) return tie;
71
+ const ao = boardOrder(a);
72
+ const bo = boardOrder(b);
73
+ if (ao == null && bo == null) return tie;
74
+ if (ao == null) return 1;
75
+ if (bo == null) return -1;
76
+ return ao < bo ? -1 : ao > bo ? 1 : tie;
77
+ }
78
+ const key = sortKeys.find((entry) => entry.id === sortKey);
79
+ return key ? key.compare(a, b) : 0;
80
+ }
81
+
82
+ function sortedByColumn(columnId: string): T[] {
83
+ const { items, getColumn } = input();
84
+ return items
85
+ .filter((item) => getColumn(item) === columnId)
86
+ .slice()
87
+ .sort(compare);
88
+ }
89
+
90
+ function idAt(list: T[], index: number): string | null {
91
+ const item = list[index];
92
+ return item === undefined ? null : input().getId(item);
93
+ }
94
+
95
+ function capOf(columnId: string): number | undefined {
96
+ const { limit } = input();
97
+ return typeof limit === "function" ? limit(columnId) : limit;
98
+ }
99
+
100
+ /** The column holds more cards than its cap, so it gets a fold. */
101
+ function isFoldable(columnId: string, total: number): boolean {
102
+ const cap = capOf(columnId);
103
+ return cap !== undefined && total > cap;
104
+ }
105
+
106
+ // Open from this board's "Show N more", on top of the header caret's `expanded`.
107
+ const laneOpen = new SvelteSet<string>();
108
+
109
+ function isOpen(columnId: string): boolean {
110
+ return expandedIds.includes(columnId) || laneOpen.has(columnId);
111
+ }
112
+
113
+ /** The fold is shut, so the tail is out of sight. */
114
+ function isCapped(columnId: string, total: number): boolean {
115
+ return isFoldable(columnId, total) && !isOpen(columnId);
116
+ }
117
+
118
+ // A card moved into a folded column stays in view, wherever its sort puts it.
119
+ const pinned = new SvelteSet<string>();
120
+
121
+ // The caret folding a column shuts this board's own fold of it too, so every lane lands on the cap.
122
+ let previousExpanded: string[] = [];
123
+ $effect(() => {
124
+ const now = expandedIds;
125
+ untrack(() => {
126
+ for (const id of previousExpanded) if (!now.includes(id)) shut(id);
127
+ });
128
+ previousExpanded = [...now];
129
+ });
130
+
131
+ function moveAndPin(id: string, move: KanbanMove): void | Promise<void> {
132
+ pinned.add(id);
133
+ return input().onMove(id, move);
134
+ }
135
+
136
+ function canDrop(columnId: string): boolean {
137
+ const { isDroppable, columns } = input();
138
+ return isDroppable?.(columnId) ?? columns.find((c) => c.id === columnId)?.droppable ?? true;
139
+ }
140
+
141
+ function setExpanded(next: string[]): void {
142
+ internalExpanded = next;
143
+ input().onExpandedChange?.(next);
144
+ }
145
+
146
+ // Shutting drops the column's pins, so the fold closes to exactly its cap.
147
+ function shut(columnId: string): void {
148
+ laneOpen.delete(columnId);
149
+ for (const item of sortedByColumn(columnId)) pinned.delete(input().getId(item));
150
+ }
151
+
152
+ /** "Show N more": opens this board's cell only. */
153
+ function expand(columnId: string): void {
154
+ laneOpen.add(columnId);
155
+ }
156
+
157
+ /** "Show less": shuts this board's cell, and lifts the caret's open if it was the caret's. */
158
+ function collapse(columnId: string): void {
159
+ shut(columnId);
160
+ if (expandedIds.includes(columnId)) setExpanded(expandedIds.filter((id) => id !== columnId));
161
+ }
162
+
163
+ /** The header caret: the whole column, in every board that shares `expanded`. */
164
+ function toggleColumn(columnId: string): void {
165
+ if (expandedIds.includes(columnId)) collapse(columnId);
166
+ else setExpanded([...expandedIds, columnId]);
167
+ }
168
+
169
+ const dnd = createKanbanDnd<T>({
170
+ items: () => input().items,
171
+ getId: (item) => input().getId(item),
172
+ getColumn: (item) => input().getColumn(item),
173
+ getColumnItems: sortedByColumn,
174
+ getSortKey: () => input().sortKey,
175
+ boardOrder: (item) => input().boardOrder?.(item),
176
+ onMove: moveAndPin,
177
+ dropAtTop: (columnId) =>
178
+ input().dropAtTop?.(columnId) ?? isCapped(columnId, sortedByColumn(columnId).length),
179
+ isDroppable: canDrop,
180
+ });
181
+
182
+ const rail = createKanbanRailState({
183
+ hidden: () => input().hidden,
184
+ onHiddenChange: () => input().onHiddenChange,
185
+ canDrop,
186
+ dnd,
187
+ });
188
+
189
+ const onCardKeyDown = createCardKeyDown<T, C>({
190
+ input,
191
+ canDrop,
192
+ isHidden: (columnId) => rail.hiddenSet.has(columnId),
193
+ sortedByColumn,
194
+ isCapped,
195
+ moveAndPin,
196
+ });
197
+
198
+ let view = $derived(
199
+ input().columns.map((column): KanbanColumnView<T, C> => {
200
+ const all = sortedByColumn(column.id);
201
+ const cap = capOf(column.id);
202
+ const foldable = isFoldable(column.id, all.length);
203
+ const open = foldable && isOpen(column.id);
204
+ if (!foldable || cap === undefined) {
205
+ return { column, all, head: all, tail: [] as T[], foldable, open, capped: false };
206
+ }
207
+ // Head: the first `cap` cards plus any pinned ones, in sort order. The fold holds the rest.
208
+ const head: T[] = [];
209
+ const tail: T[] = [];
210
+ all.forEach((item, index) => {
211
+ (index < cap || pinned.has(input().getId(item)) ? head : tail).push(item);
212
+ });
213
+ return { column, all, head, tail, foldable, open, capped: !open && tail.length > 0 };
214
+ }),
215
+ );
216
+
217
+ let hiddenColumns = $derived(input().columns.filter((column) => rail.hiddenSet.has(column.id)));
218
+
219
+ function contextOf(entry: KanbanColumnView<T, C>, active: boolean): KanbanColumnContext {
220
+ const { column, all, head, capped } = entry;
221
+ return {
222
+ count: all.length,
223
+ active,
224
+ shown: capped ? head.length : all.length,
225
+ capped,
226
+ hide: () => rail.hide(column.id),
227
+ expand: () => expand(column.id),
228
+ collapse: () => collapse(column.id),
229
+ };
230
+ }
231
+
232
+ return {
233
+ get slotHeight() {
234
+ return slotHeight;
235
+ },
236
+ set slotHeight(value: number) {
237
+ slotHeight = value;
238
+ },
239
+ get slotMotion() {
240
+ return slotMotion;
241
+ },
242
+ get expandedIds() {
243
+ return expandedIds;
244
+ },
245
+ get view() {
246
+ return view;
247
+ },
248
+ get hiddenColumns() {
249
+ return hiddenColumns;
250
+ },
251
+ dnd,
252
+ rail,
253
+ sortedByColumn,
254
+ idAt,
255
+ expand,
256
+ collapse,
257
+ toggleColumn,
258
+ onCardKeyDown,
259
+ contextOf,
260
+ };
261
+ }
262
+
263
+ export type KanbanBoardState<T, C extends KanbanColumnDef> = ReturnType<
264
+ typeof createKanbanBoardState<T, C>
265
+ >;