@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 +9 -0
- package/README.md +453 -0
- package/dist/components/HeatmapChartWithYearOfDays/HeatmapChartWithYearOfDays.svelte +74 -0
- package/dist/components/HeatmapChartWithYearOfDays/HeatmapChartWithYearOfDays.svelte.d.ts +17 -0
- package/dist/components/MiddleTruncation/MiddleTruncation.svelte +108 -0
- package/dist/components/MiddleTruncation/MiddleTruncation.svelte.d.ts +15 -0
- package/dist/components/MiddleTruncation/middle-truncation.d.ts +28 -0
- package/dist/components/MiddleTruncation/middle-truncation.js +67 -0
- package/dist/components/PasswordInput/PasswordInput.svelte +1 -1
- package/dist/components/PasswordInput/PasswordInput.svelte.d.ts +1 -1
- package/dist/components/TreeList/TreeList.svelte +2 -2
- package/dist/components/TreeListItem/TreeListItem.svelte +38 -2
- package/dist/components/TreeListItem/TreeListItem.svelte.d.ts +3 -1
- package/dist/components/WheelPicker/WheelPicker.svelte +150 -0
- package/dist/components/WheelPicker/WheelPicker.svelte.d.ts +23 -0
- package/dist/components/WheelPicker/wheel-picker.d.ts +43 -0
- package/dist/components/WheelPicker/wheel-picker.js +79 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- 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).
|
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
|
+
}
|
|
@@ -4,7 +4,7 @@ type $$ComponentProps = {
|
|
|
4
4
|
/** Current value. Bindable. */
|
|
5
5
|
value?: string;
|
|
6
6
|
/** Autocomplete hint. */
|
|
7
|
-
autocomplete?:
|
|
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
|
-
<
|
|
52
|
+
<li class={`tree-list-item ${className}`} role="treeitem" tabindex="-1" aria-label={label} aria-expanded={expanded} {onkeydown} {...restProps}>
|
|
17
53
|
{@render children?.()}
|
|
18
|
-
</
|
|
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.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"engines": {
|
|
5
|
-
"node": "
|
|
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
|
},
|