@fusion.dev/helix-headless 0.143.0 → 0.144.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/custom-elements.json +8551 -375
  2. package/dist/area-chart/root/area-chart-root.d.ts +12 -4
  3. package/dist/area-chart/root/area-chart-root.js +24 -7
  4. package/dist/avatar/root/avatar-root.d.ts +14 -0
  5. package/dist/avatar/root/avatar-root.js +23 -9
  6. package/dist/badge/root/badge-root.d.ts +11 -7
  7. package/dist/badge/root/badge-root.js +1 -1
  8. package/dist/bar-chart/root/bar-chart-root.d.ts +12 -3
  9. package/dist/bar-chart/root/bar-chart-root.js +17 -10
  10. package/dist/button/root/button-root.d.ts +47 -1
  11. package/dist/button/root/button-root.js +38 -4
  12. package/dist/callout/root/callout-root.d.ts +18 -1
  13. package/dist/callout/root/callout-root.js +15 -4
  14. package/dist/card/root/card-root.d.ts +30 -1
  15. package/dist/card/root/card-root.js +38 -5
  16. package/dist/checkbox/root/checkbox-root.js +2 -0
  17. package/dist/checkbox-group/root/checkbox-group-root.d.ts +53 -3
  18. package/dist/checkbox-group/root/checkbox-group-root.js +71 -22
  19. package/dist/code-block/lines.d.ts +13 -0
  20. package/dist/code-block/lines.js +2 -0
  21. package/dist/code-block/root/code-block-root.d.ts +30 -0
  22. package/dist/code-block/root/code-block-root.js +23 -2
  23. package/dist/color-input/root/color-input-root.d.ts +34 -1
  24. package/dist/color-input/root/color-input-root.js +23 -4
  25. package/dist/color-picker/channels.d.ts +26 -0
  26. package/dist/color-picker/channels.js +84 -0
  27. package/dist/color-picker/root/color-picker-root.d.ts +81 -5
  28. package/dist/color-picker/root/color-picker-root.js +103 -10
  29. package/dist/combobox/root/combobox-root.d.ts +38 -3
  30. package/dist/combobox/root/combobox-root.js +64 -2
  31. package/dist/command/root/command-root.d.ts +30 -4
  32. package/dist/command/root/command-root.js +33 -3
  33. package/dist/copy-button/from.d.ts +22 -0
  34. package/dist/copy-button/from.js +28 -0
  35. package/dist/copy-button/root/copy-button-root.d.ts +27 -1
  36. package/dist/copy-button/root/copy-button-root.js +43 -7
  37. package/dist/date-input/root/date-input-root.d.ts +73 -4
  38. package/dist/date-input/root/date-input-root.js +93 -43
  39. package/dist/date-picker/root/date-picker-root.d.ts +145 -1
  40. package/dist/date-picker/root/date-picker-root.js +239 -48
  41. package/dist/dialog/description.d.ts +9 -0
  42. package/dist/dialog/description.js +14 -0
  43. package/dist/dialog/root/dialog-root.d.ts +64 -1
  44. package/dist/dialog/root/dialog-root.js +49 -23
  45. package/dist/donut-chart/root/donut-chart-root.d.ts +25 -4
  46. package/dist/donut-chart/root/donut-chart-root.js +21 -6
  47. package/dist/drawer/root/drawer-root.d.ts +63 -2
  48. package/dist/drawer/root/drawer-root.js +95 -19
  49. package/dist/drawer/swipe-start.d.ts +9 -0
  50. package/dist/drawer/swipe-start.js +12 -0
  51. package/dist/element-events.generated.js +8 -5
  52. package/dist/empty/root/empty-root.d.ts +1 -1
  53. package/dist/file-input/root/file-input-root.d.ts +47 -0
  54. package/dist/file-input/root/file-input-root.js +60 -22
  55. package/dist/file-input/screen.d.ts +35 -0
  56. package/dist/file-input/screen.js +49 -0
  57. package/dist/filter-input/root/filter-input-root.d.ts +38 -4
  58. package/dist/filter-input/root/filter-input-root.js +67 -3
  59. package/dist/filter-input/suggest.d.ts +17 -0
  60. package/dist/filter-input/suggest.js +23 -0
  61. package/dist/format-date/options.d.ts +33 -0
  62. package/dist/format-date/options.js +42 -0
  63. package/dist/format-date/root/format-date-root.d.ts +51 -0
  64. package/dist/format-date/root/format-date-root.js +58 -38
  65. package/dist/heatmap/root/heatmap-root.d.ts +23 -6
  66. package/dist/heatmap/root/heatmap-root.js +24 -13
  67. package/dist/icon/index.d.ts +2 -0
  68. package/dist/icon/index.js +2 -0
  69. package/dist/icon/registry.d.ts +37 -0
  70. package/dist/icon/registry.js +83 -0
  71. package/dist/icon/root/icon-root.d.ts +30 -4
  72. package/dist/icon/root/icon-root.js +41 -6
  73. package/dist/icon/root/index.d.ts +2 -0
  74. package/dist/icon/root/index.js +2 -0
  75. package/dist/icon/set.d.ts +1 -0
  76. package/dist/icon/set.js +3 -0
  77. package/dist/image/root/image-root.d.ts +14 -0
  78. package/dist/image/root/image-root.js +15 -2
  79. package/dist/input/root/input-root.d.ts +11 -0
  80. package/dist/input/root/input-root.js +15 -0
  81. package/dist/kbd/root/kbd-root.d.ts +20 -13
  82. package/dist/kbd/root/kbd-root.js +35 -37
  83. package/dist/kbd/shortcut.d.ts +18 -0
  84. package/dist/kbd/shortcut.js +35 -0
  85. package/dist/legend/hidden-series.d.ts +8 -0
  86. package/dist/legend/hidden-series.js +13 -0
  87. package/dist/legend/root/legend-root.d.ts +56 -0
  88. package/dist/legend/root/legend-root.js +54 -5
  89. package/dist/lightbox/item/lightbox-item.d.ts +15 -0
  90. package/dist/lightbox/item/lightbox-item.js +10 -1
  91. package/dist/lightbox/root/lightbox-root.d.ts +61 -1
  92. package/dist/lightbox/root/lightbox-root.js +94 -4
  93. package/dist/line-chart/root/line-chart-root.d.ts +11 -3
  94. package/dist/line-chart/root/line-chart-root.js +19 -5
  95. package/dist/listbox/range.d.ts +17 -0
  96. package/dist/listbox/range.js +5 -0
  97. package/dist/listbox/root/listbox-root.d.ts +74 -3
  98. package/dist/listbox/root/listbox-root.js +87 -9
  99. package/dist/loader/busy.d.ts +17 -0
  100. package/dist/loader/busy.js +24 -0
  101. package/dist/loader/root/loader-root.d.ts +37 -0
  102. package/dist/loader/root/loader-root.js +52 -3
  103. package/dist/meter/root/meter-root.d.ts +7 -0
  104. package/dist/meter/root/meter-root.js +6 -3
  105. package/dist/notifications/feed-keys.d.ts +10 -0
  106. package/dist/notifications/feed-keys.js +14 -0
  107. package/dist/notifications/notification/notification.d.ts +17 -0
  108. package/dist/notifications/notification/notification.js +21 -2
  109. package/dist/notifications/root/notifications-root.d.ts +42 -0
  110. package/dist/notifications/root/notifications-root.js +60 -1
  111. package/dist/number-input/root/number-input-root.d.ts +47 -0
  112. package/dist/number-input/root/number-input-root.js +58 -22
  113. package/dist/object-inspector/root/object-inspector-root.d.ts +1 -0
  114. package/dist/object-inspector/root/object-inspector-root.js +1 -1
  115. package/dist/option/root/option-root.d.ts +54 -5
  116. package/dist/option/root/option-root.js +34 -5
  117. package/dist/pin-input/format.d.ts +7 -0
  118. package/dist/pin-input/format.js +16 -0
  119. package/dist/pin-input/root/pin-input-root.d.ts +21 -0
  120. package/dist/pin-input/root/pin-input-root.js +29 -2
  121. package/dist/popover/root/popover-root.d.ts +62 -4
  122. package/dist/popover/root/popover-root.js +53 -5
  123. package/dist/progress/root/progress-root.d.ts +7 -0
  124. package/dist/progress/root/progress-root.js +9 -3
  125. package/dist/qr-code/export.d.ts +31 -0
  126. package/dist/qr-code/export.js +10 -0
  127. package/dist/qr-code/root/qr-code-root.d.ts +26 -0
  128. package/dist/qr-code/root/qr-code-root.js +49 -2
  129. package/dist/radio/group/radio-group.d.ts +23 -0
  130. package/dist/radio/group/radio-group.js +18 -0
  131. package/dist/rating/root/rating-root.d.ts +25 -0
  132. package/dist/rating/root/rating-root.js +45 -3
  133. package/dist/scatter-chart/root/scatter-chart-root.d.ts +17 -3
  134. package/dist/scatter-chart/root/scatter-chart-root.js +25 -7
  135. package/dist/select/root/select-root.d.ts +24 -0
  136. package/dist/select/root/select-root.js +20 -6
  137. package/dist/skeleton/root/skeleton-root.d.ts +25 -0
  138. package/dist/skeleton/root/skeleton-root.js +17 -2
  139. package/dist/slider/root/slider-root.d.ts +39 -0
  140. package/dist/slider/root/slider-root.js +45 -9
  141. package/dist/stat/root/stat-root.d.ts +2 -0
  142. package/dist/stat/root/stat-root.js +1 -1
  143. package/dist/steps/step/step.d.ts +1 -0
  144. package/dist/steps/step/step.js +1 -1
  145. package/dist/switch/root/switch-root.d.ts +12 -2
  146. package/dist/switch/root/switch-root.js +6 -1
  147. package/dist/tag/group/tag-group.d.ts +107 -0
  148. package/dist/tag/group/tag-group.js +157 -0
  149. package/dist/tag/index.d.ts +1 -0
  150. package/dist/tag/index.js +1 -0
  151. package/dist/tag/root/tag-root.d.ts +60 -0
  152. package/dist/tag/root/tag-root.js +74 -18
  153. package/dist/tag/selection.d.ts +10 -0
  154. package/dist/tag/selection.js +12 -0
  155. package/dist/textarea/grow.d.ts +22 -0
  156. package/dist/textarea/grow.js +20 -0
  157. package/dist/textarea/root/textarea-root.d.ts +9 -0
  158. package/dist/textarea/root/textarea-root.js +9 -1
  159. package/dist/time-input/root/time-input-root.d.ts +44 -5
  160. package/dist/time-input/root/time-input-root.js +69 -39
  161. package/dist/time-picker/root/time-picker-root.d.ts +54 -8
  162. package/dist/time-picker/root/time-picker-root.js +121 -91
  163. package/dist/toast/hotkey.d.ts +22 -0
  164. package/dist/toast/hotkey.js +23 -0
  165. package/dist/toast/item/toast-item.d.ts +16 -1
  166. package/dist/toast/item/toast-item.js +41 -13
  167. package/dist/toast/root/toast-root.d.ts +42 -8
  168. package/dist/toast/root/toast-root.js +64 -4
  169. package/dist/toggle/group/toggle-group.d.ts +57 -15
  170. package/dist/toggle/group/toggle-group.js +69 -9
  171. package/dist/toggle/root/toggle-root.d.ts +12 -1
  172. package/dist/toggle/root/toggle-root.js +11 -3
  173. package/dist/tooltip/focus-target.d.ts +6 -0
  174. package/dist/tooltip/focus-target.js +9 -0
  175. package/dist/tooltip/root/tooltip-root.d.ts +42 -17
  176. package/dist/tooltip/root/tooltip-root.js +50 -9
  177. package/dist/tour/naming.d.ts +30 -0
  178. package/dist/tour/naming.js +10 -0
  179. package/dist/tour/root/tour-root.d.ts +35 -0
  180. package/dist/tour/root/tour-root.js +56 -5
  181. package/dist/tour/stop/tour-stop.d.ts +53 -8
  182. package/dist/tour/stop/tour-stop.js +45 -15
  183. package/dist/tour/wait.d.ts +11 -0
  184. package/dist/tour/wait.js +19 -0
  185. package/dist/trend/root/trend-root.d.ts +13 -0
  186. package/dist/trend/root/trend-root.js +14 -9
  187. package/lint.json +1 -1
  188. package/package.json +5 -5
  189. package/types/jsx.d.ts +4862 -254
  190. package/types/svelte.d.ts +1 -0
  191. package/types/vue.d.ts +1149 -67
@@ -4,17 +4,12 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { valueAsTime as asTime, customElement, elementById, fillTemplate, timeAsValue as fromTime, HelixFormElement, MixinControllerValue, minutesAsValue, reportValues, valueAsMinutes, writeTime, writtenList, } from '@fusion.dev/helix-core';
7
+ import { clockLocale, commaList, customElement, dayPeriod, elementById, fillTemplate, HelixFormElement, inlineStep, isTwelveHour, MixinControllerValue, nearestTime, offeredTimes, reportValues, secondsAsValue, timeBarred, timeFault, valueAsSeconds, wheelParts, wholeHour, writeClock, } from '@fusion.dev/helix-core';
8
8
  import { nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { html } from 'lit/static-html.js';
11
11
  import '../../listbox/index.js';
12
12
  import '../../option/index.js';
13
- const isTwelveHour = (locale) => new Intl.DateTimeFormat(locale || undefined, { timeStyle: 'short' }).resolvedOptions().hour12 ===
14
- true;
15
- const dayPeriod = (half, locale) => new Intl.DateTimeFormat(locale || undefined, { hour: 'numeric', hour12: true, timeZone: 'UTC' })
16
- .formatToParts(new Date(Date.UTC(1970, 0, 1, half === 'AM' ? 9 : 21)))
17
- .find((part) => part.type === 'dayPeriod')?.value ?? half;
18
13
  let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControllerValue(HelixFormElement) {
19
14
  constructor() {
20
15
  super(...arguments);
@@ -60,69 +55,78 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
60
55
  if (this.isDisabled) {
61
56
  return;
62
57
  }
63
- const { hour, meridiem, minute } = this.parts;
64
- let next = hour * 60 + minute;
58
+ const { hour, meridiem, minute, second } = this.parts;
59
+ let [h, m, sec] = [hour, minute, second];
65
60
  if (part === 'minute') {
66
- next = hour * 60 + Number(to);
61
+ m = Number(to);
62
+ }
63
+ else if (part === 'second') {
64
+ sec = Number(to);
67
65
  }
68
66
  else if (part === 'hour') {
69
- const shown = Number(to);
70
- const whole = isTwelveHour(this.locale) ? (shown % 12) + (meridiem === 'PM' ? 12 : 0) : shown;
71
- next = whole * 60 + minute;
67
+ h = wholeHour(Number(to), isTwelveHour(this.clock) ? meridiem : undefined);
72
68
  }
73
69
  else {
74
- const half = hour % 12;
75
- next = (half + (to === 'PM' ? 12 : 0)) * 60 + minute;
70
+ h = (hour % 12) + (to === 'PM' ? 12 : 0);
76
71
  }
77
- const whole = minutesAsValue(this.nearest(next));
78
- if (this.barred(whole)) {
72
+ const nearest = this.nearest(h * 3600 + m * 60 + sec);
73
+ if (this.barredAt(nearest)) {
79
74
  return;
80
75
  }
81
- this.requestValue(whole);
76
+ this.requestValue(this.valueFor(nearest));
82
77
  };
83
78
  this.readOnly = false;
84
79
  this.required = false;
80
+ this.secondLabel = 'Second';
81
+ this.seconds = false;
85
82
  this.step = 15;
86
83
  this.listRevealed = false;
87
84
  this.naming = undefined;
85
+ this.handleColumnsKeydown = (event) => {
86
+ const step = inlineStep(this, event.key);
87
+ if (step === 0 || event.altKey || event.ctrlKey || event.metaKey) {
88
+ return;
89
+ }
90
+ const columns = [...(this.renderRoot?.querySelectorAll('[part="column"]') ?? [])];
91
+ const at = columns.findIndex((column) => event.composedPath().includes(column));
92
+ const next = columns[at + step];
93
+ if (at < 0 || !next) {
94
+ return;
95
+ }
96
+ event.preventDefault();
97
+ next.focus();
98
+ };
88
99
  }
89
100
  get times() {
90
- const from = valueAsMinutes(this.min) ?? 0;
91
- const to = valueAsMinutes(this.max) ?? 1439;
92
- const step = Math.max(1, Math.round(this.step));
93
- const found = [];
94
- for (let at = from; at <= to; at += step) {
95
- found.push(at);
96
- }
97
- return found;
101
+ return offeredTimes(this.min, this.max, this.step, this.seconds);
102
+ }
103
+ get clock() {
104
+ return clockLocale(this.locale, this.hourCycle);
105
+ }
106
+ valueFor(at) {
107
+ return secondsAsValue(at, this.seconds);
98
108
  }
99
109
  barred(at) {
100
- return writtenList(this.disableTimes).includes(at);
101
- }
102
- nearest(minutes) {
103
- const times = this.times;
104
- let best = times[0] ?? minutes;
105
- for (const at of times) {
106
- if (Math.abs(at - minutes) < Math.abs(best - minutes)) {
107
- best = at;
108
- }
109
- }
110
- return best;
110
+ const seconds = valueAsSeconds(at);
111
+ return seconds !== null && this.barredAt(seconds);
112
+ }
113
+ barredAt(at) {
114
+ return timeBarred(at, this.disableTimes);
115
+ }
116
+ nearest(at) {
117
+ return nearestTime(this.times, at);
111
118
  }
112
119
  wholeHourBarred(shown, half) {
113
- const whole = half === undefined ? shown : (shown % 12) + (half === 'PM' ? 12 : 0);
114
- const within = this.times.filter((at) => Math.floor(at / 60) === whole);
115
- return within.length > 0 && within.every((at) => this.barred(minutesAsValue(at)));
120
+ const whole = wholeHour(shown, half);
121
+ const within = this.times.filter((at) => Math.floor(at / 3600) === whole);
122
+ return within.length > 0 && within.every((at) => this.barredAt(at));
116
123
  }
117
124
  halfBarred(half) {
118
- const within = this.times.filter((at) => (half === 'PM') === at >= 720);
119
- return within.length > 0 && within.every((at) => this.barred(minutesAsValue(at)));
125
+ const within = this.times.filter((at) => (half === 'PM') === at >= 43_200);
126
+ return within.length > 0 && within.every((at) => this.barredAt(at));
120
127
  }
121
128
  get held() {
122
- return this.value
123
- .split(',')
124
- .map((one) => one.trim())
125
- .filter(Boolean);
129
+ return commaList(this.value);
126
130
  }
127
131
  between(at) {
128
132
  if (this.mode !== 'range') {
@@ -132,17 +136,25 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
132
136
  return from !== undefined && to !== undefined && at > from && at < to;
133
137
  }
134
138
  get parts() {
135
- const at = valueAsMinutes(this.value) ?? valueAsMinutes(this.min) ?? 0;
136
- const hour = Math.floor(at / 60);
137
- return { hour, meridiem: hour < 12 ? 'AM' : 'PM', minute: at % 60 };
139
+ const at = valueAsSeconds(this.value) ?? valueAsSeconds(this.min) ?? 0;
140
+ return wheelParts(at, isTwelveHour(this.clock));
138
141
  }
139
142
  get hours() {
140
- const found = new Set(this.times.map((at) => Math.floor(at / 60)));
143
+ const found = new Set(this.times.map((at) => Math.floor(at / 3600)));
141
144
  return [...found];
142
145
  }
143
146
  get minutes() {
144
147
  const hour = this.parts.hour;
145
- return this.times.filter((at) => Math.floor(at / 60) === hour).map((at) => at % 60);
148
+ const found = new Set(this.times
149
+ .filter((at) => Math.floor(at / 3600) === hour)
150
+ .map((at) => Math.floor(at / 60) % 60));
151
+ return [...found];
152
+ }
153
+ get secondsShown() {
154
+ const { hour, minute } = this.parts;
155
+ return this.times
156
+ .filter((at) => Math.floor(at / 60) === hour * 60 + minute)
157
+ .map((at) => at % 60);
146
158
  }
147
159
  reveal() {
148
160
  if (this.mode !== undefined) {
@@ -163,32 +175,33 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
163
175
  return super.requestValue(next);
164
176
  }
165
177
  fault(at) {
166
- const minutes = valueAsMinutes(at) ?? 0;
167
- const floor = valueAsMinutes(this.min) ?? 0;
168
- const ceiling = valueAsMinutes(this.max) ?? 1439;
169
- if (minutes < floor) {
170
- return [
171
- 'rangeUnderflow',
172
- fillTemplate(this.rangeUnderflowMessage || 'Choose a time at or after {min}.', {
173
- min: writeTime(this.min, this.locale),
174
- }),
175
- ];
176
- }
177
- if (minutes > ceiling) {
178
- return [
179
- 'rangeOverflow',
180
- fillTemplate(this.rangeOverflowMessage || 'Choose a time at or before {max}.', {
181
- max: writeTime(this.max, this.locale),
182
- }),
183
- ];
184
- }
185
- if (this.barred(at)) {
186
- return ['badInput', this.unavailableMessage || 'Choose a time that is available.'];
187
- }
188
- if (!this.times.includes(minutes)) {
189
- return ['stepMismatch', this.stepMismatchMessage || 'Choose one of the times on offer.'];
178
+ switch (timeFault(at, {
179
+ disableTimes: this.disableTimes,
180
+ max: this.max,
181
+ min: this.min,
182
+ offered: this.times,
183
+ })) {
184
+ case 'rangeUnderflow':
185
+ return [
186
+ 'rangeUnderflow',
187
+ fillTemplate(this.rangeUnderflowMessage || 'Choose a time at or after {min}.', {
188
+ min: writeClock(this.min, this.clock, this.seconds),
189
+ }),
190
+ ];
191
+ case 'rangeOverflow':
192
+ return [
193
+ 'rangeOverflow',
194
+ fillTemplate(this.rangeOverflowMessage || 'Choose a time at or before {max}.', {
195
+ max: writeClock(this.max, this.clock, this.seconds),
196
+ }),
197
+ ];
198
+ case 'unavailable':
199
+ return ['badInput', this.unavailableMessage || 'Choose a time that is available.'];
200
+ case 'stepMismatch':
201
+ return ['stepMismatch', this.stepMismatchMessage || 'Choose one of the times on offer.'];
202
+ default:
203
+ return undefined;
190
204
  }
191
- return undefined;
192
205
  }
193
206
  reportRequired() {
194
207
  const anchor = this.partNamed('list') ?? undefined;
@@ -197,11 +210,12 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
197
210
  return;
198
211
  }
199
212
  const held = this.held;
200
- if (held.length > 0 && held.some((one) => asTime(one) === null)) {
213
+ if (held.length > 0 && held.some((one) => valueAsSeconds(one) === null)) {
214
+ const shape = this.seconds ? 'HH:mm:ss' : 'HH:mm';
201
215
  this.internals.setValidity({ badInput: true }, this.badInputMessage ||
202
216
  (this.mode === undefined
203
- ? 'Enter a time as HH:mm.'
204
- : 'Enter times as HH:mm, separated by commas.'), anchor);
217
+ ? `Enter a time as ${shape}.`
218
+ : `Enter times as ${shape}, separated by commas.`), anchor);
205
219
  return;
206
220
  }
207
221
  for (const one of held) {
@@ -297,9 +311,8 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
297
311
  </${this.tag('listbox')}>`;
298
312
  }
299
313
  renderSpinner() {
300
- const twelve = isTwelveHour(this.locale);
301
- const { hour, meridiem, minute } = this.parts;
302
- const shownHour = twelve ? String(hour % 12 === 0 ? 12 : hour % 12) : String(hour);
314
+ const twelve = isTwelveHour(this.clock);
315
+ const { hour, meridiem, minute, second, shown } = this.parts;
303
316
  const hours = twelve
304
317
  ? [...new Set(this.hours.map((one) => (one % 12 === 0 ? 12 : one % 12)))].sort((a, b) => a - b)
305
318
  : this.hours;
@@ -308,11 +321,15 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
308
321
  role="group"
309
322
  aria-label=${this.listName}
310
323
  data-unset=${this.value === '' ? '' : nothing}
324
+ @keydown=${this.handleColumnsKeydown}
311
325
  >
312
- ${this.renderColumn('hour', this.hourLabel, hours.map(String), (one) => (twelve ? one : one.padStart(2, '0')), shownHour, (one) => this.wholeHourBarred(Number(one), twelve ? meridiem : undefined))}
313
- ${this.renderColumn('minute', this.minuteLabel, this.minutes.map(String), (one) => one.padStart(2, '0'), String(minute), (one) => this.barred(minutesAsValue(hour * 60 + Number(one))))}
326
+ ${this.renderColumn('hour', this.hourLabel, hours.map(String), (one) => (twelve ? one : one.padStart(2, '0')), String(shown), (one) => this.wholeHourBarred(Number(one), twelve ? meridiem : undefined))}
327
+ ${this.renderColumn('minute', this.minuteLabel, this.minutes.map(String), (one) => one.padStart(2, '0'), String(minute), (one) => this.barredAt(hour * 3600 + Number(one) * 60))}
328
+ ${this.seconds
329
+ ? this.renderColumn('second', this.secondLabel, this.secondsShown.map(String), (one) => one.padStart(2, '0'), String(second), (one) => this.barredAt(hour * 3600 + minute * 60 + Number(one)))
330
+ : nothing}
314
331
  ${twelve
315
- ? this.renderColumn('meridiem', this.meridiemLabel, ['AM', 'PM'], (one) => dayPeriod(one, this.locale), meridiem, (one) => this.halfBarred(one))
332
+ ? this.renderColumn('meridiem', this.meridiemLabel, ['AM', 'PM'], (one) => dayPeriod(one, this.clock), meridiem, (one) => this.halfBarred(one))
316
333
  : nothing}
317
334
  </div>`;
318
335
  }
@@ -332,8 +349,8 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
332
349
  @helix-before-change=${this.handleChange}
333
350
  @input=${this.handleListInput}
334
351
  >
335
- ${this.times.map((minutes) => {
336
- const at = minutesAsValue(minutes);
352
+ ${this.times.map((seconds) => {
353
+ const at = this.valueFor(seconds);
337
354
  const between = this.between(at);
338
355
  return html `<${this.tag('option')}
339
356
  part="option${between ? ' option-between' : ''}"
@@ -344,7 +361,7 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
344
361
  ?disabled=${this.barred(at)}
345
362
  ?selected=${this.held.includes(at)}
346
363
  data-between=${between ? '' : nothing}
347
- >${writeTime(at, this.locale)}</${this.tag('option')}
364
+ >${writeClock(at, this.clock, this.seconds)}</${this.tag('option')}
348
365
  >`;
349
366
  })}
350
367
  </${this.tag('listbox')}>`;
@@ -356,16 +373,20 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinControl
356
373
  return html `${this.renderList()}`;
357
374
  }
358
375
  get valueAsDate() {
359
- return asTime(this.value);
376
+ const at = valueAsSeconds(this.value);
377
+ return at === null ? null : new Date(at * 1000);
360
378
  }
361
379
  set valueAsDate(date) {
362
- this.value = fromTime(date);
380
+ this.valueAsNumber = date ? date.getTime() : Number.NaN;
363
381
  }
364
382
  get valueAsNumber() {
365
- return asTime(this.value)?.getTime() ?? Number.NaN;
383
+ const at = valueAsSeconds(this.value);
384
+ return at === null ? Number.NaN : at * 1000;
366
385
  }
367
386
  set valueAsNumber(ms) {
368
- this.value = Number.isNaN(ms) ? '' : fromTime(new Date(ms));
387
+ this.value = Number.isNaN(ms)
388
+ ? ''
389
+ : this.valueFor(Math.floor((((ms % 86_400_000) + 86_400_000) % 86_400_000) / 1000));
369
390
  }
370
391
  };
371
392
  __decorate([
@@ -374,6 +395,9 @@ __decorate([
374
395
  __decorate([
375
396
  property({ attribute: 'disable-times' })
376
397
  ], HelixHeadlessTimePicker.prototype, "disableTimes", void 0);
398
+ __decorate([
399
+ property({ attribute: 'hour-cycle' })
400
+ ], HelixHeadlessTimePicker.prototype, "hourCycle", void 0);
377
401
  __decorate([
378
402
  property({ attribute: 'hour-label' })
379
403
  ], HelixHeadlessTimePicker.prototype, "hourLabel", void 0);
@@ -416,6 +440,12 @@ __decorate([
416
440
  __decorate([
417
441
  property({ reflect: true, type: Boolean })
418
442
  ], HelixHeadlessTimePicker.prototype, "required", void 0);
443
+ __decorate([
444
+ property({ attribute: 'second-label' })
445
+ ], HelixHeadlessTimePicker.prototype, "secondLabel", void 0);
446
+ __decorate([
447
+ property({ type: Boolean })
448
+ ], HelixHeadlessTimePicker.prototype, "seconds", void 0);
419
449
  __decorate([
420
450
  property({ type: Number })
421
451
  ], HelixHeadlessTimePicker.prototype, "step", void 0);
@@ -0,0 +1,22 @@
1
+ import { type ChordPress } from '@fusion.dev/helix-core';
2
+ /** A key and the modifiers held with it. */
3
+ export interface Chord {
4
+ alt: boolean;
5
+ ctrl: boolean;
6
+ key: string;
7
+ meta: boolean;
8
+ shift: boolean;
9
+ }
10
+ /** What a key press carries that a chord is compared against. */
11
+ export type Pressed = ChordPress;
12
+ /**
13
+ * Reads a chord written as keys joined by `+`, such as `F8` or `Alt+T`, case
14
+ * aside. The last part is the key. Empty, or a chord with no key, is none.
15
+ *
16
+ * Split as core's `chordParts` splits every chord, and read more strictly than
17
+ * Kbd reads one: four modifiers by their own names, no `mod`, and Shift held
18
+ * exactly as written, so the key that moves to the stack is the one written.
19
+ */
20
+ export declare const parseHotkey: (text: string) => Chord | null;
21
+ /** Whether a press is the chord: the same key, case aside, and exactly its modifiers. */
22
+ export declare const matchesHotkey: (pressed: Pressed, chord: Chord | null) => boolean;
@@ -0,0 +1,23 @@
1
+ import { chordParts } from '@fusion.dev/helix-core';
2
+ const MODIFIERS = new Set(['alt', 'ctrl', 'meta', 'shift']);
3
+ export const parseHotkey = (text) => {
4
+ const parts = chordParts(text);
5
+ const key = parts.pop();
6
+ if (!key || MODIFIERS.has(key.toLowerCase())) {
7
+ return null;
8
+ }
9
+ const held = new Set(parts.map((one) => one.toLowerCase()));
10
+ return {
11
+ alt: held.has('alt'),
12
+ ctrl: held.has('ctrl'),
13
+ key,
14
+ meta: held.has('meta'),
15
+ shift: held.has('shift'),
16
+ };
17
+ };
18
+ export const matchesHotkey = (pressed, chord) => chord !== null &&
19
+ pressed.key.toLowerCase() === chord.key.toLowerCase() &&
20
+ pressed.altKey === chord.alt &&
21
+ pressed.ctrlKey === chord.ctrl &&
22
+ pressed.metaKey === chord.meta &&
23
+ pressed.shiftKey === chord.shift;
@@ -1,4 +1,4 @@
1
- import { HelixElement, type ToneMark } from '@fusion.dev/helix-core';
1
+ import { HelixElement, SwipeController, type ToneMark } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  declare const HelixHeadlessToastItem_base: (new (...args: any[]) => import("@fusion.dev/helix-core").DismissibleElement) & typeof HelixElement;
4
4
  /**
@@ -12,6 +12,16 @@ declare const HelixHeadlessToastItem_base: (new (...args: any[]) => import("@fus
12
12
  * with each new message, and the last of three messages would stay for nine
13
13
  * seconds.
14
14
  *
15
+ * **Each message is an `alertdialog` named by its words**, which is how a reader
16
+ * who moves into the stack hears which message they are on. A `danger` message
17
+ * is its own assertive live region, so it is announced at once; every other one
18
+ * is announced politely by the region.
19
+ *
20
+ * A touch or a pen swipes a message toward the end of the line, and past
21
+ * halfway, or quickly, dismisses it through the same `helix-dismiss` as its
22
+ * control. While it moves, the distance is on the `item` part as
23
+ * `--toast-swipe-x`.
24
+ *
15
25
  * **Messages are inverted by default.** The item flips `color-scheme`, so a new
16
26
  * message stands out from the page without using a color that carries meaning.
17
27
  *
@@ -39,6 +49,7 @@ declare const HelixHeadlessToastItem_base: (new (...args: any[]) => import("@fus
39
49
  * @csspart timer - The bar showing how long is left, where a duration was given.
40
50
  *
41
51
  * @cssproperty --toast-duration - How long this message stays, in milliseconds, for a progress hint.
52
+ * @cssproperty --toast-swipe-x - How far a swipe has moved the message across, in pixels, toward the right.
42
53
  */
43
54
  export declare class HelixHeadlessToastItem extends HelixHeadlessToastItem_base {
44
55
  /**
@@ -64,6 +75,9 @@ export declare class HelixHeadlessToastItem extends HelixHeadlessToastItem_base
64
75
  */
65
76
  get open(): boolean;
66
77
  set open(next: boolean);
78
+ protected readonly swipes: SwipeController;
79
+ /** Writes how far a swipe has moved the message toward the end of the line. */
80
+ private slide;
67
81
  /** When the current run of the timer began, or nothing while it is paused. */
68
82
  private startedAt;
69
83
  /** What is left of the duration, so resuming continues rather than restarts. */
@@ -106,6 +120,7 @@ export declare class HelixHeadlessToastItem extends HelixHeadlessToastItem_base
106
120
  connectedCallback(): void;
107
121
  disconnectedCallback(): void;
108
122
  protected willUpdate(changed: PropertyValues): void;
123
+ protected updated(changed: PropertyValues): void;
109
124
  protected firstUpdated(changed: PropertyValues): void;
110
125
  /**
111
126
  * The mark for the message's tone, behind a seam.
@@ -4,14 +4,33 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { customElement, HelixElement, MixinDismissible, requestChange, toneMark, } from '@fusion.dev/helix-core';
8
- import { x } from '@fusion.dev/helix-icons';
7
+ import { customElement, drawDismiss, drawDismissGlyph, HelixElement, hostAria, isRtl, MixinDismissible, requestChange, SwipeController, settleSwipe, swipeEdge, toneMark, towardEdge, } from '@fusion.dev/helix-core';
9
8
  import { html, nothing } from 'lit';
10
9
  import { property } from 'lit/decorators.js';
11
10
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
11
  let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissible(HelixElement) {
13
12
  constructor() {
14
13
  super(...arguments);
14
+ this.swipes = new SwipeController(this, {
15
+ axis: () => (this.open ? 'x' : undefined),
16
+ end: (dx, _dy, vx) => {
17
+ const edge = swipeEdge('end', isRtl(this));
18
+ const width = this.partNamed('item')?.getBoundingClientRect().width ?? 0;
19
+ const shown = settleSwipe({
20
+ points: [1],
21
+ size: width,
22
+ travel: Math.max(0, towardEdge(edge, dx, 0)),
23
+ velocity: towardEdge(edge, vx, 0),
24
+ });
25
+ if (shown === 0 && this.dismiss()) {
26
+ return;
27
+ }
28
+ this.slide(0);
29
+ },
30
+ move: (dx) => this.slide(Math.max(0, towardEdge(swipeEdge('end', isRtl(this)), dx, 0))),
31
+ start: (_target, event) => event.pointerType !== 'mouse',
32
+ surface: () => this.partNamed('item'),
33
+ });
15
34
  this.openState = undefined;
16
35
  this.holds = 0;
17
36
  this.handlePause = () => {
@@ -33,6 +52,10 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
33
52
  this.openState = next;
34
53
  this.requestUpdate('open', previous);
35
54
  }
55
+ slide(travel) {
56
+ const physical = isRtl(this) ? -travel : travel;
57
+ this.partNamed('item')?.style.setProperty('--toast-swipe-x', travel === 0 ? '' : `${Math.round(physical)}px`);
58
+ }
36
59
  dismiss() {
37
60
  if (!this.open) {
38
61
  return true;
@@ -86,6 +109,16 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
86
109
  super.willUpdate(changed);
87
110
  this.toggleAttribute('data-closed', !this.open);
88
111
  this.toggleAttribute('hidden', !this.open);
112
+ hostAria(this.internals, { role: 'alertdialog' });
113
+ this.internals.ariaModal = 'false';
114
+ this.internals.ariaLive = this.tone === 'danger' ? 'assertive' : null;
115
+ }
116
+ updated(changed) {
117
+ super.updated(changed);
118
+ const words = this.partNamed('content');
119
+ if (words) {
120
+ hostAria(this.internals, { labelledBy: [words] });
121
+ }
89
122
  }
90
123
  firstUpdated(changed) {
91
124
  super.firstUpdated(changed);
@@ -108,9 +141,7 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
108
141
  }
109
142
  renderDismissIcon() {
110
143
  return html `<slot name="dismiss-icon">
111
- <svg part="dismiss-mark" viewBox=${x.viewBox} fill="currentColor" aria-hidden="true">
112
- ${unsafeSVG(x.content)}
113
- </svg>
144
+ ${drawDismissGlyph()}
114
145
  </slot>`;
115
146
  }
116
147
  render() {
@@ -130,14 +161,11 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
130
161
  <div part="content"><slot></slot></div>
131
162
  <div part="actions"><slot name="actions"></slot></div>
132
163
  ${this.dismissible
133
- ? html `<button
134
- part="dismiss"
135
- type="button"
136
- aria-label=${this.dismissLabel}
137
- @click=${() => this.dismiss()}
138
- >
139
- ${this.renderDismissIcon()}
140
- </button>`
164
+ ? drawDismiss({
165
+ label: this.dismissLabel,
166
+ mark: this.renderDismissIcon(),
167
+ onDismiss: () => this.dismiss(),
168
+ })
141
169
  : nothing}
142
170
  ${this.duration
143
171
  ? html `<div
@@ -9,9 +9,15 @@ import { type PropertyValues, type TemplateResult } from 'lit';
9
9
  * announce a region that arrives already holding its message. `Radio.Group` is
10
10
  * part of its family for the same reason.
11
11
  *
12
- * **The region is polite, never assertive.** `role="alert"` interrupts whatever
13
- * a screen reader is reading. For something that cannot wait, use a `Dialog`,
14
- * which stops the page for every user.
12
+ * **The region is a landmark named by `label`, and a polite live region.** A
13
+ * `danger` message is announced assertively, and every other one politely.
14
+ * Each message is an `alertdialog` named by its words, so a reader who moves
15
+ * into the stack hears which message they are on.
16
+ *
17
+ * **`hotkey` takes focus to the stack and back**, F8 unless you set it, and
18
+ * `label` says which key: `{hotkey}` in it is filled in, so the default name
19
+ * is "Notifications (F8)". Pressed while focus is in the stack, it gives focus
20
+ * back to where it was.
15
21
  *
16
22
  * **The region never creates or removes items.** You render items and remove
17
23
  * them. An item requests closing with a cancelable `helix-dismiss` event. A
@@ -19,9 +25,11 @@ import { type PropertyValues, type TemplateResult } from 'lit';
19
25
  * and the DOM stay in sync. It hides on its own unless you set `open`, which
20
26
  * makes the state yours.
21
27
  *
22
- * There is no `toast.show()`. For a `toast()` function, write a small one over
23
- * your app's state, which also lets you render the same messages on the server,
24
- * in a test, or more than once.
28
+ * **`add()` shows a message from script**, for a page with no framework state
29
+ * to render them from: it appends a message of the region's own tier and
30
+ * removes it once it has been dismissed and has finished leaving. Messages
31
+ * you render yourself stay yours, and the region never removes them. A touch
32
+ * or a pen swipes a message away toward the end of the line.
25
33
  *
26
34
  * **The stack is in the top layer.** The `region` part is a `popover="manual"`
27
35
  * shown while a message is showing, and shown again above a modal dialog that
@@ -46,11 +54,17 @@ import { type PropertyValues, type TemplateResult } from 'lit';
46
54
  *
47
55
  * @event {HelixDismissEvent} helix-dismiss - A message is being dismissed, by its control or by its timer. The message sends it, and you can listen for it on the region. Cancelable: canceling it keeps the message.
48
56
  *
49
- * @csspart region - The stack the messages are in. It is in the top layer while a message shows.
57
+ * @csspart region - The stack the messages are in. It is in the top layer while a message shows, and takes focus from `hotkey` where no message holds a control.
50
58
  */
51
59
  export declare class HelixHeadlessToast extends HelixElement {
52
60
  /**
53
- * Accessible name for the region, such as "Notifications".
61
+ * The key that takes focus to the stack and back, such as `F8` or `Alt+T`:
62
+ * keys joined by `+`, the last one the key. Empty turns it off.
63
+ */
64
+ hotkey: string;
65
+ /**
66
+ * Accessible name for the region, with `{hotkey}` filled in. Translate it with
67
+ * the rest of your interface.
54
68
  */
55
69
  label: string;
56
70
  /**
@@ -83,6 +97,26 @@ export declare class HelixHeadlessToast extends HelixElement {
83
97
  protected willUpdate(changed: PropertyValues): void;
84
98
  /** The element focus came from when it entered the stack. */
85
99
  private focusOrigin;
100
+ private readonly scope;
101
+ /** Takes focus to the stack and back, on `hotkey`. */
102
+ private readonly handleHotkey;
103
+ /** Messages `add()` made, which are the region's to remove. */
104
+ private readonly made;
105
+ /**
106
+ * Shows a message from script, and returns the element.
107
+ *
108
+ * The message is the region's own tier of `Toast.Item`, appended to the
109
+ * stack, holding `message` as its text. The region removes it once it has
110
+ * been dismissed and has finished leaving, which is the one case where it
111
+ * removes anything: a message you render stays yours.
112
+ */
113
+ add(message: string, options?: {
114
+ dismissible?: boolean;
115
+ duration?: number;
116
+ tone?: 'danger' | 'info' | 'success' | 'warning';
117
+ }): HTMLElement;
118
+ /** Removes a message `add()` made, once it has hidden and finished leaving. */
119
+ private sweep;
86
120
  /** Watches the messages arrive and hide, to raise or lower the stack. */
87
121
  private watcher;
88
122
  /** The messages that are showing, in order. */