@lilydesignsystem/svelte-headless 0.5.1 → 0.6.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/components/AlertDialog/AlertDialog.svelte +7 -1
  3. package/dist/components/BreadcrumbListItem/BreadcrumbListItem.svelte +3 -3
  4. package/dist/components/ConvertDiv/ConvertDiv.svelte +29 -0
  5. package/dist/components/ConvertDiv/ConvertDiv.svelte.d.ts +11 -0
  6. package/dist/components/ConvertFromDiv/ConvertFromDiv.svelte +26 -0
  7. package/dist/components/ConvertFromDiv/ConvertFromDiv.svelte.d.ts +9 -0
  8. package/dist/components/ConvertFromUnitDiv/ConvertFromUnitDiv.svelte +26 -0
  9. package/dist/components/ConvertFromUnitDiv/ConvertFromUnitDiv.svelte.d.ts +9 -0
  10. package/dist/components/ConvertFromValueDiv/ConvertFromValueDiv.svelte +26 -0
  11. package/dist/components/ConvertFromValueDiv/ConvertFromValueDiv.svelte.d.ts +9 -0
  12. package/dist/components/ConvertIntoDiv/ConvertIntoDiv.svelte +26 -0
  13. package/dist/components/ConvertIntoDiv/ConvertIntoDiv.svelte.d.ts +9 -0
  14. package/dist/components/ConvertIntoUnitDiv/ConvertIntoUnitDiv.svelte +26 -0
  15. package/dist/components/ConvertIntoUnitDiv/ConvertIntoUnitDiv.svelte.d.ts +9 -0
  16. package/dist/components/ConvertIntoValueDiv/ConvertIntoValueDiv.svelte +26 -0
  17. package/dist/components/ConvertIntoValueDiv/ConvertIntoValueDiv.svelte.d.ts +9 -0
  18. package/dist/components/Dialog/Dialog.svelte +14 -1
  19. package/dist/components/Dialog/modal.d.ts +4 -0
  20. package/dist/components/Dialog/modal.js +47 -0
  21. package/dist/components/FileDialog/FileDialog.svelte +7 -1
  22. package/dist/components/HeatmapChartWithYearOfDays/HeatmapChartWithYearOfDays.svelte +74 -0
  23. package/dist/components/HeatmapChartWithYearOfDays/HeatmapChartWithYearOfDays.svelte.d.ts +17 -0
  24. package/dist/components/MiddleTruncation/MiddleTruncation.svelte +108 -0
  25. package/dist/components/MiddleTruncation/MiddleTruncation.svelte.d.ts +15 -0
  26. package/dist/components/MiddleTruncation/middle-truncation.d.ts +28 -0
  27. package/dist/components/MiddleTruncation/middle-truncation.js +67 -0
  28. package/dist/components/PasswordInput/PasswordInput.svelte +1 -1
  29. package/dist/components/PasswordInput/PasswordInput.svelte.d.ts +1 -1
  30. package/dist/components/SectionListItem/SectionListItem.svelte +1 -1
  31. package/dist/components/StepListItem/StepListItem.svelte +3 -3
  32. package/dist/components/TimeoutDialog/TimeoutDialog.svelte +14 -1
  33. package/dist/components/TourListItem/TourListItem.svelte +2 -2
  34. package/dist/components/TreeList/TreeList.svelte +2 -2
  35. package/dist/components/TreeListItem/TreeListItem.svelte +38 -2
  36. package/dist/components/TreeListItem/TreeListItem.svelte.d.ts +3 -1
  37. package/dist/components/WheelPicker/WheelPicker.svelte +150 -0
  38. package/dist/components/WheelPicker/WheelPicker.svelte.d.ts +23 -0
  39. package/dist/components/WheelPicker/wheel-picker.d.ts +43 -0
  40. package/dist/components/WheelPicker/wheel-picker.js +79 -0
  41. package/dist/index.d.ts +10 -0
  42. package/dist/index.js +10 -0
  43. package/package.json +5 -3
package/CHANGELOG.md CHANGED
@@ -3,6 +3,15 @@
3
3
  The format is loosely based on [Keep a Changelog](https://keepachangelog.com/)
4
4
  and the package follows [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## 0.6.0 — 2026-10-23 (three components)
7
+
8
+ - New: `HeatmapChartWithYearOfDays`, `MiddleTruncation`, `WheelPicker`.
9
+ - `HeatmapChartWithYearOfDays`: a `HeatmapChart` for one year of days (required `year` and `weekStart`, reflected as `data-year` and `data-week-start`; a week-column by weekday-row layout contract).
10
+ - `MiddleTruncation`: a string shortened in the middle to fit its width, keeping the start and the end; the shortened text is `aria-hidden` and the full text stays in the accessibility tree; `ellipsis` is required and has no default.
11
+ - `WheelPicker`: a vertical single-select listbox where the selection follows the cursor (arrows, Home/End, Page keys, type-ahead, optional loop).
12
+ - Contract fix: `Slider`'s documented root is `<input type="range">` (the docs said `<div>`); `aria-valuetext` documented. `Button` documents the pending-action pattern (`aria-busy`, `aria-disabled`, never `disabled`).
13
+ - The 57 reference themes style the new components. See the repository CHANGELOG (2026-10-23).
14
+
6
15
  ## 0.5.1 — 2026-10-18 (Editable accessible name)
7
16
 
8
17
  - `Editable`: the display-mode button is named "value, label" (the label alone when the value is empty) so its visible text is part of its accessible name (WCAG 2.5.3).
@@ -65,6 +65,7 @@
65
65
  // - WAI-ARIA Alert and Message Dialogs Pattern: https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/
66
66
 
67
67
  import type { Snippet } from "svelte";
68
+ import { modalDialog } from "../Dialog/modal.js";
68
69
 
69
70
  let {
70
71
  class: className = "",
@@ -95,7 +96,12 @@
95
96
  {#if open}
96
97
  <dialog
97
98
  class={`alert-dialog ${className}`}
98
- open
99
+ use:modalDialog={true}
100
+ oncancel={(event) => {
101
+ event.preventDefault();
102
+ open = false;
103
+ }}
104
+ onclose={() => (open = false)}
99
105
  role="alertdialog"
100
106
  aria-labelledby={titleId}
101
107
  aria-describedby={description ? descriptionId : undefined}
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // A single step in a breadcrumb navigation trail. Renders as a semantic <li>
5
5
  // element designed to be placed inside a BreadcrumbList component. Supports
6
- // aria-current="page" to indicate the user's current location. Typically
6
+ // data-current to indicate the user's current location. Typically
7
7
  // contains a link for ancestor pages, or plain text for the current page.
8
8
  //
9
9
  // Props:
@@ -27,7 +27,7 @@
27
27
  //
28
28
  // Accessibility:
29
29
  // - Implicit listitem role from the semantic <li> element
30
- // - aria-current="page" set when current is true, indicating the user's location
30
+ // - data-current set when current is true, indicating the user's location
31
31
  //
32
32
  // Internationalization:
33
33
  // - All text comes through children snippet; no hardcoded strings
@@ -58,7 +58,7 @@
58
58
  <!-- BreadcrumbListItem.svelte -->
59
59
  <li
60
60
  class={`breadcrumb-list-item ${className}`}
61
- aria-current={current ? "page" : undefined}
61
+ data-current={current ? "" : undefined}
62
62
  {...restProps}
63
63
  >
64
64
  {@render children?.()}
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ // ConvertDiv component
3
+ //
4
+ // A headless part of ConvertDiv: a group that converts a value from one unit into another, such as dollars into euros or miles into kilometres. Contract: a plain <div> with the base class and children; role="group" named by `label`.
5
+ // Converts nothing and carries no strings of its own; the consumer supplies the controls.
6
+ import type { Snippet } from "svelte";
7
+
8
+ let {
9
+ class: className = "",
10
+ label,
11
+ children,
12
+ ...restProps
13
+ }: {
14
+ /** Accessible name of the group (required). */
15
+ label: string;
16
+
17
+ /** The content. */
18
+ children?: Snippet;
19
+ [key: string]: unknown;
20
+ } = $props();
21
+ </script>
22
+
23
+ <!-- ConvertDiv.svelte -->
24
+ <div
25
+ class={`convert-div ${className}`} role="group" aria-label={label}
26
+ {...restProps}
27
+ >
28
+ {@render children?.()}
29
+ </div>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from "svelte";
2
+ type $$ComponentProps = {
3
+ /** Accessible name of the group (required). */
4
+ label: string;
5
+ /** The content. */
6
+ children?: Snippet;
7
+ [key: string]: unknown;
8
+ };
9
+ declare const ConvertDiv: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type ConvertDiv = ReturnType<typeof ConvertDiv>;
11
+ export default ConvertDiv;
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ // ConvertFromDiv component
3
+ //
4
+ // A headless part of ConvertDiv: the source side of a conversion, holding the value and unit to convert from. Contract: a plain <div> with the base class and children.
5
+ // Converts nothing and carries no strings of its own; the consumer supplies the controls.
6
+ import type { Snippet } from "svelte";
7
+
8
+ let {
9
+ class: className = "",
10
+ children,
11
+ ...restProps
12
+ }: {
13
+
14
+ /** The content. */
15
+ children?: Snippet;
16
+ [key: string]: unknown;
17
+ } = $props();
18
+ </script>
19
+
20
+ <!-- ConvertFromDiv.svelte -->
21
+ <div
22
+ class={`convert-from-div ${className}`}
23
+ {...restProps}
24
+ >
25
+ {@render children?.()}
26
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { Snippet } from "svelte";
2
+ type $$ComponentProps = {
3
+ /** The content. */
4
+ children?: Snippet;
5
+ [key: string]: unknown;
6
+ };
7
+ declare const ConvertFromDiv: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type ConvertFromDiv = ReturnType<typeof ConvertFromDiv>;
9
+ export default ConvertFromDiv;
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ // ConvertFromUnitDiv component
3
+ //
4
+ // A headless part of ConvertDiv: the unit to convert from, such as a select box of currencies or lengths. Contract: a plain <div> with the base class and children.
5
+ // Converts nothing and carries no strings of its own; the consumer supplies the controls.
6
+ import type { Snippet } from "svelte";
7
+
8
+ let {
9
+ class: className = "",
10
+ children,
11
+ ...restProps
12
+ }: {
13
+
14
+ /** The content. */
15
+ children?: Snippet;
16
+ [key: string]: unknown;
17
+ } = $props();
18
+ </script>
19
+
20
+ <!-- ConvertFromUnitDiv.svelte -->
21
+ <div
22
+ class={`convert-from-unit-div ${className}`}
23
+ {...restProps}
24
+ >
25
+ {@render children?.()}
26
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { Snippet } from "svelte";
2
+ type $$ComponentProps = {
3
+ /** The content. */
4
+ children?: Snippet;
5
+ [key: string]: unknown;
6
+ };
7
+ declare const ConvertFromUnitDiv: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type ConvertFromUnitDiv = ReturnType<typeof ConvertFromUnitDiv>;
9
+ export default ConvertFromUnitDiv;
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ // ConvertFromValueDiv component
3
+ //
4
+ // A headless part of ConvertDiv: the value to convert from, such as a text input holding the number of dollars. Contract: a plain <div> with the base class and children.
5
+ // Converts nothing and carries no strings of its own; the consumer supplies the controls.
6
+ import type { Snippet } from "svelte";
7
+
8
+ let {
9
+ class: className = "",
10
+ children,
11
+ ...restProps
12
+ }: {
13
+
14
+ /** The content. */
15
+ children?: Snippet;
16
+ [key: string]: unknown;
17
+ } = $props();
18
+ </script>
19
+
20
+ <!-- ConvertFromValueDiv.svelte -->
21
+ <div
22
+ class={`convert-from-value-div ${className}`}
23
+ {...restProps}
24
+ >
25
+ {@render children?.()}
26
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { Snippet } from "svelte";
2
+ type $$ComponentProps = {
3
+ /** The content. */
4
+ children?: Snippet;
5
+ [key: string]: unknown;
6
+ };
7
+ declare const ConvertFromValueDiv: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type ConvertFromValueDiv = ReturnType<typeof ConvertFromValueDiv>;
9
+ export default ConvertFromValueDiv;
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ // ConvertIntoDiv component
3
+ //
4
+ // A headless part of ConvertDiv: the target side of a conversion, holding the value and unit to convert into. Contract: a plain <div> with the base class and children.
5
+ // Converts nothing and carries no strings of its own; the consumer supplies the controls.
6
+ import type { Snippet } from "svelte";
7
+
8
+ let {
9
+ class: className = "",
10
+ children,
11
+ ...restProps
12
+ }: {
13
+
14
+ /** The content. */
15
+ children?: Snippet;
16
+ [key: string]: unknown;
17
+ } = $props();
18
+ </script>
19
+
20
+ <!-- ConvertIntoDiv.svelte -->
21
+ <div
22
+ class={`convert-into-div ${className}`}
23
+ {...restProps}
24
+ >
25
+ {@render children?.()}
26
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { Snippet } from "svelte";
2
+ type $$ComponentProps = {
3
+ /** The content. */
4
+ children?: Snippet;
5
+ [key: string]: unknown;
6
+ };
7
+ declare const ConvertIntoDiv: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type ConvertIntoDiv = ReturnType<typeof ConvertIntoDiv>;
9
+ export default ConvertIntoDiv;
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ // ConvertIntoUnitDiv component
3
+ //
4
+ // A headless part of ConvertDiv: the unit to convert into, such as a select box of currencies or lengths. Contract: a plain <div> with the base class and children.
5
+ // Converts nothing and carries no strings of its own; the consumer supplies the controls.
6
+ import type { Snippet } from "svelte";
7
+
8
+ let {
9
+ class: className = "",
10
+ children,
11
+ ...restProps
12
+ }: {
13
+
14
+ /** The content. */
15
+ children?: Snippet;
16
+ [key: string]: unknown;
17
+ } = $props();
18
+ </script>
19
+
20
+ <!-- ConvertIntoUnitDiv.svelte -->
21
+ <div
22
+ class={`convert-into-unit-div ${className}`}
23
+ {...restProps}
24
+ >
25
+ {@render children?.()}
26
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { Snippet } from "svelte";
2
+ type $$ComponentProps = {
3
+ /** The content. */
4
+ children?: Snippet;
5
+ [key: string]: unknown;
6
+ };
7
+ declare const ConvertIntoUnitDiv: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type ConvertIntoUnitDiv = ReturnType<typeof ConvertIntoUnitDiv>;
9
+ export default ConvertIntoUnitDiv;
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ // ConvertIntoValueDiv component
3
+ //
4
+ // A headless part of ConvertDiv: the converted value, such as a text input showing the number of euros. Contract: a plain <div> with the base class and children.
5
+ // Converts nothing and carries no strings of its own; the consumer supplies the controls.
6
+ import type { Snippet } from "svelte";
7
+
8
+ let {
9
+ class: className = "",
10
+ children,
11
+ ...restProps
12
+ }: {
13
+
14
+ /** The content. */
15
+ children?: Snippet;
16
+ [key: string]: unknown;
17
+ } = $props();
18
+ </script>
19
+
20
+ <!-- ConvertIntoValueDiv.svelte -->
21
+ <div
22
+ class={`convert-into-value-div ${className}`}
23
+ {...restProps}
24
+ >
25
+ {@render children?.()}
26
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { Snippet } from "svelte";
2
+ type $$ComponentProps = {
3
+ /** The content. */
4
+ children?: Snippet;
5
+ [key: string]: unknown;
6
+ };
7
+ declare const ConvertIntoValueDiv: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type ConvertIntoValueDiv = ReturnType<typeof ConvertIntoValueDiv>;
9
+ export default ConvertIntoValueDiv;
@@ -54,6 +54,7 @@
54
54
  // - WAI-ARIA dialog role: https://www.w3.org/TR/wai-aria-1.2/#dialog
55
55
 
56
56
  import type { Snippet } from "svelte";
57
+ import { modalDialog } from "./modal.js";
57
58
 
58
59
  let {
59
60
  class: className = "",
@@ -80,17 +81,29 @@
80
81
  open = false;
81
82
  }
82
83
  }
84
+ function oncancel(event: Event) {
85
+ event.preventDefault();
86
+ open = false;
87
+ }
88
+ function onclose() {
89
+ open = false;
90
+ }
83
91
  </script>
84
92
 
85
93
  <!-- Dialog.svelte -->
86
94
  {#if open}
95
+ <!-- A modal dialog is opened by `showModal()` (see modal.ts), so the `open` attribute is only rendered for a
96
+ non-modal one; the keydown and cancel handlers keep `open` in step with a native Escape or `<form method="dialog">`. -->
87
97
  <dialog
88
98
  class={`dialog ${className}`}
89
- open
99
+ open={modal ? undefined : true}
90
100
  tabindex="-1"
91
101
  aria-label={label}
92
102
  aria-modal={modal || undefined}
103
+ use:modalDialog={modal}
93
104
  {onkeydown}
105
+ oncancel={oncancel}
106
+ onclose={onclose}
94
107
  {...restProps}
95
108
  >
96
109
  {@render children?.()}
@@ -0,0 +1,4 @@
1
+ export declare function modalDialog(node: HTMLDialogElement, enabled: boolean): {
2
+ update(next: boolean): void;
3
+ destroy(): void;
4
+ };
@@ -0,0 +1,47 @@
1
+ // Show a <dialog> as a real modal, or as a plain open dialog.
2
+ //
3
+ // A modal dialog is opened with `showModal()`: the browser puts it in the top layer, makes the rest of the page
4
+ // inert (no pointer, no keyboard, no assistive-technology access behind it), traps Tab inside it, moves focus to
5
+ // it and returns focus to the opener when it closes. Setting the `open` attribute does none of that, so a dialog
6
+ // that only sets `open` and says `aria-modal="true"` claims a modality it does not have.
7
+ //
8
+ // Where `showModal` is missing (jsdom, very old browsers) the dialog falls back to the `open` attribute, which
9
+ // keeps it visible and testable. Used as `use:modalDialog={modal}`.
10
+ export function modalDialog(node, enabled) {
11
+ let modal = enabled;
12
+ // Where focus was when the dialog opened. A native close() would hand focus back there, but Svelte takes the element
13
+ // out of the page before this action's cleanup runs, so by then the browser can no longer do it: we do.
14
+ const opener = typeof document === "undefined" ? null : document.activeElement;
15
+ function show() {
16
+ if (node.open)
17
+ return;
18
+ if (modal && typeof node.showModal === "function")
19
+ node.showModal();
20
+ else
21
+ node.setAttribute("open", "");
22
+ }
23
+ function hide() {
24
+ if (!node.open)
25
+ return;
26
+ if (typeof node.close === "function")
27
+ node.close();
28
+ else
29
+ node.removeAttribute("open");
30
+ }
31
+ show();
32
+ return {
33
+ update(next) {
34
+ if (next === modal)
35
+ return;
36
+ // Modal and non-modal are different openings: close the old one and open the new one.
37
+ hide();
38
+ modal = next;
39
+ show();
40
+ },
41
+ destroy() {
42
+ hide();
43
+ if (opener && opener !== document.body && opener.isConnected)
44
+ opener.focus();
45
+ },
46
+ };
47
+ }
@@ -47,6 +47,7 @@
47
47
  // - WAI-ARIA Dialog Pattern: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
48
48
  // - MDN dialog element: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog
49
49
  import type { Snippet } from "svelte";
50
+ import { modalDialog } from "../Dialog/modal.js";
50
51
  let {
51
52
  class: className = "",
52
53
  label,
@@ -71,7 +72,12 @@
71
72
  {#if open}
72
73
  <dialog
73
74
  class={`file-dialog ${className}`}
74
- open
75
+ use:modalDialog={true}
76
+ oncancel={(event) => {
77
+ event.preventDefault();
78
+ open = false;
79
+ }}
80
+ onclose={() => (open = false)}
75
81
  tabindex="-1"
76
82
  aria-label={label}
77
83
  {onkeydown}
@@ -0,0 +1,74 @@
1
+ <script lang="ts">
2
+ // HeatmapChartWithYearOfDays component
3
+ //
4
+ // A headless chart container. The consumer supplies the inline svg as
5
+ // `children`; the component draws nothing. The graphic is a
6
+ // role="img" wrapper named by `label`. The optional `dataTable` snippet
7
+ // renders the accessible table alternative as a SIBLING of that image
8
+ // wrapper, never inside it: a role="img" element makes its descendants
9
+ // presentational, so a table placed inside would be invisible to
10
+ // assistive technology.
11
+ //
12
+ // Props:
13
+ // class — string, optional. Consumer class appended after the base class.
14
+ // label — string, required. Accessible name of the chart image.
15
+ // year — number, required. The calendar year the chart shows (data-year).
16
+ // weekStart — number, required. First weekday row, 0 = Sunday … 6 = Saturday
17
+ // (data-week-start). Locale-dependent, so there is no default.
18
+ // children — Snippet. The consumer-supplied inline svg.
19
+ // dataTable — Snippet, optional. The accessible table alternative.
20
+ // ...restProps — spread onto the root <figure>.
21
+ //
22
+ // Markup:
23
+ // <figure class="heatmap-chart-with-year-of-days">
24
+ // <div class="heatmap-chart-with-year-of-days-graphic" role="img" aria-label>…svg…</div>
25
+ // <div class="heatmap-chart-with-year-of-days-data-table">…table…</div> (only when provided)
26
+ // </figure>
27
+ //
28
+ // Keyboard: none on the graphic; the data table follows native table behaviour.
29
+ //
30
+ // Claude rules:
31
+ // - Headless: no CSS, no inline styles; no drawing.
32
+ // - No hardcoded user-facing strings.
33
+
34
+ import type { Snippet } from "svelte";
35
+
36
+ let {
37
+ class: className = "",
38
+ label,
39
+ year,
40
+ weekStart,
41
+ children,
42
+ dataTable,
43
+ ...restProps
44
+ }: {
45
+ /** Accessible name for the chart. */
46
+ label: string;
47
+ /** The calendar year shown. */
48
+ year: number;
49
+ /** First weekday row: 0 = Sunday … 6 = Saturday. */
50
+ weekStart: number;
51
+ /** The consumer-supplied inline svg. */
52
+ children: Snippet;
53
+ /** Optional accessible data table alternative. */
54
+ dataTable?: Snippet;
55
+ [key: string]: unknown;
56
+ } = $props();
57
+ </script>
58
+
59
+ <!-- HeatmapChartWithYearOfDays.svelte -->
60
+ <figure
61
+ class={`heatmap-chart-with-year-of-days ${className}`}
62
+ data-year={year}
63
+ data-week-start={weekStart}
64
+ {...restProps}
65
+ >
66
+ <div class="heatmap-chart-with-year-of-days-graphic" role="img" aria-label={label}>
67
+ {@render children?.()}
68
+ </div>
69
+ {#if dataTable}
70
+ <div class="heatmap-chart-with-year-of-days-data-table">
71
+ {@render dataTable()}
72
+ </div>
73
+ {/if}
74
+ </figure>
@@ -0,0 +1,17 @@
1
+ import type { Snippet } from "svelte";
2
+ type $$ComponentProps = {
3
+ /** Accessible name for the chart. */
4
+ label: string;
5
+ /** The calendar year shown. */
6
+ year: number;
7
+ /** First weekday row: 0 = Sunday … 6 = Saturday. */
8
+ weekStart: number;
9
+ /** The consumer-supplied inline svg. */
10
+ children: Snippet;
11
+ /** Optional accessible data table alternative. */
12
+ dataTable?: Snippet;
13
+ [key: string]: unknown;
14
+ };
15
+ declare const HeatmapChartWithYearOfDays: import("svelte").Component<$$ComponentProps, {}, "">;
16
+ type HeatmapChartWithYearOfDays = ReturnType<typeof HeatmapChartWithYearOfDays>;
17
+ export default HeatmapChartWithYearOfDays;
@@ -0,0 +1,108 @@
1
+ <script lang="ts">
2
+ // MiddleTruncation component
3
+ //
4
+ // A string shortened in the middle to fit the width its container gives it ("report-fi…q3.pdf"), keeping the start and the
5
+ // end. The width is measured in the browser (canvas text metrics in the element's own font) and re-measured when the
6
+ // container resizes; until then, and on the server, the full text is shown.
7
+ //
8
+ // The full text always stays in the accessibility tree: the shortened string is aria-hidden and the whole text sits in a
9
+ // sibling the consumer visually hides. A screen reader, find-in-page and copy therefore get the real text, which a
10
+ // title attribute (hover only) or a text-only truncation would lose.
11
+ //
12
+ // Props:
13
+ // class — string, optional. Consumer class appended after the base class.
14
+ // text — string, required. The full text.
15
+ // ellipsis — string, required. The cut marker. No default: the headless layer ships no text.
16
+ // strategy — "even" | "fixed-end" | "min-end", default "even".
17
+ // even: start and end share the room. fixed-end: exactly `end` characters at the end. min-end: even, but at least `end` at the end.
18
+ // end — number, optional. Characters kept at the end for "fixed-end" and "min-end".
19
+ // ...restProps — spread onto the root <span>.
20
+ //
21
+ // Markup:
22
+ // <span class="middle-truncation" data-strategy data-truncated?>
23
+ // <span class="middle-truncation-visual" aria-hidden="true">report-fi…q3.pdf</span>
24
+ // <span class="middle-truncation-full">report-final-q3.pdf</span>
25
+ // </span>
26
+ //
27
+ // Consumer CSS (the component ships none): the root must be a block with a bounded width, `overflow: hidden` and
28
+ // `white-space: nowrap`, and `.middle-truncation-full` must be visually hidden (clipped, not display:none).
29
+ //
30
+ // Keyboard: none.
31
+ //
32
+ // Claude rules:
33
+ // - Headless: no CSS, no inline styles.
34
+ // - No hardcoded user-facing strings: the ellipsis is a required prop.
35
+
36
+ import { onMount } from "svelte";
37
+ import { truncateMiddle, type TruncationStrategy } from "./middle-truncation";
38
+
39
+ let {
40
+ class: className = "",
41
+ text,
42
+ ellipsis,
43
+ strategy = "even",
44
+ end,
45
+ ...restProps
46
+ }: {
47
+ /** The full text. */
48
+ text: string;
49
+ /** The cut marker; no default. */
50
+ ellipsis: string;
51
+ /** How the room is shared between start and end. */
52
+ strategy?: TruncationStrategy;
53
+ /** Characters kept at the end for "fixed-end" and "min-end". */
54
+ end?: number;
55
+ [key: string]: unknown;
56
+ } = $props();
57
+
58
+ let root: HTMLSpanElement | undefined = $state();
59
+ let shown: string | null = $state(null);
60
+ const displayed = $derived(shown ?? text);
61
+
62
+ let context: CanvasRenderingContext2D | null | undefined;
63
+
64
+ function recalculate(): void {
65
+ if (!root) return;
66
+ const width = root.clientWidth;
67
+ if (context === undefined) context = document.createElement("canvas").getContext("2d");
68
+ if (!context || width <= 0) {
69
+ shown = null;
70
+ return;
71
+ }
72
+ const style = getComputedStyle(root);
73
+ context.font = `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
74
+ const ctx = context;
75
+ const result = truncateMiddle(text, { ellipsis, strategy, end, maxWidth: width, measure: (s) => ctx.measureText(s).width });
76
+ shown = result === text ? null : result;
77
+ }
78
+
79
+ // Re-measure whenever the text or the options change.
80
+ $effect(() => {
81
+ text; ellipsis; strategy; end;
82
+ recalculate();
83
+ });
84
+
85
+ // Re-measure when the container resizes, at most once per animation frame.
86
+ onMount(() => {
87
+ if (!root || typeof ResizeObserver === "undefined") return;
88
+ let frame = 0;
89
+ const observer = new ResizeObserver(() => {
90
+ cancelAnimationFrame(frame);
91
+ frame = requestAnimationFrame(recalculate);
92
+ });
93
+ observer.observe(root);
94
+ return () => {
95
+ cancelAnimationFrame(frame);
96
+ observer.disconnect();
97
+ };
98
+ });
99
+ </script>
100
+
101
+ <!-- MiddleTruncation.svelte -->
102
+ <span
103
+ bind:this={root}
104
+ class={`middle-truncation ${className}`}
105
+ data-strategy={strategy}
106
+ data-truncated={shown === null ? undefined : ""}
107
+ {...restProps}
108
+ ><span class="middle-truncation-visual" aria-hidden="true">{displayed}</span><span class="middle-truncation-full">{text}</span></span>