@fusion.dev/helix-core 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 (95) hide show
  1. package/custom-elements.json +9098 -3887
  2. package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +10 -0
  3. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +5 -0
  4. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +89 -0
  5. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +102 -1
  6. package/dist/controllers/chart-pointer-controller/reading-step.d.ts +57 -0
  7. package/dist/controllers/chart-pointer-controller/reading-step.js +45 -0
  8. package/dist/controllers/copy-controller/copy-controller.d.ts +11 -6
  9. package/dist/controllers/copy-controller/copy-controller.js +11 -9
  10. package/dist/controllers/delay-controller/delay-controller.d.ts +50 -0
  11. package/dist/controllers/delay-controller/delay-controller.js +54 -0
  12. package/dist/controllers/focus-scope-controller/focus-scope-controller.d.ts +56 -0
  13. package/dist/controllers/focus-scope-controller/focus-scope-controller.js +71 -0
  14. package/dist/controllers/open-phase-controller/open-phase-controller.d.ts +49 -0
  15. package/dist/controllers/open-phase-controller/open-phase-controller.js +41 -0
  16. package/dist/controllers/source-controller/source-controller.d.ts +16 -0
  17. package/dist/controllers/source-controller/source-controller.js +44 -1
  18. package/dist/controllers/status-controller/status-controller.d.ts +47 -0
  19. package/dist/controllers/status-controller/status-controller.js +34 -0
  20. package/dist/controllers/swipe-controller/swipe-controller.d.ts +61 -0
  21. package/dist/controllers/swipe-controller/swipe-controller.js +106 -0
  22. package/dist/events/events.d.ts +35 -0
  23. package/dist/events/events.js +12 -0
  24. package/dist/index.d.ts +28 -8
  25. package/dist/index.js +27 -7
  26. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +7 -2
  27. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +5 -1
  28. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.d.ts +10 -0
  29. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +9 -0
  30. package/dist/mixins/mixin-dismissible/mixin-dismissible.d.ts +2 -0
  31. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +7 -3
  32. package/dist/mixins/mixin-field/mixin-field.d.ts +2 -0
  33. package/dist/mixins/mixin-field/mixin-field.js +8 -0
  34. package/dist/mixins/mixin-picker-editing/mixin-picker-editing.d.ts +16 -6
  35. package/dist/mixins/mixin-picker-editing/mixin-picker-editing.js +19 -1
  36. package/dist/mixins/mixin-picker-field/mixin-picker-field.d.ts +5 -4
  37. package/dist/mixins/mixin-picker-field/mixin-picker-field.js +12 -1
  38. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +67 -0
  39. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +243 -0
  40. package/dist/utils/arrow/arrow.d.ts +15 -0
  41. package/dist/utils/arrow/arrow.js +2 -0
  42. package/dist/utils/audit-name/audit-name.d.ts +28 -0
  43. package/dist/utils/audit-name/audit-name.js +95 -0
  44. package/dist/utils/calendar/calendar.d.ts +117 -0
  45. package/dist/utils/calendar/calendar.js +96 -0
  46. package/dist/utils/chord/chord.d.ts +43 -0
  47. package/dist/utils/chord/chord.js +53 -0
  48. package/dist/utils/clock/clock.d.ts +87 -0
  49. package/dist/utils/clock/clock.js +68 -0
  50. package/dist/utils/color-name/color-name.d.ts +28 -0
  51. package/dist/utils/color-name/color-name.js +150 -0
  52. package/dist/utils/count-label/count-label.d.ts +13 -0
  53. package/dist/utils/count-label/count-label.js +5 -0
  54. package/dist/utils/css-color/css-color.d.ts +12 -0
  55. package/dist/utils/css-color/css-color.js +2 -2
  56. package/dist/utils/date-segments/date-segments.d.ts +122 -0
  57. package/dist/utils/date-segments/date-segments.js +206 -0
  58. package/dist/utils/draw-dismiss/draw-dismiss.d.ts +30 -0
  59. package/dist/utils/draw-dismiss/draw-dismiss.js +16 -0
  60. package/dist/utils/draw-tooltip/draw-tooltip.d.ts +15 -21
  61. package/dist/utils/draw-tooltip/draw-tooltip.js +11 -5
  62. package/dist/utils/draw-tooltip/spoken-reading.d.ts +10 -0
  63. package/dist/utils/draw-tooltip/spoken-reading.js +10 -0
  64. package/dist/utils/follow-link/follow-link.d.ts +46 -0
  65. package/dist/utils/follow-link/follow-link.js +37 -0
  66. package/dist/utils/heading-in/heading-in.d.ts +19 -0
  67. package/dist/utils/heading-in/heading-in.js +14 -0
  68. package/dist/utils/number-format/number-format.d.ts +23 -0
  69. package/dist/utils/number-format/number-format.js +55 -0
  70. package/dist/utils/platform/platform.d.ts +23 -0
  71. package/dist/utils/platform/platform.js +11 -0
  72. package/dist/utils/read-color/read-color.d.ts +14 -0
  73. package/dist/utils/read-color/read-color.js +13 -0
  74. package/dist/utils/read-series/read-series.d.ts +6 -0
  75. package/dist/utils/read-series/read-series.js +1 -0
  76. package/dist/utils/reflect-elements/reflect-elements.d.ts +37 -0
  77. package/dist/utils/reflect-elements/reflect-elements.js +40 -0
  78. package/dist/utils/relative-time/relative-time.d.ts +13 -1
  79. package/dist/utils/relative-time/relative-time.js +7 -4
  80. package/dist/utils/report-runtime/report-runtime.d.ts +2 -0
  81. package/dist/utils/roving-stop/roving-stop.d.ts +20 -0
  82. package/dist/utils/roving-stop/roving-stop.js +3 -0
  83. package/dist/utils/swipe/swipe.d.ts +63 -0
  84. package/dist/utils/swipe/swipe.js +48 -0
  85. package/dist/utils/tab-stops/tab-stops.d.ts +16 -0
  86. package/dist/utils/tab-stops/tab-stops.js +22 -0
  87. package/dist/utils/value-as/value-as.d.ts +20 -0
  88. package/dist/utils/value-as/value-as.js +16 -0
  89. package/dist/utils/write-time/write-time.d.ts +11 -1
  90. package/dist/utils/write-time/write-time.js +7 -7
  91. package/dist/utils/written-list/written-list.d.ts +18 -0
  92. package/dist/utils/written-list/written-list.js +8 -0
  93. package/dist/version.d.ts +1 -1
  94. package/dist/version.js +1 -1
  95. package/package.json +4 -4
@@ -0,0 +1,243 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
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
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { html, nothing } from 'lit';
8
+ import { property, state } from 'lit/decorators.js';
9
+ import { isPartial, isTwelveHourLayout, segmentKey, segmentLayout, segmentName, segmentRange, segmentsOf, segmentsValue, segmentText, segmentValueText, shownValue, } from '../../utils/date-segments/date-segments.js';
10
+ import { inlineStep } from '../../utils/inline-step/inline-step.js';
11
+ export const MixinSegmentedField = (Base) => {
12
+ class SegmentedFieldElement extends Base {
13
+ constructor() {
14
+ super(...arguments);
15
+ this.emptySegmentLabel = 'Empty';
16
+ this.segmented = false;
17
+ this.parts = {};
18
+ this.typed = '';
19
+ this.own = false;
20
+ this.entered = undefined;
21
+ this.handleSegmentKeydown = (event) => {
22
+ const element = event.currentTarget;
23
+ const kind = element.dataset.segment;
24
+ const twelve = isTwelveHourLayout(this.layout);
25
+ if (event.key === 'ArrowDown' && event.altKey) {
26
+ event.preventDefault();
27
+ this.handleTriggerClick();
28
+ return;
29
+ }
30
+ const along = inlineStep(this, event.key);
31
+ if (along !== 0) {
32
+ event.preventDefault();
33
+ this.typed = '';
34
+ this.moveFocus(element, along);
35
+ return;
36
+ }
37
+ if (event.key === 'Tab') {
38
+ this.typed = '';
39
+ if (this.open) {
40
+ this.requestOpen(false);
41
+ }
42
+ return;
43
+ }
44
+ if (event.metaKey || event.ctrlKey || event.altKey) {
45
+ this.typed = '';
46
+ return;
47
+ }
48
+ if (this.readOnly || this.isDisabled) {
49
+ return;
50
+ }
51
+ const edit = segmentKey(event.key, {
52
+ kind,
53
+ locale: this.segmentLocale,
54
+ segments: this.parts,
55
+ twelve,
56
+ typed: this.typed,
57
+ });
58
+ if (edit) {
59
+ event.preventDefault();
60
+ this.typed = edit.typed;
61
+ this.take(edit.segments);
62
+ if (edit.advance) {
63
+ this.moveFocus(element, 1);
64
+ }
65
+ }
66
+ };
67
+ this.handleSegmentsFocusIn = (event) => {
68
+ this.typed = '';
69
+ if (!this.segmentElements.includes(event.relatedTarget)) {
70
+ this.entered = this.value;
71
+ }
72
+ };
73
+ this.handleSegmentsFocusOut = (event) => {
74
+ if (this.segmentElements.includes(event.relatedTarget)) {
75
+ return;
76
+ }
77
+ if (this.entered !== undefined && this.entered !== this.value) {
78
+ this.announceChange();
79
+ }
80
+ this.entered = undefined;
81
+ };
82
+ this.handleLabelPress = (event) => {
83
+ if (!this.segmentsShown || this.isDisabled) {
84
+ return;
85
+ }
86
+ const label = this.renderRoot?.querySelector?.('[part~="label"]');
87
+ if (label && event.composedPath().includes(label)) {
88
+ this.anchor?.focus();
89
+ }
90
+ };
91
+ }
92
+ get segmentable() {
93
+ return true;
94
+ }
95
+ get segmentsShown() {
96
+ return this.segmented && this.segmentable;
97
+ }
98
+ get segmentedKind() {
99
+ return 'date';
100
+ }
101
+ get segmentLocale() {
102
+ return '';
103
+ }
104
+ get segmentOptions() {
105
+ return {};
106
+ }
107
+ get layout() {
108
+ return segmentLayout(this.segmentedKind, this.segmentLocale, this.segmentOptions);
109
+ }
110
+ get anchor() {
111
+ if (this.segmentsShown) {
112
+ return this.renderRoot?.querySelector('[part~="segment"]') ?? null;
113
+ }
114
+ return super.anchor;
115
+ }
116
+ get segmentGroup() {
117
+ return this.renderRoot?.querySelector('[part="segments"]') ?? null;
118
+ }
119
+ get segmentElements() {
120
+ return [...(this.renderRoot?.querySelectorAll('[part~="segment"]') ?? [])];
121
+ }
122
+ take(next) {
123
+ this.parts = next;
124
+ const value = segmentsValue(this.segmentedKind, next, this.layout);
125
+ if (value === this.value) {
126
+ this.sync();
127
+ return;
128
+ }
129
+ this.own = true;
130
+ if (!this.requestValue(value)) {
131
+ this.own = false;
132
+ this.parts = segmentsOf(this.segmentedKind, this.value);
133
+ }
134
+ }
135
+ moveFocus(from, by) {
136
+ const all = this.segmentElements;
137
+ all[all.indexOf(from) + by]?.focus();
138
+ }
139
+ willUpdate(changed) {
140
+ super.willUpdate(changed);
141
+ const own = this.own;
142
+ this.own = false;
143
+ if (!own &&
144
+ this.segmentsShown &&
145
+ segmentsValue(this.segmentedKind, this.parts, this.layout) !== this.value) {
146
+ this.parts = segmentsOf(this.segmentedKind, this.value);
147
+ }
148
+ }
149
+ formResetCallback() {
150
+ super.formResetCallback();
151
+ this.parts = {};
152
+ this.typed = '';
153
+ }
154
+ connectedCallback() {
155
+ super.connectedCallback();
156
+ this.addEventListener('click', this.handleLabelPress);
157
+ }
158
+ disconnectedCallback() {
159
+ super.disconnectedCallback();
160
+ this.removeEventListener('click', this.handleLabelPress);
161
+ }
162
+ get blocksImplicitSubmission() {
163
+ return this.segmentsShown || super.blocksImplicitSubmission;
164
+ }
165
+ sync() {
166
+ super.sync();
167
+ if (!this.segmentsShown) {
168
+ return;
169
+ }
170
+ const anchor = this.anchor ?? undefined;
171
+ const partial = isPartial(this.segmentedKind, this.parts, this.layout);
172
+ const message = this.validate();
173
+ if (partial) {
174
+ this.internals.setValidity({ badInput: true }, this.badInputMessage ||
175
+ (this.segmentedKind === 'date' ? 'Enter a whole date.' : 'Enter a whole time.'), anchor);
176
+ }
177
+ else if (this.required && this.value === '') {
178
+ this.internals.setValidity({ valueMissing: true }, this.valueMissingMessage ||
179
+ (this.segmentedKind === 'date' ? 'Enter a date.' : 'Enter a time.'), anchor);
180
+ }
181
+ else if (message !== '') {
182
+ this.internals.setValidity({ badInput: true }, message, anchor);
183
+ }
184
+ else {
185
+ this.internals.setValidity({});
186
+ }
187
+ this.refreshFeedback();
188
+ }
189
+ renderSegments() {
190
+ const layout = this.layout;
191
+ const twelve = isTwelveHourLayout(layout);
192
+ const locale = this.segmentLocale;
193
+ return html `<div
194
+ part="segments"
195
+ id=${this.controlId}
196
+ role="group"
197
+ aria-labelledby=${this.labeled ? this.labelId : nothing}
198
+ aria-describedby=${this.describedBy ?? nothing}
199
+ aria-invalid=${this.invalidity ?? nothing}
200
+ @focusin=${this.handleSegmentsFocusIn}
201
+ @focusout=${this.handleSegmentsFocusOut}
202
+ @keypress=${this.handleKeypress}
203
+ >
204
+ ${layout.map((slot) => {
205
+ if ('literal' in slot) {
206
+ return html `<span part="literal" aria-hidden="true">${slot.literal}</span>`;
207
+ }
208
+ const kind = slot.kind;
209
+ const shown = shownValue(kind, this.parts, twelve);
210
+ const { max, min } = segmentRange(kind, this.parts, twelve);
211
+ const text = segmentText(kind, this.parts, twelve, locale);
212
+ const empty = shown === undefined;
213
+ return html `<span
214
+ part="segment${empty ? ' segment-placeholder' : ''}"
215
+ data-segment=${kind}
216
+ role="spinbutton"
217
+ tabindex=${this.isDisabled ? '-1' : '0'}
218
+ inputmode=${kind === 'dayPeriod' ? nothing : 'numeric'}
219
+ aria-label=${segmentName(kind, locale)}
220
+ aria-valuemin=${min}
221
+ aria-valuemax=${max}
222
+ aria-valuenow=${shown ?? nothing}
223
+ aria-valuetext=${empty ? this.emptySegmentLabel : segmentValueText(kind, this.parts, twelve, locale)}
224
+ aria-disabled=${this.isDisabled ? 'true' : nothing}
225
+ aria-readonly=${this.readOnly ? 'true' : nothing}
226
+ @keydown=${this.handleSegmentKeydown}
227
+ >${text}</span
228
+ >`;
229
+ })}
230
+ </div>`;
231
+ }
232
+ }
233
+ __decorate([
234
+ property({ attribute: 'empty-segment-label' })
235
+ ], SegmentedFieldElement.prototype, "emptySegmentLabel", void 0);
236
+ __decorate([
237
+ property({ type: Boolean })
238
+ ], SegmentedFieldElement.prototype, "segmented", void 0);
239
+ __decorate([
240
+ state()
241
+ ], SegmentedFieldElement.prototype, "parts", void 0);
242
+ return SegmentedFieldElement;
243
+ };
@@ -0,0 +1,15 @@
1
+ import type { Placement, Side } from '../../controllers/floating-controller/floating-controller.js';
2
+ /**
3
+ * How far a panel's arrow reaches past the edge it sits on, which is what the
4
+ * panel's offset adds so the gap is measured to the point.
5
+ *
6
+ * A square of side `n` turned forty-five degrees about its center reaches
7
+ * `n / sqrt(2)` from it, and the arrow is placed with its center on the
8
+ * panel's edge. Read from the element rather than assumed, because the size
9
+ * is a hook a theme may move. `offsetWidth` rather than a rect, which reports
10
+ * the turned box and would count the reach twice. Zero for no arrow, and while
11
+ * the panel is closed and unrendered, when there is nothing to place.
12
+ */
13
+ export declare const arrowReach: (arrow: HTMLElement | null | undefined) => number;
14
+ /** The side of its anchor a placement puts a panel on: `top-start` is `top`. */
15
+ export declare const sideOf: (placement: Placement) => Side;
@@ -0,0 +1,2 @@
1
+ export const arrowReach = (arrow) => (arrow?.offsetWidth ?? 0) / Math.SQRT2;
2
+ export const sideOf = (placement) => placement.split('-')[0];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Whether an element has an accessible name, by the routes a page names a
3
+ * control: its own `aria-label`, `aria-labelledby` or `title`, a `<label>`, a
4
+ * `placeholder`, and its content where its role is named by its content.
5
+ *
6
+ * **An estimate, cheap on purpose.** It answers whether any route holds words,
7
+ * which is what the warning is about, and leaves the order the routes win in
8
+ * to the browser.
9
+ */
10
+ export declare const hasName: (element: HTMLElement) => boolean;
11
+ /**
12
+ * Warns once, through the runtime channel, when a control has no accessible
13
+ * name: none on the host, and none on the control inside it.
14
+ *
15
+ * **Read a task after the control first renders**, so a label that arrives in
16
+ * the same pass counts: slotted, written by a framework beside the control, or
17
+ * forwarded from a `<label for>`. One timer serves every control that asked in
18
+ * that task, and an element is read once in its life, so a name taken away
19
+ * later is not reported.
20
+ *
21
+ * **In every build.** Every runtime warning in the library reports through the
22
+ * same channel whatever the build, and a project that wants the console quiet
23
+ * tells `@fusion.dev/helix-diagnostics` with `core.warnings: false`.
24
+ *
25
+ * `also` answers for a name the component gave through its internals, which no
26
+ * attribute shows. `fix` is the sentence that says how to name it.
27
+ */
28
+ export declare const auditName: (host: HTMLElement, control: () => HTMLElement | null, fix: string, also?: () => boolean) => void;
@@ -0,0 +1,95 @@
1
+ import { reportRuntime } from '../report-runtime/report-runtime.js';
2
+ const nameText = (node) => {
3
+ if (node instanceof HTMLSlotElement) {
4
+ return node.assignedNodes({ flatten: true }).map(nameText).join('');
5
+ }
6
+ if (node.nodeType === Node.TEXT_NODE) {
7
+ return node.textContent ?? '';
8
+ }
9
+ if (!(node instanceof Element)) {
10
+ return '';
11
+ }
12
+ if ((node instanceof HTMLElement && node.hidden) || node.getAttribute('aria-hidden') === 'true') {
13
+ return '';
14
+ }
15
+ const said = node.getAttribute('aria-label') ?? node.getAttribute('alt');
16
+ if (said?.trim()) {
17
+ return said;
18
+ }
19
+ return [...node.childNodes].map(nameText).join('');
20
+ };
21
+ const namesWithWords = (ids, from) => {
22
+ const root = from.getRootNode();
23
+ return (ids ?? '')
24
+ .split(/\s+/)
25
+ .filter(Boolean)
26
+ .some((id) => {
27
+ const found = root.getElementById?.(id);
28
+ return found ? nameText(found).trim() !== '' : false;
29
+ });
30
+ };
31
+ const NAMED_BY_CONTENT = new Set([
32
+ 'button',
33
+ 'checkbox',
34
+ 'link',
35
+ 'menuitem',
36
+ 'menuitemcheckbox',
37
+ 'menuitemradio',
38
+ 'option',
39
+ 'radio',
40
+ 'switch',
41
+ 'tab',
42
+ ]);
43
+ export const hasName = (element) => {
44
+ for (const attribute of ['aria-label', 'title', 'placeholder']) {
45
+ if (element.getAttribute(attribute)?.trim()) {
46
+ return true;
47
+ }
48
+ }
49
+ if (namesWithWords(element.getAttribute('aria-labelledby'), element)) {
50
+ return true;
51
+ }
52
+ const referenced = element
53
+ .ariaLabelledByElements;
54
+ if ((referenced ?? []).some((one) => nameText(one).trim() !== '')) {
55
+ return true;
56
+ }
57
+ const labels = element.labels;
58
+ if ([...(labels ?? [])].some((label) => nameText(label).trim() !== '')) {
59
+ return true;
60
+ }
61
+ const role = element.getAttribute('role') ?? (element.localName === 'button' ? 'button' : '');
62
+ return NAMED_BY_CONTENT.has(role) && nameText(element).trim() !== '';
63
+ };
64
+ const audited = new WeakSet();
65
+ const waiting = new Map();
66
+ const readWaiting = () => {
67
+ const checks = [...waiting.values()];
68
+ waiting.clear();
69
+ for (const check of checks) {
70
+ check();
71
+ }
72
+ };
73
+ export const auditName = (host, control, fix, also) => {
74
+ if (audited.has(host) || waiting.has(host)) {
75
+ return;
76
+ }
77
+ if (waiting.size === 0) {
78
+ setTimeout(readWaiting, 0);
79
+ }
80
+ waiting.set(host, () => {
81
+ if (!host.isConnected) {
82
+ return;
83
+ }
84
+ audited.add(host);
85
+ const inner = control();
86
+ if (also?.() || hasName(host) || (inner !== null && hasName(inner))) {
87
+ return;
88
+ }
89
+ reportRuntime({
90
+ kind: 'missing-name',
91
+ message: `<${host.localName}> has no accessible name, so a screen reader announces it by its role alone. ${fix}`,
92
+ tag: host.localName,
93
+ });
94
+ });
95
+ };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Today, in the reader's own timezone.
3
+ *
4
+ * **`toISOString` converts to UTC first, which makes "today" a different day
5
+ * for most of the planet.** Anywhere west of UTC, from late afternoon onward,
6
+ * `fromDate(new Date())` names tomorrow. The parts are read locally instead and
7
+ * pasted together, which is the only way to get a calendar date out of a `Date`
8
+ * without a conversion happening on the way.
9
+ */
10
+ export declare const localToday: (now?: Date) => string;
11
+ /**
12
+ * The first day of the week where a language is spoken, `0` for Sunday.
13
+ *
14
+ * From `Intl.Locale`'s week information, which an engine offers as
15
+ * `getWeekInfo()` or, in older versions, as `weekInfo`. Sunday where it offers
16
+ * neither, as Firefox does not, or where the tag cannot be read.
17
+ */
18
+ export declare const weekStartOf: (locale: string) => number;
19
+ /** A month as `YYYY-MM`. */
20
+ export declare const monthOf: (date: Date) => string;
21
+ /** A date some days away, which may be negative, at midnight UTC. */
22
+ export declare const addDays: (date: Date, days: number) => Date;
23
+ /** Whether two dates fall in the same month of the same year. */
24
+ export declare const sameMonth: (left: Date, right: Date) => boolean;
25
+ /**
26
+ * A date some months away, clamped, so the last of a long month lands on the
27
+ * last of a short one instead of rolling into the month after it.
28
+ */
29
+ export declare const addMonths: (date: Date, months: number) => Date;
30
+ /** What bars a date, besides `min` and `max`. */
31
+ export interface Barring {
32
+ /** Dates as `YYYY-MM-DD`, separated by commas or spaces. */
33
+ disableDates: string;
34
+ /** Days of the week, `0` for Sunday, separated by commas or spaces. */
35
+ disableDays: string;
36
+ /** A consumer's own rule, asked of each date. */
37
+ isDateUnavailable?: ((date: string) => boolean) | undefined;
38
+ /** Where `step` counts from, or empty for 1 January 1970. */
39
+ min: string;
40
+ /** How far apart the selectable dates are, in days. */
41
+ step: number;
42
+ }
43
+ /**
44
+ * Why a date cannot be chosen, besides `min` and `max`, or `null` where it can.
45
+ *
46
+ * `listed` is a date the rules name: in `disable-dates`, on a day in
47
+ * `disable-days`, or one `isDateUnavailable` refuses. `step` is a date `step`
48
+ * skips, counted from `min` where there is one and from the epoch where there is
49
+ * not, which is the platform's own base. Whole days, because both ends are
50
+ * midnight UTC and neither carries a time.
51
+ */
52
+ export declare const barredBy: (date: Date, rules: Barring) => 'listed' | 'step' | null;
53
+ /** The views a calendar can show: the days of a month, the months of a year, or a page of years. */
54
+ export type CalendarView = 'day' | 'month' | 'year';
55
+ /** How many years the year view shows at once, in rows of three. */
56
+ export declare const YEARS_SHOWN = 12;
57
+ /** The first year on the page of years that holds `year`. */
58
+ export declare const yearPageOf: (year: number) => number;
59
+ /**
60
+ * Where a key moves within the months of a year or a page of years, laid out
61
+ * in rows of three, as an offset from `at`, the index on the page.
62
+ *
63
+ * `along` is which way the inline keys go, `1` or `-1`, or `0` for a key that
64
+ * is not one. The vertical keys move a row, Home and End to the ends of a row,
65
+ * and PageUp and PageDown a whole page. `null` is a key the grid leaves alone.
66
+ */
67
+ export declare const periodMove: (key: string, at: number, along: number) => number | null;
68
+ /** Whether a month, as its first day, lies wholly outside `min` and `max`, either of which may be empty. */
69
+ export declare const monthOut: (first: Date, min: string, max: string) => boolean;
70
+ /** Whether a year lies wholly outside `min` and `max`, either of which may be empty. */
71
+ export declare const yearOut: (year: number, min: string, max: string) => boolean;
72
+ /** One month or year in the months or the years view. */
73
+ export interface PeriodCell {
74
+ /** The month's index in its year, or the year. */
75
+ at: number;
76
+ /** Whether it is the month or year the calendar shows. */
77
+ chosen: boolean;
78
+ /** Whether today falls in it. */
79
+ current: boolean;
80
+ /** Its first day. */
81
+ first: Date;
82
+ /** Whether it lies wholly outside `min` and `max`. */
83
+ out: boolean;
84
+ }
85
+ /** What the months or the years view is drawn from. */
86
+ export interface PeriodPage {
87
+ /** The latest date, or empty. */
88
+ max: string;
89
+ /** The earliest date, or empty. */
90
+ min: string;
91
+ /** The year the months view is on, or any year on the page of years. */
92
+ page: number;
93
+ /** The first of the month the calendar shows. */
94
+ shown: Date;
95
+ /** Today, as `YYYY-MM-DD`. */
96
+ today: string;
97
+ /** Which view: `month` for the months of a year, `year` for a page of years. */
98
+ view: 'month' | 'year';
99
+ }
100
+ /**
101
+ * The twelve cells of the months or the years view, in reading order: the
102
+ * months of `page`, or the page of years that holds it.
103
+ */
104
+ export declare const periodCells: ({ max, min, page, shown, today, view }: PeriodPage) => PeriodCell[];
105
+ /**
106
+ * Where a key takes the tab stop in the months or the years, or `null` for a
107
+ * key the grid leaves alone.
108
+ *
109
+ * `stop` is the month's index or the year the stop is on, `year` the year the
110
+ * months view shows, and `along` which way the inline keys go, as
111
+ * `periodMove` takes it. A move off the months of one year lands in the next
112
+ * or the last, and a move off a page of years turns the page.
113
+ */
114
+ export declare const periodKey: (view: 'month' | 'year', stop: number, year: number, key: string, along: number) => {
115
+ focus: number;
116
+ pageYear: number;
117
+ } | null;
@@ -0,0 +1,96 @@
1
+ import { valueAsDate as asDate, dateAsValue as fromDate } from '../value-as/value-as.js';
2
+ import { writtenList } from '../written-list/written-list.js';
3
+ export const localToday = (now = new Date()) => {
4
+ const month = `${now.getMonth() + 1}`.padStart(2, '0');
5
+ const day = `${now.getDate()}`.padStart(2, '0');
6
+ return `${now.getFullYear()}-${month}-${day}`;
7
+ };
8
+ export const weekStartOf = (locale) => {
9
+ try {
10
+ const tag = new Intl.Locale(locale);
11
+ const first = tag.getWeekInfo?.().firstDay ?? tag.weekInfo?.firstDay;
12
+ return first === undefined ? 0 : first % 7;
13
+ }
14
+ catch {
15
+ return 0;
16
+ }
17
+ };
18
+ export const monthOf = (date) => `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;
19
+ export const addDays = (date, days) => new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + days));
20
+ export const sameMonth = (left, right) => left.getUTCFullYear() === right.getUTCFullYear() && left.getUTCMonth() === right.getUTCMonth();
21
+ export const addMonths = (date, months) => {
22
+ const target = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + months, 1));
23
+ const last = new Date(Date.UTC(target.getUTCFullYear(), target.getUTCMonth() + 1, 0)).getUTCDate();
24
+ return new Date(Date.UTC(target.getUTCFullYear(), target.getUTCMonth(), Math.min(date.getUTCDate(), last)));
25
+ };
26
+ export const barredBy = (date, rules) => {
27
+ const iso = fromDate(date);
28
+ if (writtenList(rules.disableDates).includes(iso) ||
29
+ writtenList(rules.disableDays).includes(String(date.getUTCDay())) ||
30
+ rules.isDateUnavailable?.(iso) === true) {
31
+ return 'listed';
32
+ }
33
+ const step = Math.max(1, Math.floor(rules.step) || 1);
34
+ if (step > 1) {
35
+ const base = rules.min === '' ? 0 : (asDate(rules.min)?.getTime() ?? 0);
36
+ if (Math.round((date.getTime() - base) / 86_400_000) % step !== 0) {
37
+ return 'step';
38
+ }
39
+ }
40
+ return null;
41
+ };
42
+ export const YEARS_SHOWN = 12;
43
+ export const yearPageOf = (year) => Math.floor(year / YEARS_SHOWN) * YEARS_SHOWN;
44
+ export const periodMove = (key, at, along) => {
45
+ if (along !== 0) {
46
+ return along;
47
+ }
48
+ switch (key) {
49
+ case 'ArrowUp':
50
+ return -3;
51
+ case 'ArrowDown':
52
+ return 3;
53
+ case 'Home':
54
+ return -(at % 3);
55
+ case 'End':
56
+ return 2 - (at % 3);
57
+ case 'PageUp':
58
+ return -12;
59
+ case 'PageDown':
60
+ return 12;
61
+ default:
62
+ return null;
63
+ }
64
+ };
65
+ export const monthOut = (first, min, max) => {
66
+ const month = monthOf(first);
67
+ return (min !== '' && month < min.slice(0, 7)) || (max !== '' && month > max.slice(0, 7));
68
+ };
69
+ export const yearOut = (year, min, max) => (min !== '' && year < Number(min.slice(0, 4))) || (max !== '' && year > Number(max.slice(0, 4)));
70
+ export const periodCells = ({ max, min, page, shown, today, view }) => {
71
+ const years = view === 'year';
72
+ const start = years ? yearPageOf(page) : page;
73
+ return Array.from({ length: 12 }, (_, index) => {
74
+ const at = years ? start + index : index;
75
+ const first = new Date(Date.UTC(years ? at : start, years ? 0 : index, 1));
76
+ return {
77
+ at,
78
+ chosen: years ? at === shown.getUTCFullYear() : monthOf(first) === monthOf(shown),
79
+ current: years ? String(at) === today.slice(0, 4) : monthOf(first) === today.slice(0, 7),
80
+ first,
81
+ out: years ? yearOut(at, min, max) : monthOut(first, min, max),
82
+ };
83
+ });
84
+ };
85
+ export const periodKey = (view, stop, year, key, along) => {
86
+ const index = view === 'year' ? stop - yearPageOf(stop) : stop;
87
+ const delta = periodMove(key, index, along);
88
+ if (delta === null) {
89
+ return null;
90
+ }
91
+ if (view === 'year') {
92
+ return { focus: stop + delta, pageYear: stop + delta };
93
+ }
94
+ const months = year * 12 + stop + delta;
95
+ return { focus: ((months % 12) + 12) % 12, pageYear: Math.floor(months / 12) };
96
+ };
@@ -0,0 +1,43 @@
1
+ /** The keys of a chord, in the order written, with spaces and empty entries dropped. */
2
+ export declare const chordParts: (keys: string) => string[];
3
+ /**
4
+ * A chord as `aria-keyshortcuts` writes it: `mod+k` is `Meta+K` on an Apple
5
+ * keyboard and `Control+K` elsewhere. A letter is written in capitals and a
6
+ * symbol as itself. Empty for no keys.
7
+ */
8
+ export declare const ariaChord: (keys: string, apple: boolean) => string;
9
+ /**
10
+ * Whether a chord types a character, and so is indistinguishable from typing.
11
+ *
12
+ * One code point means a character rather than a name: `/` and `?` are
13
+ * printable, `escape` and `enter` are not. **Shift is allowed beside it**,
14
+ * because Shift is how a character is typed: `shift+?` and `shift+a` are a
15
+ * question mark and a capital A in a field.
16
+ */
17
+ export declare const printableChord: (keys: string) => boolean;
18
+ /** What a key press carries that a chord is compared against. */
19
+ export interface ChordPress {
20
+ altKey: boolean;
21
+ ctrlKey: boolean;
22
+ key: string;
23
+ metaKey: boolean;
24
+ shiftKey: boolean;
25
+ }
26
+ /**
27
+ * Whether a press is the chord, modifiers and all.
28
+ *
29
+ * `mod` is one key, the platform's: Command where `apple` and Control
30
+ * elsewhere. The other one is then checked like any modifier the chord does
31
+ * not name, so Control and Command together do not answer `mod+k`. `cmd` and
32
+ * `meta` are Command, `alt` and `opt` are Option.
33
+ *
34
+ * **A symbol is typed with Shift, so Shift says nothing about it.** On a US
35
+ * layout `?` arrives as `?` with `shiftKey` set, and a chord of `?` that
36
+ * refused Shift could never be pressed at all. A letter has a shifted form of
37
+ * its own, so `a` still refuses Shift and `shift+a` still asks for it.
38
+ *
39
+ * Every other modifier is checked in both directions, so `mod+k` is not
40
+ * answered by `shift+mod+k`: a chord a reader did not press is a chord that
41
+ * should go to whatever did want it.
42
+ */
43
+ export declare const matchesChord: (press: ChordPress, keys: string, apple: boolean) => boolean;