@joewinke/jatui 0.40.0 → 0.41.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.
Files changed (39) hide show
  1. package/CLAUDE.md +29 -0
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/dist/components/AnimatedNumber.svelte +42 -1
  4. package/dist/components/AnimatedNumber.svelte.d.ts +7 -0
  5. package/dist/components/SegmentedControl.svelte +263 -0
  6. package/dist/components/SegmentedControl.svelte.d.ts +48 -0
  7. package/dist/components/settings/SettingsRow.svelte +177 -0
  8. package/dist/components/settings/SettingsRow.svelte.d.ts +32 -0
  9. package/dist/components/settings/SettingsSection.svelte +139 -0
  10. package/dist/components/settings/SettingsSection.svelte.d.ts +21 -0
  11. package/dist/components/stats/ActivityHeatmap.svelte +412 -0
  12. package/dist/components/stats/ActivityHeatmap.svelte.d.ts +72 -0
  13. package/dist/components/stats/BarList.svelte +262 -0
  14. package/dist/components/stats/BarList.svelte.d.ts +57 -0
  15. package/dist/components/stats/ChartCard.svelte +230 -0
  16. package/dist/components/stats/ChartCard.svelte.d.ts +63 -0
  17. package/dist/components/stats/StatCard.svelte +387 -0
  18. package/dist/components/stats/StatCard.svelte.d.ts +37 -0
  19. package/dist/components/stats/StatGroup.svelte +67 -0
  20. package/dist/components/stats/StatGroup.svelte.d.ts +14 -0
  21. package/dist/components/stats/heatmap.d.ts +76 -0
  22. package/dist/components/stats/heatmap.js +151 -0
  23. package/dist/components/tree/TreeView.svelte +680 -0
  24. package/dist/components/tree/TreeView.svelte.d.ts +123 -0
  25. package/dist/components/tree/flattenTree.d.ts +57 -0
  26. package/dist/components/tree/flattenTree.js +91 -0
  27. package/dist/components/tree/treeKeys.d.ts +46 -0
  28. package/dist/components/tree/treeKeys.js +80 -0
  29. package/dist/data/grid/cells.d.ts +40 -0
  30. package/dist/data/grid/cells.js +89 -0
  31. package/dist/data/grid/clipboard.d.ts +46 -0
  32. package/dist/data/grid/clipboard.js +131 -0
  33. package/dist/data/grid/edits.d.ts +148 -0
  34. package/dist/data/grid/edits.js +222 -0
  35. package/dist/data/grid/index.d.ts +12 -0
  36. package/dist/data/grid/index.js +12 -0
  37. package/dist/index.d.ts +22 -0
  38. package/dist/index.js +21 -0
  39. package/package.json +5 -1
@@ -0,0 +1,387 @@
1
+ <!--
2
+ StatCard: one KPI tile (jatui-6kitr). Label, a value that counts to its
3
+ new number, a delta chip toned by direction, caption, optional icon tile,
4
+ info tooltip and an inline sparkline (plain SVG, no chart library).
5
+
6
+ Two looks after BoardUI's stat cards (MIT, © 2026 Mertcan Dundar
7
+ Esmergul): `plain` and `footer` (caption + delta in a footer band). All
8
+ colour comes from theme tokens; deltas use success / error / neutral, never
9
+ fixed hues. Group several with StatGroup.
10
+ -->
11
+ <script lang="ts" module>
12
+ export type StatTone = 'primary' | 'secondary' | 'accent' | 'success' | 'info' | 'warning' | 'error' | 'neutral';
13
+ </script>
14
+
15
+ <script lang="ts">
16
+ import type { Snippet } from 'svelte';
17
+ import AnimatedNumber from '../AnimatedNumber.svelte';
18
+ import { tooltip } from '../../actions/tooltip';
19
+
20
+ interface Props {
21
+ label: string;
22
+ /** A number animates; a string is shown as-is. */
23
+ value: number | string | null | undefined;
24
+ format?: Intl.NumberFormatOptions;
25
+ /** Number animation: 'tween' (default) counts, 'roll' is the odometer, false is static. */
26
+ animate?: 'tween' | 'roll' | false;
27
+ /** A number is a FRACTION (0.125 → "+12.5%"); a string is shown as-is. */
28
+ delta?: number | string | null;
29
+ deltaFormat?: Intl.NumberFormatOptions;
30
+ /** 'auto' tones by sign. */
31
+ deltaTone?: 'auto' | 'success' | 'error' | 'neutral';
32
+ /** For metrics where down is good (churn, cost, response time). */
33
+ invertDelta?: boolean;
34
+ caption?: string;
35
+ /** Help text behind an (i) icon. */
36
+ info?: string;
37
+ /** Trend values drawn as a small inline line. */
38
+ sparkline?: readonly number[];
39
+ variant?: 'plain' | 'footer';
40
+ /** Icon tile tint. */
41
+ tone?: StatTone;
42
+ size?: 'sm' | 'md';
43
+ /** Makes the whole card a link. */
44
+ href?: string;
45
+ loading?: boolean;
46
+ icon?: Snippet;
47
+ /** Extra content under the value (or in the footer band for `footer`). */
48
+ children?: Snippet;
49
+ locale?: string;
50
+ class?: string;
51
+ }
52
+
53
+ let {
54
+ label,
55
+ value,
56
+ format,
57
+ animate = 'tween',
58
+ delta = null,
59
+ deltaFormat = { style: 'percent', maximumFractionDigits: 1, signDisplay: 'exceptZero' },
60
+ deltaTone = 'auto',
61
+ invertDelta = false,
62
+ caption,
63
+ info,
64
+ sparkline,
65
+ variant = 'plain',
66
+ tone = 'primary',
67
+ size = 'md',
68
+ href,
69
+ loading = false,
70
+ icon,
71
+ children,
72
+ locale,
73
+ class: className = ''
74
+ }: Props = $props();
75
+
76
+ const uid = $props.id();
77
+
78
+ const deltaText = $derived(
79
+ delta == null ? '' : typeof delta === 'number' ? new Intl.NumberFormat(locale, deltaFormat).format(delta) : delta
80
+ );
81
+ const direction = $derived.by((): 'up' | 'down' | 'flat' => {
82
+ if (delta == null) return 'flat';
83
+ const n = typeof delta === 'number' ? delta : parseFloat(delta.replace(/[^\d.+-]/g, ''));
84
+ if (!Number.isFinite(n) || n === 0) return 'flat';
85
+ return n > 0 ? 'up' : 'down';
86
+ });
87
+ const resolvedTone = $derived.by(() => {
88
+ if (deltaTone !== 'auto') return deltaTone;
89
+ if (direction === 'flat') return 'neutral';
90
+ return (direction === 'up') !== invertDelta ? 'success' : 'error';
91
+ });
92
+
93
+ const staticValue = $derived(
94
+ value == null ? '—' : typeof value === 'number' ? new Intl.NumberFormat(locale, format).format(value) : value
95
+ );
96
+
97
+ // Inline sparkline path in a 100×28 box.
98
+ const spark = $derived.by(() => {
99
+ const pts = (sparkline ?? []).filter((v) => Number.isFinite(v));
100
+ if (pts.length < 2) return null;
101
+ const min = Math.min(...pts);
102
+ const max = Math.max(...pts);
103
+ const span = max - min || 1;
104
+ const xy = pts.map((v, i) => [(i / (pts.length - 1)) * 100, 26 - ((v - min) / span) * 24] as const);
105
+ const line = xy.map(([x, y], i) => `${i ? 'L' : 'M'}${x.toFixed(2)} ${y.toFixed(2)}`).join(' ');
106
+ return { line, area: `${line} L100 28 L0 28 Z` };
107
+ });
108
+ </script>
109
+
110
+ {#snippet deltaChip()}
111
+ {#if deltaText}
112
+ <span class="sc-delta tone-{resolvedTone}">
113
+ {#if direction !== 'flat'}
114
+ <svg viewBox="0 0 12 12" aria-hidden="true" class:is-down={direction === 'down'}>
115
+ <path d="M6 2.5v7M2.75 5.5 6 2.25 9.25 5.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
116
+ </svg>
117
+ <span class="sr-only">{direction === 'up' ? 'Up' : 'Down'}</span>
118
+ {/if}
119
+ {deltaText}
120
+ </span>
121
+ {/if}
122
+ {/snippet}
123
+
124
+ {#snippet body()}
125
+ <div class="sc-main">
126
+ <div class="sc-head">
127
+ {#if icon}
128
+ <span class="sc-icon tone-{tone}" aria-hidden="true">{@render icon()}</span>
129
+ {/if}
130
+ <span class="sc-label" id="{uid}-label">{label}</span>
131
+ {#if info}
132
+ <span
133
+ class="sc-info"
134
+ role="img"
135
+ aria-label={info}
136
+ use:tooltip={{ text: info, placement: 'top' }}
137
+ >
138
+ <svg viewBox="0 0 16 16" aria-hidden="true">
139
+ <circle cx="8" cy="8" r="6.25" fill="none" stroke="currentColor" stroke-width="1.25" />
140
+ <path d="M8 7.25v3.5M8 5.1v.05" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
141
+ </svg>
142
+ </span>
143
+ {/if}
144
+ </div>
145
+ <div class="sc-value-row">
146
+ {#if loading}
147
+ <span class="sc-skeleton" aria-hidden="true"></span>
148
+ <span class="sr-only">Loading</span>
149
+ {:else}
150
+ <span class="sc-value">
151
+ {#if typeof value === 'number' && animate}
152
+ <AnimatedNumber {value} format={format ?? {}} mode={animate} />
153
+ {:else}
154
+ {staticValue}
155
+ {/if}
156
+ </span>
157
+ {#if variant === 'plain'}{@render deltaChip()}{/if}
158
+ {/if}
159
+ {#if spark && !loading}
160
+ <svg class="sc-spark" viewBox="0 0 100 28" preserveAspectRatio="none" aria-hidden="true">
161
+ <path d={spark.area} class="sc-spark-area" />
162
+ <path d={spark.line} class="sc-spark-line" vector-effect="non-scaling-stroke" />
163
+ </svg>
164
+ {/if}
165
+ </div>
166
+ {#if variant === 'plain' && caption}
167
+ <p class="sc-caption">{caption}</p>
168
+ {/if}
169
+ {#if variant === 'plain' && children}{@render children()}{/if}
170
+ </div>
171
+ {#if variant === 'footer' && (caption || deltaText || children)}
172
+ <div class="sc-footer">
173
+ {#if caption}<span class="sc-caption">{caption}</span>{/if}
174
+ {#if children}{@render children()}{/if}
175
+ {@render deltaChip()}
176
+ </div>
177
+ {/if}
178
+ {/snippet}
179
+
180
+ {#if href}
181
+ <a
182
+ {href}
183
+ class="jatui-stat-card sc-{variant} sc-{size} is-link {className}"
184
+ aria-labelledby="{uid}-label"
185
+ aria-busy={loading || undefined}
186
+ >
187
+ {@render body()}
188
+ </a>
189
+ {:else}
190
+ <div
191
+ class="jatui-stat-card sc-{variant} sc-{size} {className}"
192
+ role="group"
193
+ aria-labelledby="{uid}-label"
194
+ aria-busy={loading || undefined}
195
+ >
196
+ {@render body()}
197
+ </div>
198
+ {/if}
199
+
200
+ <style>
201
+ .jatui-stat-card {
202
+ --sc-pad: var(--jatui-space-xl, 16px);
203
+ display: flex;
204
+ flex-direction: column;
205
+ min-width: 0;
206
+ border: var(--jatui-border-w, 1px) solid color-mix(in oklch, var(--color-base-content) 10%, transparent);
207
+ border-radius: var(--jatui-radius-box, 0.5rem);
208
+ background: var(--color-base-100);
209
+ color: var(--color-base-content);
210
+ text-decoration: none;
211
+ overflow: hidden;
212
+ }
213
+ .sc-sm {
214
+ --sc-pad: var(--jatui-space-lg, 12px);
215
+ }
216
+ .is-link {
217
+ transition: border-color var(--motion-fast, 150ms) var(--motion-standard, ease);
218
+ }
219
+ .is-link:hover {
220
+ border-color: color-mix(in oklch, var(--color-base-content) 25%, transparent);
221
+ }
222
+ .is-link:focus-visible {
223
+ outline: 2px solid var(--color-primary);
224
+ outline-offset: 2px;
225
+ }
226
+
227
+ .sc-main {
228
+ display: flex;
229
+ flex-direction: column;
230
+ gap: var(--jatui-space-base, 8px);
231
+ padding: var(--sc-pad);
232
+ flex: 1 1 auto;
233
+ }
234
+ .sc-head {
235
+ display: flex;
236
+ align-items: center;
237
+ gap: var(--jatui-space-base, 8px);
238
+ min-width: 0;
239
+ }
240
+ .sc-label {
241
+ min-width: 0;
242
+ overflow: hidden;
243
+ text-overflow: ellipsis;
244
+ white-space: nowrap;
245
+ font-size: 0.8125rem;
246
+ font-weight: 500;
247
+ color: color-mix(in oklch, var(--color-base-content) 65%, transparent);
248
+ }
249
+ .sc-info {
250
+ display: inline-flex;
251
+ width: 0.875rem;
252
+ height: 0.875rem;
253
+ flex: none;
254
+ color: color-mix(in oklch, var(--color-base-content) 45%, transparent);
255
+ cursor: help;
256
+ }
257
+ .sc-info svg {
258
+ width: 100%;
259
+ height: 100%;
260
+ }
261
+
262
+ .sc-icon {
263
+ --sc-tone: var(--color-primary);
264
+ display: inline-flex;
265
+ align-items: center;
266
+ justify-content: center;
267
+ flex: none;
268
+ width: 2rem;
269
+ height: 2rem;
270
+ border-radius: var(--jatui-radius-field, 0.25rem);
271
+ background: color-mix(in oklch, var(--sc-tone) 14%, var(--color-base-100));
272
+ color: var(--sc-tone);
273
+ }
274
+ .sc-sm .sc-icon {
275
+ width: 1.625rem;
276
+ height: 1.625rem;
277
+ }
278
+ .sc-icon :global(svg) {
279
+ width: 55%;
280
+ height: 55%;
281
+ }
282
+ .sc-icon.tone-secondary { --sc-tone: var(--color-secondary); }
283
+ .sc-icon.tone-accent { --sc-tone: var(--color-accent); }
284
+ .sc-icon.tone-success { --sc-tone: var(--color-success); }
285
+ .sc-icon.tone-info { --sc-tone: var(--color-info); }
286
+ .sc-icon.tone-warning { --sc-tone: var(--color-warning); }
287
+ .sc-icon.tone-error { --sc-tone: var(--color-error); }
288
+ .sc-icon.tone-neutral { --sc-tone: var(--color-base-content); }
289
+
290
+ .sc-value-row {
291
+ display: flex;
292
+ align-items: center;
293
+ flex-wrap: wrap;
294
+ gap: var(--jatui-space-base, 8px);
295
+ min-height: 2.25rem;
296
+ }
297
+ .sc-value {
298
+ font-size: 1.75rem;
299
+ line-height: 1.15;
300
+ font-weight: 600;
301
+ letter-spacing: -0.01em;
302
+ font-variant-numeric: tabular-nums;
303
+ }
304
+ .sc-sm .sc-value {
305
+ font-size: 1.375rem;
306
+ }
307
+ .sc-skeleton {
308
+ display: block;
309
+ width: 6rem;
310
+ height: 1.75rem;
311
+ border-radius: var(--jatui-radius-field, 0.25rem);
312
+ background: color-mix(in oklch, var(--color-base-content) 10%, transparent);
313
+ animation: sc-pulse 1.4s ease-in-out infinite;
314
+ }
315
+ @keyframes sc-pulse {
316
+ 50% {
317
+ opacity: 0.5;
318
+ }
319
+ }
320
+
321
+ .sc-delta {
322
+ --sc-d: var(--color-base-content);
323
+ display: inline-flex;
324
+ align-items: center;
325
+ gap: 2px;
326
+ padding: 1px var(--jatui-space-md, 6px);
327
+ border-radius: var(--jatui-radius-selector, 0.375rem);
328
+ background: color-mix(in oklch, var(--sc-d) 12%, transparent);
329
+ color: color-mix(in oklch, var(--sc-d) 85%, var(--color-base-content));
330
+ font-size: 0.75rem;
331
+ font-weight: 600;
332
+ font-variant-numeric: tabular-nums;
333
+ white-space: nowrap;
334
+ }
335
+ .sc-delta.tone-success { --sc-d: var(--color-success); }
336
+ .sc-delta.tone-error { --sc-d: var(--color-error); }
337
+ .sc-delta.tone-neutral {
338
+ --sc-d: var(--color-base-content);
339
+ color: color-mix(in oklch, var(--color-base-content) 65%, transparent);
340
+ }
341
+ .sc-delta svg {
342
+ width: 0.75rem;
343
+ height: 0.75rem;
344
+ }
345
+ .sc-delta svg.is-down {
346
+ transform: rotate(180deg);
347
+ }
348
+
349
+ .sc-caption {
350
+ margin: 0;
351
+ font-size: 0.75rem;
352
+ color: color-mix(in oklch, var(--color-base-content) 55%, transparent);
353
+ }
354
+ .sc-footer {
355
+ display: flex;
356
+ align-items: center;
357
+ justify-content: space-between;
358
+ gap: var(--jatui-space-base, 8px);
359
+ padding: var(--jatui-space-base, 8px) var(--sc-pad);
360
+ border-top: var(--jatui-border-w, 1px) solid color-mix(in oklch, var(--color-base-content) 8%, transparent);
361
+ background: color-mix(in oklch, var(--color-base-content) 3%, var(--color-base-100));
362
+ }
363
+
364
+ .sc-spark {
365
+ flex: 1 1 4rem;
366
+ max-width: 8rem;
367
+ height: 1.75rem;
368
+ margin-inline-start: auto;
369
+ overflow: visible;
370
+ }
371
+ .sc-spark-line {
372
+ fill: none;
373
+ stroke: var(--color-primary);
374
+ stroke-width: 1.5;
375
+ stroke-linejoin: round;
376
+ stroke-linecap: round;
377
+ }
378
+ .sc-spark-area {
379
+ fill: color-mix(in oklch, var(--color-primary) 14%, transparent);
380
+ }
381
+
382
+ @media (prefers-reduced-motion: reduce) {
383
+ .sc-skeleton {
384
+ animation: none;
385
+ }
386
+ }
387
+ </style>
@@ -0,0 +1,37 @@
1
+ export type StatTone = 'primary' | 'secondary' | 'accent' | 'success' | 'info' | 'warning' | 'error' | 'neutral';
2
+ import type { Snippet } from 'svelte';
3
+ interface Props {
4
+ label: string;
5
+ /** A number animates; a string is shown as-is. */
6
+ value: number | string | null | undefined;
7
+ format?: Intl.NumberFormatOptions;
8
+ /** Number animation: 'tween' (default) counts, 'roll' is the odometer, false is static. */
9
+ animate?: 'tween' | 'roll' | false;
10
+ /** A number is a FRACTION (0.125 → "+12.5%"); a string is shown as-is. */
11
+ delta?: number | string | null;
12
+ deltaFormat?: Intl.NumberFormatOptions;
13
+ /** 'auto' tones by sign. */
14
+ deltaTone?: 'auto' | 'success' | 'error' | 'neutral';
15
+ /** For metrics where down is good (churn, cost, response time). */
16
+ invertDelta?: boolean;
17
+ caption?: string;
18
+ /** Help text behind an (i) icon. */
19
+ info?: string;
20
+ /** Trend values drawn as a small inline line. */
21
+ sparkline?: readonly number[];
22
+ variant?: 'plain' | 'footer';
23
+ /** Icon tile tint. */
24
+ tone?: StatTone;
25
+ size?: 'sm' | 'md';
26
+ /** Makes the whole card a link. */
27
+ href?: string;
28
+ loading?: boolean;
29
+ icon?: Snippet;
30
+ /** Extra content under the value (or in the footer band for `footer`). */
31
+ children?: Snippet;
32
+ locale?: string;
33
+ class?: string;
34
+ }
35
+ declare const StatCard: import("svelte").Component<Props, {}, "">;
36
+ type StatCard = ReturnType<typeof StatCard>;
37
+ export default StatCard;
@@ -0,0 +1,67 @@
1
+ <!--
2
+ StatGroup: lays out StatCards (jatui-6kitr). `cards` (default) is a
3
+ responsive grid of separate cards; `joined` is one bordered panel with
4
+ hairline dividers — the replacement for raw DaisyUI `stats` markup.
5
+ -->
6
+ <script lang="ts">
7
+ import type { Snippet } from 'svelte';
8
+
9
+ interface Props {
10
+ /** Fixed column count. Default: auto-fit columns of at least `minWidth`. */
11
+ columns?: number;
12
+ minWidth?: string;
13
+ variant?: 'cards' | 'joined';
14
+ /** Names the group for assistive tech. */
15
+ ariaLabel?: string;
16
+ children: Snippet;
17
+ class?: string;
18
+ }
19
+
20
+ let {
21
+ columns,
22
+ minWidth = '12rem',
23
+ variant = 'cards',
24
+ ariaLabel,
25
+ children,
26
+ class: className = ''
27
+ }: Props = $props();
28
+
29
+ const template = $derived(
30
+ columns ? `repeat(${columns}, minmax(0, 1fr))` : `repeat(auto-fit, minmax(min(${minWidth}, 100%), 1fr))`
31
+ );
32
+ </script>
33
+
34
+ <div
35
+ class="jatui-stat-group sg-{variant} {className}"
36
+ role={ariaLabel ? 'group' : undefined}
37
+ aria-label={ariaLabel}
38
+ style:grid-template-columns={template}
39
+ >
40
+ {@render children()}
41
+ </div>
42
+
43
+ <style>
44
+ .jatui-stat-group {
45
+ display: grid;
46
+ gap: var(--jatui-space-lg, 12px);
47
+ min-width: 0;
48
+ }
49
+ .sg-joined {
50
+ --sg-divider: color-mix(in oklch, var(--color-base-content) 10%, transparent);
51
+ gap: 0;
52
+ border: var(--jatui-border-w, 1px) solid var(--sg-divider);
53
+ border-radius: var(--jatui-radius-box, 0.5rem);
54
+ background: var(--color-base-100);
55
+ overflow: hidden;
56
+ }
57
+ /* Hairlines on every card's end + bottom edge; the outer ones are clipped. */
58
+ .sg-joined > :global(.jatui-stat-card) {
59
+ border: 0;
60
+ border-radius: 0;
61
+ /* transparent so a neighbour never paints over the hairline */
62
+ background: transparent;
63
+ box-shadow:
64
+ var(--jatui-border-w, 1px) 0 0 var(--sg-divider),
65
+ 0 var(--jatui-border-w, 1px) 0 var(--sg-divider);
66
+ }
67
+ </style>
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ /** Fixed column count. Default: auto-fit columns of at least `minWidth`. */
4
+ columns?: number;
5
+ minWidth?: string;
6
+ variant?: 'cards' | 'joined';
7
+ /** Names the group for assistive tech. */
8
+ ariaLabel?: string;
9
+ children: Snippet;
10
+ class?: string;
11
+ }
12
+ declare const StatGroup: import("svelte").Component<Props, {}, "">;
13
+ type StatGroup = ReturnType<typeof StatGroup>;
14
+ export default StatGroup;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * ActivityHeatmap layout + keyboard math (jatui-6kitr). Pure and DOM-free.
3
+ *
4
+ * Days are local calendar dates, keyed "YYYY-MM-DD". The grid is 7 weekday
5
+ * ROWS × N week COLUMNS (GitHub contributions layout); `weekStart` picks the
6
+ * top row (0 = Sunday, 1 = Monday).
7
+ */
8
+ export interface HeatDay {
9
+ /** "YYYY-MM-DD" (local). */
10
+ date: string;
11
+ count: number;
12
+ /** 0 (none) .. levels. */
13
+ level: number;
14
+ /** Week column, 0-based. */
15
+ col: number;
16
+ /** Weekday row, 0-based from `weekStart`. */
17
+ row: number;
18
+ }
19
+ export interface HeatmapLayout {
20
+ /** Every day in range, in order. */
21
+ days: HeatDay[];
22
+ /** rows[row][col] — null where the first/last week runs outside the range. */
23
+ rows: (HeatDay | null)[][];
24
+ weeks: number;
25
+ max: number;
26
+ total: number;
27
+ /** Days with count > 0. */
28
+ activeDays: number;
29
+ /** Month labels anchored to the first column that starts in that month. */
30
+ months: {
31
+ label: string;
32
+ col: number;
33
+ }[];
34
+ start: string;
35
+ end: string;
36
+ }
37
+ export interface BuildHeatmapOptions<T> {
38
+ data: readonly T[];
39
+ getDate: (d: T) => string | Date;
40
+ getCount: (d: T) => number;
41
+ /** Inclusive. Default: `end` minus one year plus one day. */
42
+ start?: string | Date;
43
+ /** Inclusive. Default: today. */
44
+ end?: string | Date;
45
+ weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
46
+ /** Non-zero colour steps. Default 4. */
47
+ levels?: number;
48
+ /** Custom count → level (0..levels). Default: proportional to the max. */
49
+ levelOf?: (count: number, max: number, levels: number) => number;
50
+ locale?: string;
51
+ /** Min columns between month labels so they never overlap. Default 3. */
52
+ monthGap?: number;
53
+ }
54
+ /** Local "YYYY-MM-DD". */
55
+ export declare function isoDay(d: Date): string;
56
+ /** Parse a day as LOCAL noon (DST-safe arithmetic). "YYYY-MM-DD" is never read as UTC. */
57
+ export declare function parseDay(input: string | Date): Date;
58
+ export declare function addDays(day: string, n: number): string;
59
+ /** Whole days from a to b (b - a). */
60
+ export declare function daysBetween(a: string, b: string): number;
61
+ export declare function defaultLevelOf(count: number, max: number, levels: number): number;
62
+ export declare function buildHeatmap<T>(opts: BuildHeatmapOptions<T>): HeatmapLayout;
63
+ /**
64
+ * Keyboard move inside the heatmap grid. Returns the new focused day,
65
+ * clamped to [start, end], or null for keys it does not handle.
66
+ *
67
+ * ArrowUp/Down previous / next day (row)
68
+ * ArrowLeft/Right previous / next week (column)
69
+ * PageUp/PageDown four weeks back / forward
70
+ * Home / End first / last week, same weekday (row start / end)
71
+ * mod+Home/End first / last day in range
72
+ */
73
+ export declare function moveHeatFocus(day: string, key: string, range: {
74
+ start: string;
75
+ end: string;
76
+ }, mod?: boolean): string | null;