@lilydesignsystem/svelte-headless 0.5.1 → 0.6.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.
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).
package/README.md ADDED
@@ -0,0 +1,453 @@
1
+ # Lily Design System™ - Svelte Headless
2
+
3
+ A headless Svelte 5 component library with 490 components. All components are headless (no built-in styles), fully accessible (WCAG 2.2 AAA), and use Svelte 5 runes.
4
+
5
+ ## Components
6
+
7
+ - **accordion**:
8
+ - **accordion-nav**: a navigation container for collapsible accordion information
9
+ - **accordion-list**: an ordered list of accordion list item components
10
+ - **accordion-list-item**: one accordion list item component
11
+ - **accordion-link**: one accordion link in the trail
12
+ - **action-link**: a hyperlink styled as an action trigger
13
+ - **ai-label**: an indicator of AI instances that is a pathway to AI explainability
14
+ - **alert-dialog**: a modal dialog for urgent messages requiring user acknowledgment
15
+ - **alert**: a status message for important information or feedback
16
+ - **angle-slider-range-input**: a range input for selecting an angle in degrees
17
+ - **aspect-ratio-container**: a container that maintains a fixed aspect ratio
18
+ - **avatar**:
19
+ - **avatar**: an avatar indicator that shows an avatar image or avatar text
20
+ - **avatar-image**: an avatar indicator inside image such as a user photo
21
+ - **avatar-text**: an avatar indicator inner text such as a user name
22
+ - **back-link**: a navigation link to return to a previous page
23
+ - **badge**: a small label for counts, statuses, or categories
24
+ - **banner**: a prominent message bar across the top of a page
25
+ - **banner-box**: a banner box that is inside a banner component, using flexbox horizontal
26
+ - **beach-ball**: a decorative animated beach ball element
27
+ - **breadcrumb**:
28
+ - **breadcrumb-nav**: a navigation container for breadcrumb trail links
29
+ - **breadcrumb-list**: an ordered list of breadcrumb navigation items
30
+ - **breadcrumb-list-item**: one breadcrumb navigation link in the trail
31
+ - **breadcrumb-link**: one breadcrumb link in the trail
32
+ - **button**: a generic clickable button element
33
+ - **button-input**: an input element of type button for form actions
34
+ - **calendar-table**:
35
+ - **calendar-table**: a calendar table interactive grid for managing dates, days, etc.
36
+ - **calendar-table-head**: a calendar table interactive grid thead for managing dates, days, etc.
37
+ - **calendar-table-body**: a calendar table interactive grid tbody for managing dates, days, etc.
38
+ - **calendar-table-foot**: a calendar table interactive grid tfoot for managing dates, days, etc.
39
+ - **calendar-table-th**: a calendar table interactive grid column for managing dates, days, etc.
40
+ - **calendar-table-row**: a calendar table interactive grid row for managing dates, days, etc.
41
+ - **calendar-table-td**: a calendar table interactive grid data cell for managing dates, days, etc.
42
+ - **calendar-range-picker**: a picker for selecting a date range on a calendar
43
+ - **call-to-action**: a prominent prompt encouraging user action
44
+ - **caption**: a caption for a table or figure element
45
+ - **card**: a grouped content container with header, body, and footer areas
46
+ - **care-card**: a medical care instruction card with urgency levels
47
+ - **carousel**: a slideshow for cycling through content items
48
+ - **character-counter**: a counter showing remaining or used characters in a text field
49
+ - **character**: a single character display element
50
+ - **chat**:
51
+ - **chat-nav**: a navigation container for chat information
52
+ - **chat-list**: an ordered list of chat list item components
53
+ - **chat-list-item**: one chat list item component, typically containing one chat message component
54
+ - **chat-message**: a chat message shows one chat conversation message entry and all its data, including the author avatar, author name, time, etc.
55
+ - **citation**: a citation acknowledges the relevance of a work of another to the topic of discussion
56
+ - **checkbox-group**: a group component that manages a collection of checkboxes with shared state
57
+ - **checkbox-input**: a checkbox input for toggling a boolean value
58
+ - **check-list**: an ordered list of check list item components
59
+ - **check-list-item**: one check list item component with a checkbox
60
+ - **clipboard-copy-button**: a button that copies text to the clipboard
61
+ - **collapsible**: a container that can be expanded or collapsed
62
+ - **code**: an inline code span for displaying short code snippets within surrounding text
63
+ - **code-block**: a block of formatted code with optional line numbers and line highlighting
64
+ - **color-input**: an input for selecting a color value
65
+ - **color-picker**: a two-dimensional board for picking colors by hue and saturation
66
+ - **color-picker-button**: a button showing a color swatch in a color picker
67
+ - **comment**: a comment displays user discussions and user feedback
68
+ - **combobox**: a text input combined with a dropdown list for filtering options
69
+ - **command**: a command palette for searching and executing actions
70
+ - **contents-nav**: a navigation landmark for a table of contents
71
+ - **contents-list**: an ordered list of contents list item components
72
+ - **contents-list-item**: one contents list item component linking to a page section
73
+ - **contents-link**: one contents link in the trail
74
+ - **context-menu**: a menu that appears on right-click or long-press
75
+ - **context-menu-item**: one item in a context menu
76
+ - **currency-input**: a locale-aware currency input with automatic formatting, symbols, separators
77
+ - **data-filter-form**: a form for filtering data by criteria
78
+ - **data-table**:
79
+ - **data-table**: a data table interactive grid for displaying and sorting tabular data
80
+ - **data-table-head**: a data table interactive grid thead for displaying and sorting tabular data
81
+ - **data-table-body**: a data table interactive grid tbody for displaying and sorting tabular data
82
+ - **data-table-foot**: a data table interactive grid tfoot for displaying and sorting tabular data
83
+ - **data-table-th**: a data table interactive grid column for displaying and sorting tabular data
84
+ - **data-table-row**: a data table interactive grid row for displaying and sorting tabular data
85
+ - **data-table-td**: a data table interactive grid data cell for displaying and sorting tabular data
86
+ - **date-field**: a structured field for entering date components
87
+ - **date-input**: an input for entering a date value
88
+ - **date-range**: paired start and end date inputs
89
+ - **date-time-now-input**: an input for entering a date and time and "now" button
90
+ - **date-time-local-input**: an input for entering a date and time without time zone
91
+ - **details**: a disclosure widget that shows and hides content
92
+ - **dial**: a rotary dial control for selecting a value
93
+ - **dial-group**: a group of dial components
94
+ - **diff**: a side-by-side comparison of two items
95
+ - **digital-object-identifier-link**: a permanent hyperlink for a Digital Object Identifier (DOI) to an electronic source
96
+ - **dialog**: a modal or non-modal dialog window
97
+ - **do-list**: a guideline list of encouraged do-list-item components
98
+ - **do-list-item**: an encouraged-practice item in a do-list guideline list
99
+ - **dont-list**: a guideline list of discouraged dont-list-item components
100
+ - **dont-list-item**: a discouraged-practice item in a dont-list guideline list
101
+ - **drawer**: a panel that slides in from the edge of the screen
102
+ - **dropdown-menu**: a menu that opens below a trigger button
103
+ - **editable-form**: a form wrapper for inline editing of content
104
+ - **editable**: an inline-editable text element that toggles between view and edit modes
105
+ - **email-link**: a mailto hyperlink for an email address
106
+ - **email-input**: an input for entering an email address
107
+ - **emoji-character-picker**: a picker for browsing and selecting emoji characters
108
+ - **emoji**: a single emoji character with accessible name
109
+ - **error-message**: an error message associated with a form field
110
+ - **event**: an event component that contains other event-related information
111
+ - **error-summary**: a summary of all validation errors on a form
112
+ - **expander**: a control that expands to reveal more content
113
+ - **field**: a form field wrapper with label, input, and error message
114
+ - **fieldset**: a group of related form fields with a legend
115
+ - **figure**: a self-contained figure with optional caption
116
+ - **file-dialog**: a dialog for browsing and selecting files
117
+ - **file-input**: an input for selecting files from the file system
118
+ - **file-manager**: a file browser for navigating and managing files
119
+ - **file-upload**: a drag-and-drop area for uploading files
120
+ - **five-face-rating**:
121
+ - **five-face-rating-view**: a read-only display of a five-face satisfaction rating
122
+ - **five-face-rating-picker**: a picker for selecting a 1-5 satisfaction rating using face labels
123
+ - **five-face-rating-picker-button**: a picker button for selecting a 1-5 satisfaction rating using face labels
124
+ - **five-star-rating**:
125
+ - **five-star-rating-view**: a read-only display of a five-star rating
126
+ - **five-star-rating-picker**: a picker for selecting a 1-5 star rating using radio buttons
127
+ - **five-star-rating-picker-button**: a picker button for selecting a 1-5 star rating using radio buttons
128
+ - **flair**: a decorative highlight or emphasis element
129
+ - **floating-panel**: a panel that floats above page content
130
+ - **footer**: a page or section footer area
131
+ - **footnote**: a footnote reference and content element
132
+ - **form**: a form element for collecting and submitting user data
133
+ - **gantt-table**:
134
+ - **gantt-table**: a Gantt chart table interactive grid for planning schedule visualization
135
+ - **gantt-table-thead**: a Gantt chart table interactive grid thead for planning schedule visualization
136
+ - **gantt-table-tbody**: a Gantt chart table interactive grid tbody for planning schedule visualization
137
+ - **gantt-table-tfoot**: a Gantt chart table interactive grid tfoot for planning schedule visualization
138
+ - **gantt-table-th**: a Gantt chart table interactive grid column for planning schedule visualization
139
+ - **gantt-table-tr**: a Gantt chart table interactive grid row for planning schedule visualization
140
+ - **gantt-table-td**: a Gantt chart table interactive grid data cell for planning schedule visualization
141
+ - **hamburger-menu**: a toggle button that opens a mobile navigation menu
142
+ - **header**: a page or section header area
143
+ - **hero**: a large box or image with a title and description
144
+ - **hidden-input**: a hidden input for including data in form submission
145
+ - **hint**: hint text providing guidance for a form field
146
+ - **grail-layout**:
147
+ - **grail-layout**: a responsive web design structure with header, left aside, center main, right aside, footer
148
+ - **grail-layout-top-header**: grail layout top header full width with header tag
149
+ - **grail-layout-left-aside**: grail layout left aside sidebar with aside tag
150
+ - **grail-layout-center-main**: grail layout center main content with main tag
151
+ - **grail-layout-right-aside**: grail layout right aside sidebar with aside tag
152
+ - **grail-layout-bottom-footer**: grail layout bottom footer full width with footer tag
153
+ - **hover-card**: a card that appears on hover over a trigger element
154
+ - **icon**: a container for displaying an icon
155
+ - **image-file-input**: an input for selecting image files with preview
156
+ - **image**: an image element with alt text
157
+ - **image-input**: a graphical submit button that displays an image
158
+ - **information-callout**: a callout box highlighting informational content
159
+ - **input**: a generic HTML input element
160
+ - **inset-text**: indented text to distinguish it from surrounding content
161
+ - **kanban-table**:
162
+ - **kanban-table**: a kanban board table interactive grid for organizing items by status
163
+ - **kanban-table-head**: a kanban board table interactive grid thead for organizing items by status
164
+ - **kanban-table-body**: a kanban board table interactive grid tbody for organizing items by status
165
+ - **kanban-table-foot**: a kanban board table interactive grid tfoot for organizing items by status
166
+ - **kanban-table-th**: a kanban board table interactive grid column for organizing items by status
167
+ - **kanban-table-row**: a kanban board table interactive grid row for organizing items by status
168
+ - **kanban-table-td**: kanban board table interactive grid data cell for organizing items by status
169
+ - **kbd**: show keyboard shortcuts and key combinations in a styled inline element
170
+ - **label**: a label associated with a form input
171
+ - **listbox**: a list of selectable options with keyboard navigation
172
+ - **loading**: a loading indicator, such as text, or image, or animation
173
+ - **measurement**:
174
+ - **measurement-instance-input**: an input for entering a measurement value and unit
175
+ - **measurement-instance-view**: a read-only display of a measurement value and unit
176
+ - **measurement-system-input**: an input for selecting a measurement system
177
+ - **measurement-system-view**: a read-only display of a measurement system
178
+ - **measurement-unit-input**: an input for selecting a measurement unit
179
+ - **measurement-unit-view**: a read-only display of a measurement unit
180
+ - **medical-banner**: a prominent message bar across the top of a page, with medical information
181
+ - **medical-banner-box**: a medical banner box inside a medical-banner, using flexbox horizontal
182
+ - **medical-banner-box-for-danger**: a medical record banner box for danger information
183
+ - **medical-banner-box-for-advice**: a medical record banner box for advice information
184
+ - **menu**: a list of actions or options triggered by a button
185
+ - **menu-item**: one item in a menu
186
+ - **menu-bar**: a horizontal bar of menu triggers
187
+ - **menu-bar-button**: one item in a menu bar
188
+ - **meter**: a gauge displaying a scalar value within a known range
189
+ - **mockup-browser**: a box area that looks like a web browser
190
+ - **mockup-laptop**: a box area that looks like a laptop computer
191
+ - **mockup-phone-portrait**: a box area that looks like a mobile phone
192
+ - **mockup-tablet-landscape**: a box area that looks like a tablet computer in landscape mode
193
+ - **mockup-tablet-portrait**: a box area that looks like a tablet computer in portrait mode
194
+ - **mockup-shell**: a box area that looks like a terminal shell
195
+ - **mockup-watch**: a box area that looks like a smart watch
196
+ - **mockup-window**: a box area that looks like a desktop window
197
+ - **month-input**: an input for selecting a month and year
198
+ - **navigation-menu**: a site-wide navigation menu with links
199
+ - **net-promoter-score**:
200
+ - **net-promoter-score-view**: a read-only display of a Net Promoter Score
201
+ - **net-promoter-score-picker**: a picker for selecting a 0-10 Net Promoter Score
202
+ - **net-promoter-score-picker-button**: a picker button for selecting a 0-10 Net Promoter Score
203
+ - **notification**: a brief message about an event or update
204
+ - **number-input**: an input for entering a numeric value with validation
205
+ - **option**: an option element within a select dropdown
206
+ - **organization**: an organization component that contains other organization-related information
207
+ - **pagination**:
208
+ - **pagination-nav**: an ordered list of page navigation links
209
+ - **pagination-list**: an ordered list of page navigation links
210
+ - **pagination-list-item**: one page link in a pagination list
211
+ - **pagination-link**: one pagination link in the trail
212
+ - **panel**: a generic content panel with optional heading
213
+ - **person**: a person component that contains other person-related information
214
+ - **password-input**: an input for entering a password with obscured text
215
+ - **password-input-or-text-input-div**: an input for entering a password or text with show/hide toggle
216
+ - **place**: a place component that contains other place-related information
217
+ - **pin-input-div**: a series of single-digit inputs for entering a PIN or OTP code
218
+ - **popover**: a floating content box anchored to a trigger element
219
+ - **popup**: a temporary overlay that appears above page content
220
+ - **postal-code**:
221
+ - **postal-code-input**: an input for entering a postal or ZIP code
222
+ - **postal-code-view**: a read-only display of a postal or ZIP code
223
+ - **progress-circle**: a circular progress indicator
224
+ - **progress-spinner**: an indeterminate spinning progress indicator
225
+ - **progress**: a horizontal progress bar showing completion
226
+ - **qr-code**: a QR code image generated from text or URL data
227
+ - **radio-group**: a group of radio buttons for selecting one option
228
+ - **radio-input**: a single radio button input
229
+ - **range-input**: a slider input for selecting a value within a range
230
+ - **red-amber-green**:
231
+ - **red-amber-green-view**: a read-only display of a red/amber/green status
232
+ - **red-amber-green-picker**: a picker for selecting a red/amber/green status
233
+ - **red-amber-green-picker-button**: a picker button for selecting a red/amber/green status
234
+ - **red-orange-yellow-green-blue**:
235
+ - **red-orange-yellow-green-blue-view**: a read-only display of a five-level color status
236
+ - **red-orange-yellow-green-blue-picker**: a picker for selecting a five-level color status
237
+ - **red-orange-yellow-green-blue-picker-button**: a picker button for selecting a five-level color status
238
+ - **reset-input**: a button input that resets a form to default values
239
+ - **resizable**: a container that the user can resize by dragging
240
+ - **review-date**: a display of a content review date
241
+ - **screen-reader-span**: a visually hidden span of text intended for screen readers to provide more descriptive labels
242
+ - **scroll-area**: a scrollable container with custom scrollbar support
243
+ - **scroll-bar**: a custom scrollbar element
244
+ - **search-input**: an input for entering a search query
245
+ - **segment-group**: a group of mutually exclusive segment options
246
+ - **segment-group-item**: one selectable segment in a segment group
247
+ - **select-with-extras**: a select dropdown with additional features like search or groups
248
+ - **select**: a dropdown select element for choosing one option
249
+ - **separator**: a horizontal or vertical divider between content sections
250
+ - **sheet**: a panel that slides in from a screen edge as an overlay
251
+ - **sidebar**: a side panel for navigation or supplementary content
252
+ - **signature-pad**: a drawing area for capturing a handwritten signature
253
+ - **skeleton**: a placeholder loading animation for content
254
+ - **skip-link**: a hidden link for keyboard users to skip to main content
255
+ - **slide-out-drawer**: a drawer that slides out from the side of the page
256
+ - **slider**: a draggable control for selecting a value along a track
257
+ - **slider-button**: a button with a slider that the user needs to slide to confirm
258
+ - **sonner**: a toast notification manager
259
+ - **sparkline**: a small inline chart showing a data trend
260
+ - **splitter**: a draggable divider for resizing adjacent panels
261
+ - **submit-input**: a button input that submits a form
262
+ - **super-banner**: a super-banner communicates a high-priority state that affects an entire app, experience, process, or system
263
+ - **summary-list**: an ordered list of key-value summary pairs
264
+ - **summary-list-item**: one key-value pair in a summary list
265
+ - **switch-button**: a toggle switch for turning a setting on or off
266
+ - **tab-bar**: a group of tabs for switching between content panels
267
+ - **tab-bar-button**: one tab button in a tab group
268
+ - **table**:
269
+ - **table**: a data table with rows and columns
270
+ - **table-head**: a table header section
271
+ - **table-body**: a table body section
272
+ - **table-foot**: a table footer section
273
+ - **table-th**: a table column
274
+ - **table-row**: a table row
275
+ - **table-td**: a table data cell
276
+ - **tag-group**: a group of tag elements
277
+ - **tag**: a keyword label for categorizing content
278
+ - **tag-input**: an input for adding and removing tags
279
+ - **task-bar**: a horizontal bar of task shortcuts or actions
280
+ - **task-bar-button**: one item in a task bar
281
+ - **task-list**: an ordered list of task list items
282
+ - **task-list-item**: one task item with a checkbox and label
283
+ - **tel-input**: an input for entering a telephone number
284
+ - **tel-link**: a tel: hyperlink for a telephone number
285
+ - **text-input**: a single-line text input field
286
+ - **text-input-with-search**: a single-line text input field with search capability
287
+ - **text-area-input**: a multi-line text input area
288
+ - **text-area-input-with-character-counter**: a multi-line text area with character counter
289
+ - **theme-select**: a select dropdown for choosing a theme
290
+ - **theme-select-option**: one option in a theme select dropdown
291
+ - **theme-view**: a read-only display of the current theme
292
+ - **time-input**: an input for entering a time value
293
+ - **time-picker-input**: an input with a dropdown for picking a time
294
+ - **timeline-list**: an ordered list of chronological events or milestones
295
+ - **timeline-list-item**: one event in a timeline list
296
+ - **tile**: a grouping container that presents related content in a structured format, often as a single clickable area
297
+ - **timer**: a countdown or elapsed time display
298
+ - **timer-button**: a button with a timer that will automatically click after a given amount of time
299
+ - **toast**: a brief auto-dismissing notification message
300
+ - **toggle-group**: a group of toggle buttons for selecting options
301
+ - **toggle-button**: a button that toggles between pressed and unpressed states
302
+ - **tool-bar**: a horizontal bar of tool actions
303
+ - **tool-bar-button**: one action button in a tool bar
304
+ - **tooltip**: a small popup showing descriptive text on hover or focus
305
+ - **tour**: a tour guide, such as for sightseeing, or pathways, or demonstrations, etc.
306
+ - **tour-list**: an ordered list of tour guide steps
307
+ - **tour-list-item**: one step in a tour guide list
308
+ - **tree-menu**: a hierarchical tree menu with expandable branches
309
+ - **tree-nav**: a hierarchical navigation with expandable branches
310
+ - **tree-list**: a hierarchical list with nested expandable items
311
+ - **tree-list-item**: one item in a tree navigation list
312
+ - **tree-link**: one tree link in the trail
313
+ - **united-kingdom**:
314
+ - **united-kingdom-national-health-service-number-input**: an input for entering a UK NHS number
315
+ - **united-kingdom-national-health-service-number-view**: a read-only display of a UK NHS number
316
+ - **united-states**:
317
+ - **united-states-social-security-number-input**: an input for entering a US Social Security number
318
+ - **united-states-social-security-number-view**: a read-only display of a US Social Security number
319
+ - **url-input**: an input for entering a URL
320
+ - **warning-callout**: a callout box highlighting a warning message
321
+ - **week-input**: an input for selecting a week and year
322
+
323
+ ## Component name patterns
324
+
325
+ - \*Bar \*BarButton
326
+ - MenuBar MenuBarButton
327
+ - TabBar TabBarButton
328
+ - TaskBar TaskBarButton
329
+ - ToolBar ToolBarButton
330
+ - \*Group \*GroupItem
331
+ - SegmentGroup SegmentGroupItem
332
+ - \*Guide \*GuideList \*GuideListItem
333
+ - Tour TourList TourListItem
334
+ - \*List \*ListItem
335
+ - CheckList CheckListItem
336
+ - ContentsList ContentsListItem
337
+ - DoList DoListItem
338
+ - DontList DontListItem
339
+ - PaginationList PaginationListItem
340
+ - SummaryList SummaryListItem
341
+ - \*Nav \*List \*ListItem
342
+ - AccordionNav AccordionList AccordionListItem
343
+ - BreadcrumbNav BreadcrumbList BreadcrumbListItem
344
+ - ContentsNav ContentsList ContentsListItem
345
+ - PaginationNav PaginationList PaginationListItem
346
+ - TreeNav TreeList TreeListItem
347
+ - *Select *SelectOption
348
+ - ThemeSelect ThemeSelectOption
349
+ - \*Menu \*MenuItem
350
+ - ContextMenu ContextMenuItem
351
+ - Menu MenuItem
352
+ - \*Input \*Link
353
+ - TelInput TelLink
354
+ - EmailInput EmailLink
355
+ - \*Picker \*PickerButton
356
+ - ColorPicker ColorPickerButton
357
+ - FiveFaceRatingPicker FiveFaceRatingPickerButton
358
+ - FiveStarRatingPicker FiveStarRatingPickerButton
359
+ - NetPromoterScorePicker NetPromoterScorePickerButton
360
+ - RedAmberGreenPicker RedAmberGreenPickerButton
361
+ - RedOrangeYellowGreenBluePicker RedOrangeYellowGreenBluePickerButton
362
+ - \*Table \*TableHead \*TableBody \*TableFoot \*TableTD \*TableRow \*TableTD
363
+ - Table TableHead TableBody TableFoot TableTD TableRow TableTD
364
+ - CalendarTable CalendarTableHead CalendarTableBody CalendarTableFoot CalendarTableTD CalendarTableRow CalendarTableTD
365
+ - DataTable DataTableHead DataTableBody DataTableFoot DataTableTD DataTableRow DataTableTD
366
+ - GanttTable GanttTableHead GanttTableBody GanttTableTfoot GanttTableTH GanttTableTr GanttTableTD
367
+ - KanbanTable KanbanTableHead KanbanTableBody KanbanTableFoot KanbanTableTD KanbanTableRow KanbanTableTD
368
+
369
+ ## Component patterns for name suffix and HTML tag
370
+
371
+ | Suffix | HTML tag |
372
+ | ---------- | ------------ |
373
+ | article | `<article>` |
374
+ | aside | `<aside>` |
375
+ | button | `<button>` |
376
+ | dialog | `<dialog>` |
377
+ | div | `<div>` |
378
+ | fieldset | `<fieldset>` |
379
+ | figure | `<figure>` |
380
+ | footer | `<footer>` |
381
+ | header | `<header>` |
382
+ | input | `<input>` |
383
+ | kbd | `<kbd>` |
384
+ | list | `<ol>` |
385
+ | list-item | `<li>` |
386
+ | main | `<main>` |
387
+ | meter | `<meter>` |
388
+ | nav | `<nav>` |
389
+ | option | `<option>` |
390
+ | picker | `<div>` |
391
+ | progress | `<progress>` |
392
+ | table | `<table>` |
393
+ | table-head | `<thead>` |
394
+ | table-body | `<tbody>` |
395
+ | table-foot | `<tfoot>` |
396
+ | table-row | `<tr>` |
397
+ | table-td | `<td>` |
398
+ | select | `<select>` |
399
+ | span | `<span>` |
400
+
401
+ ## Architecture
402
+
403
+ - **Svelte 5** with runes (`$props()`, `$state()`, `$derived()`, `$bindable()`)
404
+ - **Headless design**: no CSS, no styles -- consumers provide all styling
405
+ - **Accessible**: WCAG 2.2 AAA, ARIA attributes, keyboard navigation
406
+ - **TypeScript**: fully typed props and events
407
+ - **Internationalization**: no hardcoded user-facing strings
408
+
409
+ ## Testing
410
+
411
+ Vitest + @testing-library/svelte for component testing. No @testing-library/jest-dom.
412
+
413
+ ```sh
414
+ pnpm test # run all tests
415
+ pnpm run test:ui # open vitest UI
416
+ pnpm run test:watch # watch mode
417
+ ```
418
+
419
+ ## Accessibility
420
+
421
+ All components follow WCAG 2.2 AAA guidelines:
422
+
423
+ - **Semantic HTML**: Proper element usage (`<button>`, `<nav>`, `<dialog>`, etc.)
424
+ - **ARIA attributes**: Roles, labels, states, and properties
425
+ - **Keyboard navigation**: Full keyboard support for all interactive components
426
+ - **Screen readers**: Proper announcements via aria-live, role="alert", etc.
427
+ - **Focus management**: Logical focus order and visible focus indicators
428
+ - **Error handling**: Accessible error messages linked via aria-errormessage
429
+
430
+ ## Internationalization
431
+
432
+ No strings are hardcoded in any component. All user-facing text comes through props. Labels, placeholders, and error messages are all configurable by the consumer.
433
+
434
+ ## Related Projects
435
+
436
+ - [Lily Design System](https://github.com/LilyDesignSystem/lily) — Parent project
437
+ - [Svelte SvelteKit Examples](../lily-design-system-svelte-sveltekit-examples/) — Example app with simple styling
438
+ - [React Headless](../lily-design-system-react-headless/) — React equivalent
439
+ - [Vue Headless](../lily-design-system-vue-headless/) — Vue equivalent
440
+ - [Blazor Headless](../lily-design-system-blazor-headless/) — Blazor equivalent
441
+ - [Angular Headless](../lily-design-system-angular-headless/) — Angular 20 equivalent
442
+
443
+ ## License
444
+
445
+ MIT or Apache-2.0 or GPL-2.0 or GPL-3.0, or contact us for more options.
446
+
447
+ ## Contact
448
+
449
+ Joel Parker Henderson (joel@joelparkerhenderson.com)
450
+
451
+ ---
452
+
453
+ Lily™ and Lily Design System™ are trademarks.
@@ -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>
@@ -0,0 +1,15 @@
1
+ import { type TruncationStrategy } from "./middle-truncation";
2
+ type $$ComponentProps = {
3
+ /** The full text. */
4
+ text: string;
5
+ /** The cut marker; no default. */
6
+ ellipsis: string;
7
+ /** How the room is shared between start and end. */
8
+ strategy?: TruncationStrategy;
9
+ /** Characters kept at the end for "fixed-end" and "min-end". */
10
+ end?: number;
11
+ [key: string]: unknown;
12
+ };
13
+ declare const MiddleTruncation: import("svelte").Component<$$ComponentProps, {}, "">;
14
+ type MiddleTruncation = ReturnType<typeof MiddleTruncation>;
15
+ export default MiddleTruncation;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Middle truncation: shorten a string to fit a width by cutting out the middle, keeping the start and the end ("report-fi…q3.pdf").
3
+ * Pure and framework-neutral: the caller supplies `measure`, so the module reads no DOM and is tested without a browser.
4
+ *
5
+ * Works on grapheme clusters (Intl.Segmenter, falling back to code points), so it never splits a surrogate pair, a combining
6
+ * sequence or an emoji. The width search is binary, so it measures O(log n) candidate strings.
7
+ *
8
+ * Strategies:
9
+ * "even" the start and the end share what fits, the end taking the extra cluster when the count is odd.
10
+ * "fixed-end" exactly `end` clusters are kept at the end; the start takes all the remaining room.
11
+ * "min-end" an even split, but at least `end` clusters are kept at the end (a file extension, a domain suffix).
12
+ */
13
+ export type TruncationStrategy = "even" | "fixed-end" | "min-end";
14
+ export interface TruncateMiddleOptions {
15
+ /** The ellipsis string. Required: the module has no default text (internationalisation). */
16
+ ellipsis: string;
17
+ /** The width available to the whole result, in the unit `measure` returns. */
18
+ maxWidth: number;
19
+ /** Width of a string in the same unit as `maxWidth`. */
20
+ measure: (text: string) => number;
21
+ strategy?: TruncationStrategy;
22
+ /** Clusters kept at the end for "fixed-end" and "min-end". Ignored by "even". */
23
+ end?: number;
24
+ }
25
+ /** Split into user-perceived characters. */
26
+ export declare function splitGraphemes(text: string): string[];
27
+ /** The text shortened to fit `maxWidth`, or unchanged when it already fits. */
28
+ export declare function truncateMiddle(text: string, options: TruncateMiddleOptions): string;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Middle truncation: shorten a string to fit a width by cutting out the middle, keeping the start and the end ("report-fi…q3.pdf").
3
+ * Pure and framework-neutral: the caller supplies `measure`, so the module reads no DOM and is tested without a browser.
4
+ *
5
+ * Works on grapheme clusters (Intl.Segmenter, falling back to code points), so it never splits a surrogate pair, a combining
6
+ * sequence or an emoji. The width search is binary, so it measures O(log n) candidate strings.
7
+ *
8
+ * Strategies:
9
+ * "even" the start and the end share what fits, the end taking the extra cluster when the count is odd.
10
+ * "fixed-end" exactly `end` clusters are kept at the end; the start takes all the remaining room.
11
+ * "min-end" an even split, but at least `end` clusters are kept at the end (a file extension, a domain suffix).
12
+ */
13
+ /** Split into user-perceived characters. */
14
+ export function splitGraphemes(text) {
15
+ const Segmenter = Intl.Segmenter;
16
+ if (Segmenter)
17
+ return Array.from(new Segmenter(undefined, { granularity: "grapheme" }).segment(text), (s) => s.segment);
18
+ return Array.from(text);
19
+ }
20
+ function clampEnd(end, length) {
21
+ if (end === undefined || !Number.isFinite(end))
22
+ return 0;
23
+ return Math.min(Math.max(Math.floor(end), 0), length);
24
+ }
25
+ /** The text shortened to fit `maxWidth`, or unchanged when it already fits. */
26
+ export function truncateMiddle(text, options) {
27
+ const { ellipsis, maxWidth, measure, strategy = "even" } = options;
28
+ if (measure(text) <= maxWidth)
29
+ return text;
30
+ const g = splitGraphemes(text);
31
+ const n = g.length;
32
+ const tail = (count) => (count > 0 ? g.slice(n - count).join("") : "");
33
+ const head = (count) => g.slice(0, count).join("");
34
+ if (strategy === "fixed-end") {
35
+ const end = clampEnd(options.end, n);
36
+ const endText = tail(end);
37
+ const room = maxWidth - measure(ellipsis + endText);
38
+ let low = 0;
39
+ let high = n - end;
40
+ while (low < high) {
41
+ const middle = Math.ceil((low + high) / 2);
42
+ if (measure(head(middle)) <= room)
43
+ low = middle;
44
+ else
45
+ high = middle - 1;
46
+ }
47
+ return head(low) + ellipsis + endText;
48
+ }
49
+ const minEnd = strategy === "min-end" ? clampEnd(options.end, n) : 0;
50
+ const split = (kept) => {
51
+ const endCount = Math.min(n, Math.max(Math.ceil(kept / 2), minEnd));
52
+ return [Math.max(0, kept - endCount), endCount];
53
+ };
54
+ const room = maxWidth - measure(ellipsis);
55
+ let low = 0;
56
+ let high = n;
57
+ while (low < high) {
58
+ const middle = Math.ceil((low + high) / 2);
59
+ const [startCount, endCount] = split(middle);
60
+ if (measure(head(startCount) + tail(endCount)) <= room)
61
+ low = middle;
62
+ else
63
+ high = middle - 1;
64
+ }
65
+ const [startCount, endCount] = split(low);
66
+ return head(startCount) + ellipsis + tail(endCount);
67
+ }
@@ -62,7 +62,7 @@
62
62
  /** Current value. Bindable. */
63
63
  value?: string;
64
64
  /** Autocomplete hint. */
65
- autocomplete?: string;
65
+ autocomplete?: "current-password" | "new-password" | "off";
66
66
  /** Whether required. */
67
67
  required?: boolean;
68
68
  /** Whether disabled. */
@@ -4,7 +4,7 @@ type $$ComponentProps = {
4
4
  /** Current value. Bindable. */
5
5
  value?: string;
6
6
  /** Autocomplete hint. */
7
- autocomplete?: string;
7
+ autocomplete?: "current-password" | "new-password" | "off";
8
8
  /** Whether required. */
9
9
  required?: boolean;
10
10
  /** Whether disabled. */
@@ -40,7 +40,7 @@
40
40
  //
41
41
  // Claude rules:
42
42
  // - Headless: no CSS, no styles -- consumer provides all styling
43
- // - Queries [role='treeitem'] descendants for keyboard navigation
43
+ // - Queries [role='treeitem'] descendants for keyboard navigation, skipping items inside a collapsed (aria-expanded=false) item
44
44
  // - Uses $state() for internal DOM reference
45
45
  //
46
46
  // References:
@@ -68,7 +68,7 @@
68
68
  if (!treeRef) return;
69
69
  const items = Array.from(
70
70
  treeRef.querySelectorAll<HTMLElement>("[role='treeitem']"),
71
- );
71
+ ).filter((el) => !el.parentElement?.closest("[role='treeitem'][aria-expanded='false']"));
72
72
  const current = document.activeElement as HTMLElement;
73
73
  const index = items.indexOf(current);
74
74
  switch (event.key) {
@@ -3,16 +3,52 @@
3
3
  let {
4
4
  class: className = "",
5
5
  label = undefined,
6
+ expanded = $bindable(undefined),
6
7
  children,
7
8
  ...restProps
8
9
  }: {
9
10
  label?: string;
11
+ /** Expanded state, bindable. Leave undefined for a leaf (no aria-expanded). */
12
+ expanded?: boolean;
10
13
  children: Snippet;
11
14
  [key: string]: unknown;
12
15
  } = $props();
16
+
17
+ // ArrowRight/ArrowLeft (WAI-ARIA tree pattern). Only for the focused item itself, not a
18
+ // nested one the event bubbled up from. Swapped in right-to-left layouts.
19
+ function onkeydown(event: KeyboardEvent) {
20
+ const item = event.currentTarget as HTMLElement;
21
+ if (event.target !== item) return;
22
+ const rtl = getComputedStyle(item).direction === "rtl";
23
+ const open = rtl ? "ArrowLeft" : "ArrowRight";
24
+ const close = rtl ? "ArrowRight" : "ArrowLeft";
25
+ if (event.key === open) {
26
+ if (expanded === false) {
27
+ event.preventDefault();
28
+ expanded = true;
29
+ } else if (expanded === true) {
30
+ const child = item.querySelector<HTMLElement>("[role='treeitem']");
31
+ if (child) {
32
+ event.preventDefault();
33
+ child.focus();
34
+ }
35
+ }
36
+ } else if (event.key === close) {
37
+ if (expanded === true) {
38
+ event.preventDefault();
39
+ expanded = false;
40
+ } else {
41
+ const parent = item.parentElement?.closest<HTMLElement>("[role='treeitem']");
42
+ if (parent) {
43
+ event.preventDefault();
44
+ parent.focus();
45
+ }
46
+ }
47
+ }
48
+ }
13
49
  </script>
14
50
 
15
51
  <!-- TreeListItem.svelte -->
16
- <div class={`tree-list-item ${className}`} aria-label={label} {...restProps}>
52
+ <li class={`tree-list-item ${className}`} role="treeitem" tabindex="-1" aria-label={label} aria-expanded={expanded} {onkeydown} {...restProps}>
17
53
  {@render children?.()}
18
- </div>
54
+ </li>
@@ -1,9 +1,11 @@
1
1
  import type { Snippet } from "svelte";
2
2
  type $$ComponentProps = {
3
3
  label?: string;
4
+ /** Expanded state, bindable. Leave undefined for a leaf (no aria-expanded). */
5
+ expanded?: boolean;
4
6
  children: Snippet;
5
7
  [key: string]: unknown;
6
8
  };
7
- declare const TreeListItem: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ declare const TreeListItem: import("svelte").Component<$$ComponentProps, {}, "expanded">;
8
10
  type TreeListItem = ReturnType<typeof TreeListItem>;
9
11
  export default TreeListItem;
@@ -0,0 +1,150 @@
1
+ <script lang="ts">
2
+ // WheelPicker component
3
+ //
4
+ // A vertical single-select listbox where the selection follows the cursor: moving with the arrow keys, typing, or
5
+ // clicking an option chooses it. Use it to pick one of a short list (hours, minutes, months, years) in the manner of a
6
+ // scroll wheel. The component owns the roles, the keys and the selected value; the wheel LOOK (scroll snapping, fading,
7
+ // 3-D) is the consumer's CSS. Pointer dragging and touch inertia are plain scrolling of `.wheel-picker-list`.
8
+ //
9
+ // Props:
10
+ // class — string, optional. Consumer class appended after the base class.
11
+ // label — string, required. Accessible name of the listbox.
12
+ // options — { value: string; label: string; disabled?: boolean }[], required.
13
+ // value — string, bindable, default "". The selected option's value; "" or an unknown value selects nothing.
14
+ // loop — boolean, default false. Arrow keys wrap from the last enabled option to the first and back.
15
+ // pageSize — number, default 5. Enabled options PageUp / PageDown move by.
16
+ // disabled — boolean, default false. Ignores keys and clicks; not focusable.
17
+ // name — string, optional. When set, a hidden input carries the value for a form.
18
+ // onChange — (value: string) => void, optional. Called once when the selection changes.
19
+ // ...restProps — spread onto the root <div>.
20
+ //
21
+ // Markup:
22
+ // <div class="wheel-picker">
23
+ // <div class="wheel-picker-list" role="listbox" aria-orientation="vertical" tabindex="0" aria-activedescendant>
24
+ // <div class="wheel-picker-option" role="option" aria-selected aria-disabled? data-value>…</div>
25
+ // </div>
26
+ // <input type="hidden" name value> (only with `name`)
27
+ // </div>
28
+ //
29
+ // Keyboard (APG listbox, selection follows focus): ArrowDown / ArrowUp, Home / End, PageDown / PageUp, type-ahead over
30
+ // the labels. Disabled options are skipped.
31
+ //
32
+ // Claude rules:
33
+ // - Headless: no CSS, no inline styles; no animation (the selected option is centred instantly).
34
+ // - No hardcoded user-facing strings.
35
+
36
+ import {
37
+ isWheelTypeaheadKey,
38
+ nextWheelIndex,
39
+ nextWheelPrefix,
40
+ WHEEL_MOVE_KEYS,
41
+ wheelTypeaheadIndex,
42
+ type WheelMoveKey,
43
+ type WheelOption,
44
+ type WheelTypeahead,
45
+ } from "./wheel-picker";
46
+
47
+ let {
48
+ class: className = "",
49
+ label,
50
+ options,
51
+ value = $bindable(""),
52
+ loop = false,
53
+ pageSize = 5,
54
+ disabled = false,
55
+ name,
56
+ onChange,
57
+ ...restProps
58
+ }: {
59
+ /** Accessible name. */
60
+ label: string;
61
+ /** The choices. */
62
+ options: WheelOption[];
63
+ /** The selected option's value. Bindable. */
64
+ value?: string;
65
+ /** Arrow keys wrap at the ends. */
66
+ loop?: boolean;
67
+ /** Enabled options PageUp / PageDown move by. */
68
+ pageSize?: number;
69
+ /** Ignores keys and clicks. */
70
+ disabled?: boolean;
71
+ /** Form field name for a hidden input. */
72
+ name?: string;
73
+ /** Called once when the selection changes. */
74
+ onChange?: (value: string) => void;
75
+ [key: string]: unknown;
76
+ } = $props();
77
+
78
+ const uid = $props.id();
79
+ let list: HTMLElement | undefined = $state();
80
+ let typeahead: WheelTypeahead = { prefix: "", at: 0 };
81
+
82
+ const selectedIndex = $derived(options.findIndex((o) => o.value === value));
83
+
84
+ function select(index: number): void {
85
+ if (index < 0) return;
86
+ const next = options[index].value;
87
+ if (next === value) return; // applying the selection already held is a no-op
88
+ value = next;
89
+ onChange?.(next);
90
+ }
91
+
92
+ function onkeydown(event: KeyboardEvent): void {
93
+ if (disabled) return;
94
+ if (WHEEL_MOVE_KEYS.includes(event.key)) {
95
+ event.preventDefault();
96
+ select(nextWheelIndex(options, selectedIndex, event.key as WheelMoveKey, { loop, pageSize }));
97
+ } else if (isWheelTypeaheadKey(event)) {
98
+ typeahead = nextWheelPrefix(typeahead, event.key, Date.now());
99
+ select(wheelTypeaheadIndex(options, selectedIndex, typeahead.prefix));
100
+ }
101
+ }
102
+
103
+ function onclick(event: MouseEvent): void {
104
+ if (disabled) return;
105
+ const target = (event.target as Element).closest<HTMLElement>('[role="option"]');
106
+ if (!target || target.getAttribute("aria-disabled") === "true") return;
107
+ select(Number(target.getAttribute("data-index")));
108
+ list?.focus();
109
+ }
110
+
111
+ // Keep the selected option centred in the list. Only the list scrolls (never the page), and instantly: no animation.
112
+ $effect(() => {
113
+ const index = selectedIndex;
114
+ if (!list || index < 0) return;
115
+ const option = list.querySelector<HTMLElement>(`[data-index="${index}"]`);
116
+ if (!option) return;
117
+ list.scrollTop = option.offsetTop - (list.clientHeight - option.offsetHeight) / 2;
118
+ });
119
+ </script>
120
+
121
+ <!-- WheelPicker.svelte -->
122
+ <div class={`wheel-picker ${className}`} {...restProps}>
123
+ <div
124
+ bind:this={list}
125
+ class="wheel-picker-list"
126
+ role="listbox"
127
+ aria-label={label}
128
+ aria-orientation="vertical"
129
+ aria-disabled={disabled ? "true" : undefined}
130
+ aria-activedescendant={selectedIndex >= 0 ? `${uid}-option-${selectedIndex}` : undefined}
131
+ tabindex={disabled ? -1 : 0}
132
+ {onkeydown}
133
+ {onclick}
134
+ >
135
+ {#each options as option, index (option.value)}
136
+ <div
137
+ class="wheel-picker-option"
138
+ role="option"
139
+ id={`${uid}-option-${index}`}
140
+ aria-selected={index === selectedIndex}
141
+ aria-disabled={option.disabled ? "true" : undefined}
142
+ data-value={option.value}
143
+ data-index={index}
144
+ >{option.label}</div>
145
+ {/each}
146
+ </div>
147
+ {#if name}
148
+ <input type="hidden" {name} {value} />
149
+ {/if}
150
+ </div>
@@ -0,0 +1,23 @@
1
+ import { type WheelOption } from "./wheel-picker";
2
+ type $$ComponentProps = {
3
+ /** Accessible name. */
4
+ label: string;
5
+ /** The choices. */
6
+ options: WheelOption[];
7
+ /** The selected option's value. Bindable. */
8
+ value?: string;
9
+ /** Arrow keys wrap at the ends. */
10
+ loop?: boolean;
11
+ /** Enabled options PageUp / PageDown move by. */
12
+ pageSize?: number;
13
+ /** Ignores keys and clicks. */
14
+ disabled?: boolean;
15
+ /** Form field name for a hidden input. */
16
+ name?: string;
17
+ /** Called once when the selection changes. */
18
+ onChange?: (value: string) => void;
19
+ [key: string]: unknown;
20
+ };
21
+ declare const WheelPicker: import("svelte").Component<$$ComponentProps, {}, "value">;
22
+ type WheelPicker = ReturnType<typeof WheelPicker>;
23
+ export default WheelPicker;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Keyboard model for a wheel picker: a single-select listbox where the selection follows the cursor (APG listbox, "selection
3
+ * follows focus"), so moving the cursor IS choosing. Pure and framework-neutral, tested without a browser.
4
+ *
5
+ * Disabled options are skipped by every move. Arrows wrap only when `loop` is set; Home, End, PageUp and PageDown never wrap.
6
+ */
7
+ export interface WheelOption {
8
+ value: string;
9
+ label: string;
10
+ disabled?: boolean;
11
+ }
12
+ export type WheelMoveKey = "ArrowUp" | "ArrowDown" | "Home" | "End" | "PageUp" | "PageDown";
13
+ export declare const WHEEL_MOVE_KEYS: readonly string[];
14
+ export interface WheelMoveOptions {
15
+ /** Arrow keys wrap from the last enabled option to the first and back. Default false. */
16
+ loop?: boolean;
17
+ /** Enabled options PageUp / PageDown move by. Default 5. */
18
+ pageSize?: number;
19
+ }
20
+ /**
21
+ * The index to select after `key`, or -1 when no option can be selected. `current` is the selected index, or -1 for none.
22
+ * With nothing selected, ArrowDown / PageDown / Home choose the first enabled option and ArrowUp / PageUp / End the last.
23
+ */
24
+ export declare function nextWheelIndex(options: readonly WheelOption[], current: number, key: WheelMoveKey, move?: WheelMoveOptions): number;
25
+ export declare const WHEEL_TYPEAHEAD_RESET_MS = 700;
26
+ export interface WheelTypeahead {
27
+ prefix: string;
28
+ at: number;
29
+ }
30
+ /** The prefix after `key` at time `now`: it grows while presses are within the reset time, else starts over. */
31
+ export declare function nextWheelPrefix(previous: WheelTypeahead, key: string, now: number, resetMs?: number): WheelTypeahead;
32
+ /** Whether a key press is a type-ahead character: one printable character other than space, no Ctrl, Alt or Meta. */
33
+ export declare function isWheelTypeaheadKey(event: {
34
+ key: string;
35
+ ctrlKey?: boolean;
36
+ altKey?: boolean;
37
+ metaKey?: boolean;
38
+ }): boolean;
39
+ /**
40
+ * The enabled option to move to for the typed `prefix`, or -1. Searches from the option after `current` and wraps; a prefix of one
41
+ * repeated letter ("ss") counts as that single letter, so pressing it again steps through the options that start with it.
42
+ */
43
+ export declare function wheelTypeaheadIndex(options: readonly WheelOption[], current: number, prefix: string): number;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Keyboard model for a wheel picker: a single-select listbox where the selection follows the cursor (APG listbox, "selection
3
+ * follows focus"), so moving the cursor IS choosing. Pure and framework-neutral, tested without a browser.
4
+ *
5
+ * Disabled options are skipped by every move. Arrows wrap only when `loop` is set; Home, End, PageUp and PageDown never wrap.
6
+ */
7
+ export const WHEEL_MOVE_KEYS = ["ArrowUp", "ArrowDown", "Home", "End", "PageUp", "PageDown"];
8
+ /** Indexes of the options that can be selected. */
9
+ function enabledIndexes(options) {
10
+ const out = [];
11
+ options.forEach((o, i) => {
12
+ if (!o.disabled)
13
+ out.push(i);
14
+ });
15
+ return out;
16
+ }
17
+ /**
18
+ * The index to select after `key`, or -1 when no option can be selected. `current` is the selected index, or -1 for none.
19
+ * With nothing selected, ArrowDown / PageDown / Home choose the first enabled option and ArrowUp / PageUp / End the last.
20
+ */
21
+ export function nextWheelIndex(options, current, key, move = {}) {
22
+ const enabled = enabledIndexes(options);
23
+ if (enabled.length === 0)
24
+ return -1;
25
+ const first = enabled[0];
26
+ const last = enabled[enabled.length - 1];
27
+ const position = enabled.indexOf(current);
28
+ const pageSize = Math.max(1, Math.floor(move.pageSize ?? 5));
29
+ if (key === "Home")
30
+ return first;
31
+ if (key === "End")
32
+ return last;
33
+ if (position === -1)
34
+ return key === "ArrowDown" || key === "PageDown" ? first : last;
35
+ switch (key) {
36
+ case "ArrowDown":
37
+ return position + 1 < enabled.length ? enabled[position + 1] : move.loop ? first : current;
38
+ case "ArrowUp":
39
+ return position > 0 ? enabled[position - 1] : move.loop ? last : current;
40
+ case "PageDown":
41
+ return enabled[Math.min(enabled.length - 1, position + pageSize)];
42
+ case "PageUp":
43
+ return enabled[Math.max(0, position - pageSize)];
44
+ }
45
+ }
46
+ /** Lower-case and strip diacritics so "é" is typed with "e". */
47
+ function fold(text) {
48
+ return text.normalize("NFD").replace(/\p{M}/gu, "").toLocaleLowerCase();
49
+ }
50
+ export const WHEEL_TYPEAHEAD_RESET_MS = 700;
51
+ /** The prefix after `key` at time `now`: it grows while presses are within the reset time, else starts over. */
52
+ export function nextWheelPrefix(previous, key, now, resetMs = WHEEL_TYPEAHEAD_RESET_MS) {
53
+ const continuing = previous.prefix !== "" && now - previous.at <= resetMs;
54
+ return { prefix: (continuing ? previous.prefix : "") + key, at: now };
55
+ }
56
+ /** Whether a key press is a type-ahead character: one printable character other than space, no Ctrl, Alt or Meta. */
57
+ export function isWheelTypeaheadKey(event) {
58
+ return event.key.length === 1 && event.key !== " " && !event.ctrlKey && !event.altKey && !event.metaKey;
59
+ }
60
+ /**
61
+ * The enabled option to move to for the typed `prefix`, or -1. Searches from the option after `current` and wraps; a prefix of one
62
+ * repeated letter ("ss") counts as that single letter, so pressing it again steps through the options that start with it.
63
+ */
64
+ export function wheelTypeaheadIndex(options, current, prefix) {
65
+ const folded = fold(prefix);
66
+ if (folded === "" || options.length === 0)
67
+ return -1;
68
+ const repeated = folded.length > 1 && [...folded].every((c) => c === folded[0]);
69
+ const needle = repeated ? folded[0] : folded;
70
+ const startOffset = needle.length > 1 ? 0 : 1;
71
+ const from = current < 0 ? -startOffset : current;
72
+ for (let step = 0; step < options.length; step++) {
73
+ const index = (from + startOffset + step + options.length) % options.length;
74
+ const option = options[index];
75
+ if (!option.disabled && fold(option.label).startsWith(needle))
76
+ return index;
77
+ }
78
+ return -1;
79
+ }
package/dist/index.d.ts CHANGED
@@ -241,6 +241,7 @@ export { default as HangukJuminDeungnokBeonhoView } from "./components/HangukJum
241
241
  export { default as Header } from "./components/Header/Header.svelte";
242
242
  export { default as Headline } from "./components/Headline/Headline.svelte";
243
243
  export { default as HeatmapChart } from "./components/HeatmapChart/HeatmapChart.svelte";
244
+ export { default as HeatmapChartWithYearOfDays } from "./components/HeatmapChartWithYearOfDays/HeatmapChartWithYearOfDays.svelte";
244
245
  export { default as Hero } from "./components/Hero/Hero.svelte";
245
246
  export { default as HeroHeadline } from "./components/HeroHeadline/HeroHeadline.svelte";
246
247
  export { default as HiddenInput } from "./components/HiddenInput/HiddenInput.svelte";
@@ -323,6 +324,7 @@ export { default as MenuItem } from "./components/MenuItem/MenuItem.svelte";
323
324
  export { default as Meter } from "./components/Meter/Meter.svelte";
324
325
  export { default as MexicoClaveUnicaDeRegistroDePoblacionInput } from "./components/MexicoClaveUnicaDeRegistroDePoblacionInput/MexicoClaveUnicaDeRegistroDePoblacionInput.svelte";
325
326
  export { default as MexicoClaveUnicaDeRegistroDePoblacionView } from "./components/MexicoClaveUnicaDeRegistroDePoblacionView/MexicoClaveUnicaDeRegistroDePoblacionView.svelte";
327
+ export { default as MiddleTruncation } from "./components/MiddleTruncation/MiddleTruncation.svelte";
326
328
  export { default as MisrAlRaqmAlQawmiInput } from "./components/MisrAlRaqmAlQawmiInput/MisrAlRaqmAlQawmiInput.svelte";
327
329
  export { default as MisrAlRaqmAlQawmiView } from "./components/MisrAlRaqmAlQawmiView/MisrAlRaqmAlQawmiView.svelte";
328
330
  export { default as MockupBrowser } from "./components/MockupBrowser/MockupBrowser.svelte";
@@ -565,6 +567,7 @@ export { default as Visible } from "./components/Visible/Visible.svelte";
565
567
  export { default as WarningCallout } from "./components/WarningCallout/WarningCallout.svelte";
566
568
  export { default as Watermark } from "./components/Watermark/Watermark.svelte";
567
569
  export { default as WeekInput } from "./components/WeekInput/WeekInput.svelte";
570
+ export { default as WheelPicker } from "./components/WheelPicker/WheelPicker.svelte";
568
571
  export { default as YisraelTeudatZehutInput } from "./components/YisraelTeudatZehutInput/YisraelTeudatZehutInput.svelte";
569
572
  export { default as YisraelTeudatZehutView } from "./components/YisraelTeudatZehutView/YisraelTeudatZehutView.svelte";
570
573
  export { default as ZhongguoJuminShenfenzhengHaomaInput } from "./components/ZhongguoJuminShenfenzhengHaomaInput/ZhongguoJuminShenfenzhengHaomaInput.svelte";
package/dist/index.js CHANGED
@@ -243,6 +243,7 @@ export { default as HangukJuminDeungnokBeonhoView } from "./components/HangukJum
243
243
  export { default as Header } from "./components/Header/Header.svelte";
244
244
  export { default as Headline } from "./components/Headline/Headline.svelte";
245
245
  export { default as HeatmapChart } from "./components/HeatmapChart/HeatmapChart.svelte";
246
+ export { default as HeatmapChartWithYearOfDays } from "./components/HeatmapChartWithYearOfDays/HeatmapChartWithYearOfDays.svelte";
246
247
  export { default as Hero } from "./components/Hero/Hero.svelte";
247
248
  export { default as HeroHeadline } from "./components/HeroHeadline/HeroHeadline.svelte";
248
249
  export { default as HiddenInput } from "./components/HiddenInput/HiddenInput.svelte";
@@ -325,6 +326,7 @@ export { default as MenuItem } from "./components/MenuItem/MenuItem.svelte";
325
326
  export { default as Meter } from "./components/Meter/Meter.svelte";
326
327
  export { default as MexicoClaveUnicaDeRegistroDePoblacionInput } from "./components/MexicoClaveUnicaDeRegistroDePoblacionInput/MexicoClaveUnicaDeRegistroDePoblacionInput.svelte";
327
328
  export { default as MexicoClaveUnicaDeRegistroDePoblacionView } from "./components/MexicoClaveUnicaDeRegistroDePoblacionView/MexicoClaveUnicaDeRegistroDePoblacionView.svelte";
329
+ export { default as MiddleTruncation } from "./components/MiddleTruncation/MiddleTruncation.svelte";
328
330
  export { default as MisrAlRaqmAlQawmiInput } from "./components/MisrAlRaqmAlQawmiInput/MisrAlRaqmAlQawmiInput.svelte";
329
331
  export { default as MisrAlRaqmAlQawmiView } from "./components/MisrAlRaqmAlQawmiView/MisrAlRaqmAlQawmiView.svelte";
330
332
  export { default as MockupBrowser } from "./components/MockupBrowser/MockupBrowser.svelte";
@@ -567,6 +569,7 @@ export { default as Visible } from "./components/Visible/Visible.svelte";
567
569
  export { default as WarningCallout } from "./components/WarningCallout/WarningCallout.svelte";
568
570
  export { default as Watermark } from "./components/Watermark/Watermark.svelte";
569
571
  export { default as WeekInput } from "./components/WeekInput/WeekInput.svelte";
572
+ export { default as WheelPicker } from "./components/WheelPicker/WheelPicker.svelte";
570
573
  export { default as YisraelTeudatZehutInput } from "./components/YisraelTeudatZehutInput/YisraelTeudatZehutInput.svelte";
571
574
  export { default as YisraelTeudatZehutView } from "./components/YisraelTeudatZehutView/YisraelTeudatZehutView.svelte";
572
575
  export { default as ZhongguoJuminShenfenzhengHaomaInput } from "./components/ZhongguoJuminShenfenzhengHaomaInput/ZhongguoJuminShenfenzhengHaomaInput.svelte";
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@lilydesignsystem/svelte-headless",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "engines": {
5
- "node": "=26"
5
+ "node": ">=26"
6
6
  },
7
7
  "description": "A comprehensive headless Svelte 5 component library with 491 accessible, unstyled components for building design systems. Targets WCAG 2.2 AAA.",
8
8
  "scripts": {
@@ -10,7 +10,8 @@
10
10
  "prepublishOnly": "node build.mjs",
11
11
  "test": "vitest run",
12
12
  "test:ui": "vitest --ui",
13
- "test:watch": "vitest"
13
+ "test:watch": "vitest",
14
+ "check": "svelte-check --tsconfig ./tsconfig.check.json"
14
15
  },
15
16
  "repository": {
16
17
  "type": "git",
@@ -42,6 +43,7 @@
42
43
  "@vitest/ui": "^5.0.5",
43
44
  "jsdom": "^30.1.3",
44
45
  "svelte": "^5.57.2",
46
+ "svelte-check": "^4.7.7",
45
47
  "vite": "^8.3.4",
46
48
  "vitest": "^5.0.5"
47
49
  },