@fusion.dev/helix-headless 0.2.0 → 0.3.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/custom-elements.json +234 -234
- package/dist/accordion/item/accordion-item.js +0 -108
- package/dist/accordion/root/accordion-root.js +0 -33
- package/dist/announcement/root/announcement-root.js +1 -76
- package/dist/app-header/root/app-header-root.js +0 -84
- package/dist/avatar/group/avatar-group.js +0 -77
- package/dist/avatar/root/avatar-root.js +0 -72
- package/dist/badge/root/badge-root.js +0 -67
- package/dist/bar-chart/root/bar-chart-root.js +3 -263
- package/dist/blockquote/root/blockquote-root.js +0 -46
- package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
- package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
- package/dist/button/root/button-root.js +0 -215
- package/dist/button-group/root/button-group-root.js +0 -30
- package/dist/callout/root/callout-root.js +0 -52
- package/dist/card/root/card-root.js +0 -41
- package/dist/carousel/root/carousel-root.d.ts +2 -2
- package/dist/carousel/root/carousel-root.js +2 -362
- package/dist/chat/composer/chat-composer.js +0 -131
- package/dist/chat/message/chat-message.js +0 -137
- package/dist/chat/root/chat-root.js +0 -193
- package/dist/checkbox/root/checkbox-root.js +0 -113
- package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
- package/dist/code/root/code-root.js +0 -26
- package/dist/code-block/root/code-block-root.js +0 -100
- package/dist/color-input/root/color-input-root.js +0 -217
- package/dist/color-picker/color.js +0 -69
- package/dist/color-picker/root/color-picker-root.js +0 -272
- package/dist/combobox/root/combobox-root.js +0 -428
- package/dist/command/root/command-root.js +0 -177
- package/dist/copy-button/root/copy-button-root.js +0 -134
- package/dist/data-table/root/data-table-root.js +0 -336
- package/dist/data-table/root/order.js +0 -100
- package/dist/date-input/root/date-input-root.js +1 -398
- package/dist/date-picker/root/date-picker-root.js +0 -358
- package/dist/dialog/root/dialog-root.js +0 -189
- package/dist/divider/root/divider-root.js +0 -71
- package/dist/donut-chart/root/donut-chart-root.js +0 -214
- package/dist/drawer/root/drawer-root.js +0 -116
- package/dist/drilldown/item/drilldown-item.js +0 -82
- package/dist/drilldown/root/drilldown-root.js +0 -91
- package/dist/element-events.generated.js +0 -5
- package/dist/empty/root/empty-root.js +0 -35
- package/dist/file-input/root/file-input-root.js +0 -232
- package/dist/format-date/root/format-date-root.js +0 -95
- package/dist/heatmap/root/buckets.js +0 -57
- package/dist/heatmap/root/heatmap-root.js +0 -313
- package/dist/icon/root/icon-root.js +0 -48
- package/dist/icon-button/root/icon-button-root.js +0 -30
- package/dist/image/root/image-root.js +0 -174
- package/dist/index.js +0 -9
- package/dist/input/root/input-root.js +0 -228
- package/dist/kbd/root/kbd-root.js +0 -117
- package/dist/legend/root/legend-root.js +0 -73
- package/dist/line-chart/root/line-chart-root.js +0 -172
- package/dist/list/item/list-item.js +0 -92
- package/dist/list/root/list-root.js +0 -26
- package/dist/listbox/root/listbox-root.js +2 -429
- package/dist/loader/root/loader-root.js +0 -40
- package/dist/megamenu/root/megamenu-root.js +0 -284
- package/dist/menu/item/menu-item.js +1 -221
- package/dist/menu/root/menu-root.js +1 -171
- package/dist/meter/root/meter-root.js +1 -152
- package/dist/navigation/root/navigation-root.js +0 -56
- package/dist/notifications/notification/notification.js +0 -159
- package/dist/notifications/root/notifications-root.js +0 -148
- package/dist/number-input/root/number-input-root.js +1 -263
- package/dist/option/group/option-group.js +0 -26
- package/dist/option/root/option-root.js +0 -157
- package/dist/outline/root/outline-root.js +0 -292
- package/dist/pagination/root/pagination-root.js +0 -361
- package/dist/pagination/size/pagination-size.js +0 -104
- package/dist/pagination/summary/pagination-summary.js +0 -90
- package/dist/pin-input/root/pin-input-root.js +0 -276
- package/dist/popover/root/popover-root.js +0 -103
- package/dist/progress/root/progress-root.js +1 -133
- package/dist/qr-code/root/qr-code-root.js +0 -96
- package/dist/quote/root/quote-root.js +0 -25
- package/dist/radio/group/radio-group.js +0 -81
- package/dist/radio/root/radio-root.js +0 -85
- package/dist/rating/root/rating-root.js +0 -220
- package/dist/scroll-area/root/scroll-area-root.js +0 -100
- package/dist/select/root/select-root.js +2 -218
- package/dist/skeleton/root/skeleton-root.js +0 -20
- package/dist/slider/root/slider-root.js +1 -267
- package/dist/sparkline/root/sparkline-root.js +3 -164
- package/dist/speed-dial/item/speed-dial-item.js +0 -52
- package/dist/speed-dial/root/speed-dial-root.js +1 -116
- package/dist/splitter/root/splitter-root.js +0 -489
- package/dist/stat/group/stat-group.js +0 -18
- package/dist/stat/root/stat-root.js +0 -23
- package/dist/steps/root/steps-root.js +0 -62
- package/dist/steps/step/step.js +0 -131
- package/dist/switch/root/switch-root.js +0 -80
- package/dist/table/cell/table-cell.js +0 -23
- package/dist/table/column/table-column.js +0 -158
- package/dist/table/root/table-root.js +0 -82
- package/dist/table/row/table-row.js +0 -21
- package/dist/tabs/panel/tab-panel.js +0 -49
- package/dist/tabs/root/tabs-root.js +0 -98
- package/dist/tabs/tab/tab.js +0 -59
- package/dist/tag/root/tag-root.js +0 -31
- package/dist/textarea/root/textarea-root.js +0 -162
- package/dist/time-input/root/time-input-root.js +0 -283
- package/dist/time-picker/root/time-picker-root.js +0 -326
- package/dist/timeline/item/timeline-item.js +0 -66
- package/dist/timeline/root/timeline-root.js +0 -41
- package/dist/toast/item/toast-item.js +0 -165
- package/dist/toast/root/toast-root.js +0 -51
- package/dist/toggle/group/toggle-group.js +0 -150
- package/dist/toggle/root/toggle-root.js +0 -78
- package/dist/toolbar/root/toolbar-root.js +0 -116
- package/dist/tooltip/root/tooltip-root.js +0 -427
- package/dist/tour/root/tour-root.js +0 -130
- package/dist/tour/stop/tour-stop.js +1 -127
- package/dist/tree/item/tree-item.js +0 -198
- package/dist/tree/root/tree-root.js +0 -97
- package/dist/trend/root/trend-root.js +0 -53
- package/dist/video/root/video-root.d.ts +8 -12
- package/dist/video/root/video-root.js +5 -756
- package/dist/video-playlist/item/video-playlist-item.js +1 -90
- package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
- package/dist/video-playlist/root/video-playlist-root.js +0 -113
- package/dist/virtual-list/root/virtual-list-root.js +0 -105
- package/jsx.d.ts +1408 -2576
- package/package.json +6 -6
- package/vue.d.ts +1 -1
|
@@ -10,19 +10,6 @@ import { nothing } from 'lit';
|
|
|
10
10
|
import { property, state } from 'lit/decorators.js';
|
|
11
11
|
import { html } from 'lit/static-html.js';
|
|
12
12
|
import '../../tag/index.js';
|
|
13
|
-
/**
|
|
14
|
-
* A file's size, at the largest unit that leaves a number worth reading.
|
|
15
|
-
*
|
|
16
|
-
* Binary steps, because that is what a file system reports and what the number
|
|
17
|
-
* beside it in a finder will say. One decimal place from kilobytes up and none
|
|
18
|
-
* for bytes: `1.2 MB` is the reading, `1.23 MB` is noise, and `1.5 bytes` is not
|
|
19
|
-
* a thing.
|
|
20
|
-
*
|
|
21
|
-
* `Intl.NumberFormat` for the number rather than `toFixed`, so a decimal comma
|
|
22
|
-
* lands where a decimal comma belongs. The unit is not localized with it: `MB`
|
|
23
|
-
* is written the same way in the places this ships, and `Intl`'s own byte units
|
|
24
|
-
* carry no scale to pick from.
|
|
25
|
-
*/
|
|
26
13
|
const size = (bytes, locale) => {
|
|
27
14
|
const units = ['bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
28
15
|
let value = bytes;
|
|
@@ -34,19 +21,9 @@ const size = (bytes, locale) => {
|
|
|
34
21
|
const written = new Intl.NumberFormat(locale || undefined, {
|
|
35
22
|
maximumFractionDigits: step === 0 ? 0 : 1,
|
|
36
23
|
}).format(value);
|
|
37
|
-
// One byte is a byte. The plural is only wrong at exactly one and it is the
|
|
38
|
-
// size an empty-ish file has, so it is the one a reader sees while testing.
|
|
39
24
|
const unit = step === 0 && value === 1 ? 'byte' : units[step];
|
|
40
25
|
return `${written} ${unit}`;
|
|
41
26
|
};
|
|
42
|
-
/**
|
|
43
|
-
* What kind of file it is, in the shortest honest form.
|
|
44
|
-
*
|
|
45
|
-
* The subtype of the MIME type, so `image/png` reads `PNG`, which is the word a
|
|
46
|
-
* reader already has for it. A browser leaves `type` empty for anything it does
|
|
47
|
-
* not recognize, so the extension is the fallback: it is what the name already
|
|
48
|
-
* shows, which makes it a poor label and a better one than nothing.
|
|
49
|
-
*/
|
|
50
27
|
const kind = (file) => {
|
|
51
28
|
const subtype = file.type.split('/')[1];
|
|
52
29
|
if (subtype) {
|
|
@@ -55,131 +32,22 @@ const kind = (file) => {
|
|
|
55
32
|
const extension = file.name.includes('.') ? file.name.split('.').pop() : '';
|
|
56
33
|
return extension ? extension.toUpperCase() : 'File';
|
|
57
34
|
};
|
|
58
|
-
/**
|
|
59
|
-
* @summary A control for choosing files, by pointer or by dropping them on it.
|
|
60
|
-
*
|
|
61
|
-
* @description
|
|
62
|
-
* **A real `<input type="file">`, kept and made invisible rather than
|
|
63
|
-
* replaced.** It is the only way to open a file picker: the dialog is a
|
|
64
|
-
* privileged action a script cannot start on its own, and it opens for a click
|
|
65
|
-
* on the input or on a `<label>` pointing at it. A component that draws its own
|
|
66
|
-
* button ends up calling `.click()` on a hidden input anyway, and then has two
|
|
67
|
-
* things a screen reader can find where there is one control.
|
|
68
|
-
*
|
|
69
|
-
* So the input stays, holds the label, the name, the validity and the focus,
|
|
70
|
-
* and the visible trigger is a `<label for>` rather than a button. A label is
|
|
71
|
-
* not focusable and does not need to be: the input is, and Enter and Space on it
|
|
72
|
-
* open the picker because that is what the platform does with a file input.
|
|
73
|
-
*
|
|
74
|
-
* **Dropping is why this is a component rather than an attribute.** A bare file
|
|
75
|
-
* input accepts a drop only on the input itself, which is invisible here, so the
|
|
76
|
-
* field is the drop target and it says when something is over it.
|
|
77
|
-
*
|
|
78
|
-
* **Files are not a value a consumer can hand back.** Every other control here
|
|
79
|
-
* takes the controlled and uncontrolled pair, and this one does not: a `FileList`
|
|
80
|
-
* cannot be constructed from markup, and browsers refuse a consumer writing
|
|
81
|
-
* `value` on a file input outright, because a page that could name a file could
|
|
82
|
-
* read it. So this element owns what was chosen, announces it, and offers
|
|
83
|
-
* `clear()` for the one change a consumer can legitimately ask for.
|
|
84
|
-
*
|
|
85
|
-
* **The list under it is the control's own, not decoration.** A file input with
|
|
86
|
-
* files chosen reports "3 files" and nothing else, which is unusable for
|
|
87
|
-
* checking that the right ones were picked. The names are rendered, and each
|
|
88
|
-
* carries the way to take it back out.
|
|
89
|
-
*
|
|
90
|
-
* @since 0.1.0
|
|
91
|
-
* @status stable
|
|
92
|
-
* @category forms
|
|
93
|
-
* @aliases FileUpload, FilePicker, Dropzone, InputFile
|
|
94
|
-
* @documentation https://helix.fusion.dev/components/file-input
|
|
95
|
-
*
|
|
96
|
-
* @slot feedback - What is wrong with it.
|
|
97
|
-
* @slot hint - Help under the field.
|
|
98
|
-
* @slot trigger - What the reader presses to open the picker.
|
|
99
|
-
*
|
|
100
|
-
* @event {Event} change - Announced once the value has moved.
|
|
101
|
-
* @event {HelixBeforeChangeEvent} helix-before-change - Fired before the chosen files change, with their names. Cancelable: refusing it takes the decision and nothing moves.
|
|
102
|
-
* @event {Event} input - Announced once the value has moved.
|
|
103
|
-
*
|
|
104
|
-
* @csspart control - The file input itself, which carries the label and validity.
|
|
105
|
-
* @csspart detail - Its name and its reading, stacked.
|
|
106
|
-
* @csspart feedback - What is wrong with it.
|
|
107
|
-
* @csspart field - The box that is pressed and dropped on.
|
|
108
|
-
* @csspart file - One chosen file.
|
|
109
|
-
* @csspart hint - Help under the field, while nothing is wrong.
|
|
110
|
-
* @csspart kind - What kind of file it is.
|
|
111
|
-
* @csspart kind-dismiss - The remove button, when `dismissible`.
|
|
112
|
-
* @csspart kind-dismiss-mark - The glyph on the dismiss control, where none is slotted.
|
|
113
|
-
* @csspart kind-prefix - The category before the label.
|
|
114
|
-
* @csspart label - The question, over the field.
|
|
115
|
-
* @csspart list - The chosen files.
|
|
116
|
-
* @csspart meta - The row holding its size and kind.
|
|
117
|
-
* @csspart name - Its name.
|
|
118
|
-
* @csspart remove - The control that takes it back out.
|
|
119
|
-
* @csspart size - How big it is.
|
|
120
|
-
* @csspart size-dismiss - The remove button, when `dismissible`.
|
|
121
|
-
* @csspart size-dismiss-mark - The glyph on the dismiss control, where none is slotted.
|
|
122
|
-
* @csspart size-prefix - The category before the label.
|
|
123
|
-
* @csspart trigger - The label that opens the picker.
|
|
124
|
-
*/
|
|
125
35
|
let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(HelixFormElement) {
|
|
126
36
|
constructor() {
|
|
127
37
|
super(...arguments);
|
|
128
|
-
/* A consumer's `aria-describedby` names ids in their own tree and the control
|
|
129
|
-
is in this shadow root, so the reference has to be carried down as an
|
|
130
|
-
element rather than copied as an id. See `AriaForwardController`: measured
|
|
131
|
-
in Chrome's own tree, the host held the description and the control the
|
|
132
|
-
reader is actually on was exposed without it. */
|
|
133
38
|
this.aria = new AriaForwardController(this, {
|
|
134
39
|
target: () => this.renderRoot?.querySelector('[part="control"]') ?? null,
|
|
135
40
|
});
|
|
136
|
-
/**
|
|
137
|
-
* Which files the picker offers, as a native `accept` list.
|
|
138
|
-
*/
|
|
139
41
|
this.accept = '';
|
|
140
|
-
/**
|
|
141
|
-
* Language tag the sizes are written in.
|
|
142
|
-
*
|
|
143
|
-
* Absent uses the runtime's own.
|
|
144
|
-
*/
|
|
145
42
|
this.locale = '';
|
|
146
|
-
/**
|
|
147
|
-
* Whether more than one file can be chosen.
|
|
148
|
-
*/
|
|
149
43
|
this.multiple = false;
|
|
150
|
-
/**
|
|
151
|
-
* What the control removing one file is called, per file.
|
|
152
|
-
*/
|
|
153
44
|
this.removeLabel = 'Remove';
|
|
154
|
-
/**
|
|
155
|
-
* What the trigger reads, where nothing is slotted into it.
|
|
156
|
-
*/
|
|
157
45
|
this.triggerLabel = 'Choose files';
|
|
158
|
-
/**
|
|
159
|
-
* Whether something is being dragged over the field.
|
|
160
|
-
*
|
|
161
|
-
* State rather than a property: it is a fact about a gesture in progress and
|
|
162
|
-
* nothing a consumer sets. It reaches the stylesheet as `data-dropping` on an
|
|
163
|
-
* element in this shadow root, never on the host, which is markup a consumer
|
|
164
|
-
* rendered.
|
|
165
|
-
*/
|
|
166
46
|
this.dropping = false;
|
|
167
47
|
this.handleChange = (event) => {
|
|
168
|
-
// The native `change` is relayed under this element's own name, so a
|
|
169
|
-
// consumer hears one event from one element rather than two from two.
|
|
170
48
|
event.stopPropagation();
|
|
171
49
|
this.announce();
|
|
172
50
|
};
|
|
173
|
-
/*
|
|
174
|
-
* Takes a drop, which a bare file input can only do on itself.
|
|
175
|
-
*
|
|
176
|
-
* The input is invisible here, so the field is the target and the files are
|
|
177
|
-
* put into the input afterwards: that keeps one source of truth, and it is
|
|
178
|
-
* what makes a dropped file and a picked file indistinguishable to a form.
|
|
179
|
-
*
|
|
180
|
-
* `preventDefault` on dragover as well as on drop, because without it the
|
|
181
|
-
* browser navigates to the file and the page is gone.
|
|
182
|
-
*/
|
|
183
51
|
this.handleDragOver = (event) => {
|
|
184
52
|
if (this.isDisabled) {
|
|
185
53
|
return;
|
|
@@ -202,8 +70,6 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
|
|
|
202
70
|
return;
|
|
203
71
|
}
|
|
204
72
|
const keeping = new DataTransfer();
|
|
205
|
-
// A single-file control takes the last thing dropped, the way choosing
|
|
206
|
-
// again in the picker replaces rather than adds.
|
|
207
73
|
for (const file of this.multiple ? [...this.files, ...dropped] : dropped.slice(-1)) {
|
|
208
74
|
keeping.items.add(file);
|
|
209
75
|
}
|
|
@@ -217,38 +83,15 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
|
|
|
217
83
|
}
|
|
218
84
|
};
|
|
219
85
|
}
|
|
220
|
-
/**
|
|
221
|
-
* The files chosen, which is the element's own and never assigned.
|
|
222
|
-
*
|
|
223
|
-
* Read from the input rather than mirrored into a field. The input is what
|
|
224
|
-
* the picker filled and what a form submits, so a copy here could only ever
|
|
225
|
-
* disagree with it.
|
|
226
|
-
*/
|
|
227
86
|
get files() {
|
|
228
87
|
return [...(this.control?.files ?? [])];
|
|
229
88
|
}
|
|
230
|
-
/**
|
|
231
|
-
* The input, or nothing where there is no tree to ask.
|
|
232
|
-
*
|
|
233
|
-
* The guard is for the server, whose element shim carries attributes but not
|
|
234
|
-
* traversal: `render` reads `files`, `files` reads this, and an unguarded
|
|
235
|
-
* `querySelector` threw before a single character of markup came out. Nothing
|
|
236
|
-
* is lost by answering `null` there, because a file only ever arrives through
|
|
237
|
-
* a reader and a server has never met one.
|
|
238
|
-
*/
|
|
239
89
|
get control() {
|
|
240
90
|
if (typeof this.renderRoot?.querySelector !== 'function') {
|
|
241
91
|
return null;
|
|
242
92
|
}
|
|
243
93
|
return this.renderRoot.querySelector('[part="control"]');
|
|
244
94
|
}
|
|
245
|
-
/**
|
|
246
|
-
* Takes every file back out.
|
|
247
|
-
*
|
|
248
|
-
* The one change a consumer can ask for. Setting a `FileList` is refused by
|
|
249
|
-
* every browser for a reason worth keeping, but emptying one is not: nothing
|
|
250
|
-
* about the page learns anything it did not already have.
|
|
251
|
-
*/
|
|
252
95
|
clear() {
|
|
253
96
|
const control = this.control;
|
|
254
97
|
if (!control) {
|
|
@@ -257,17 +100,6 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
|
|
|
257
100
|
control.value = '';
|
|
258
101
|
this.announce();
|
|
259
102
|
}
|
|
260
|
-
/**
|
|
261
|
-
* Drops one file, by index.
|
|
262
|
-
*
|
|
263
|
-
* `removeAt` rather than `remove`, which is `Element`'s own and takes no
|
|
264
|
-
* arguments: shadowing it would break every caller who meant the node.
|
|
265
|
-
*
|
|
266
|
-
* Through `DataTransfer`, which is the only way to build a `FileList`: the
|
|
267
|
-
* type has no constructor and `input.files` takes nothing else. It is the same
|
|
268
|
-
* mechanism a drop uses, so a file put in by dragging and one put in by the
|
|
269
|
-
* picker come back out the same way.
|
|
270
|
-
*/
|
|
271
103
|
removeAt(at) {
|
|
272
104
|
const control = this.control;
|
|
273
105
|
if (!control || typeof DataTransfer === 'undefined') {
|
|
@@ -282,32 +114,15 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
|
|
|
282
114
|
control.files = keeping.files;
|
|
283
115
|
this.announce();
|
|
284
116
|
}
|
|
285
|
-
/** Says what is now chosen, by name, and puts it in the form. */
|
|
286
117
|
announce() {
|
|
287
118
|
this.sync();
|
|
288
119
|
this.requestUpdate();
|
|
289
120
|
this.dispatchEvent(new HelixBeforeChangeEvent({
|
|
290
121
|
value: this.files.map((file) => file.name).join(', '),
|
|
291
122
|
}));
|
|
292
|
-
/* And under the platform's own names, which is what `onChange` and `onInput`
|
|
293
|
-
bind and what a native file input sends when the chosen files move: both,
|
|
294
|
-
in that order, the way every other control here does.
|
|
295
|
-
|
|
296
|
-
Without the explicit `input`, the only one a consumer receives is the
|
|
297
|
-
inner `<input type="file">`'s own, composed out of this shadow root, so
|
|
298
|
-
the documented event does not exist and an undocumented one arrives in
|
|
299
|
-
its place. */
|
|
300
123
|
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
|
301
124
|
this.dispatchEvent(new Event('change', { bubbles: true, composed: false }));
|
|
302
125
|
}
|
|
303
|
-
/**
|
|
304
|
-
* Reports the files and the validity the host stands behind.
|
|
305
|
-
*
|
|
306
|
-
* `FormData` rather than a string, because a file input submits the file
|
|
307
|
-
* itself and several of them under one name. The anchor on `setValidity` is
|
|
308
|
-
* the input, so the browser's own bubble points at something a reader can see
|
|
309
|
-
* rather than at a host with no text in it.
|
|
310
|
-
*/
|
|
311
126
|
sync() {
|
|
312
127
|
const data = new FormData();
|
|
313
128
|
if (this.name) {
|
|
@@ -315,18 +130,6 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
|
|
|
315
130
|
data.append(this.name, file);
|
|
316
131
|
}
|
|
317
132
|
}
|
|
318
|
-
/* **An empty one contributes no entry, and the native control contributes an
|
|
319
|
-
empty file.** Measured, one form holding both: `<input type="file"
|
|
320
|
-
name="nf">` with nothing chosen submits `nf=` carrying a `File` of zero
|
|
321
|
-
bytes and no name, and this submits nothing at all.
|
|
322
|
-
|
|
323
|
-
Kept, as a decision rather than an omission. A phantom entry naming a file
|
|
324
|
-
that does not exist is a thing every server reading this form has to know
|
|
325
|
-
to discard, and the reason the platform sends one is compatibility with
|
|
326
|
-
form encodings that predate `FormData`. `value` is excused on this family
|
|
327
|
-
already, for the neighboring reason that the native one is
|
|
328
|
-
`C:\fakepath\name`; this is the same component declining to reproduce a
|
|
329
|
-
lie, one level down. */
|
|
330
133
|
this.internals.setFormValue(data);
|
|
331
134
|
const control = this.control;
|
|
332
135
|
if (!control) {
|
|
@@ -337,52 +140,17 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
|
|
|
337
140
|
}
|
|
338
141
|
firstUpdated(changed) {
|
|
339
142
|
super.firstUpdated(changed);
|
|
340
|
-
// Whatever the markup arrived with, so a form knows before anything is
|
|
341
|
-
// touched. `willUpdate` is too early: there is no control to read yet.
|
|
342
143
|
this.sync();
|
|
343
144
|
}
|
|
344
|
-
/** Puts focus on the input, the way a label pointing at one would. */
|
|
345
145
|
focus(options) {
|
|
346
146
|
this.control?.focus(options);
|
|
347
147
|
}
|
|
348
|
-
/** Empties it, which is what a form reset means for a file input. */
|
|
349
148
|
formResetCallback() {
|
|
350
149
|
if (this.control) {
|
|
351
150
|
restoreValue(this.control, '');
|
|
352
151
|
}
|
|
353
152
|
this.announce();
|
|
354
153
|
}
|
|
355
|
-
/*
|
|
356
|
-
* The hint goes under the field, and the list after both.
|
|
357
|
-
*
|
|
358
|
-
* A hint is instruction for the next action rather than a note about the
|
|
359
|
-
* result: "up to 10MB each" is a rule for what may be added, and a rule read
|
|
360
|
-
* after the outcome is a rule read too late. Its position also has to be
|
|
361
|
-
* stable, and under the list it travels down the page as files are added, a
|
|
362
|
-
* card's height at a time. Every other field here puts it immediately under
|
|
363
|
-
* the control.
|
|
364
|
-
*
|
|
365
|
-
* Feedback stands in place of the hint rather than beside it, so wherever the
|
|
366
|
-
* hint is, an error is. An error about a file that was refused belongs
|
|
367
|
-
* against the control that refused it, not below a list of the ones that were
|
|
368
|
-
* not.
|
|
369
|
-
*/
|
|
370
|
-
/**
|
|
371
|
-
* What is known about a chosen file, as tags, behind a seam the styled tier
|
|
372
|
-
* can move.
|
|
373
|
-
*
|
|
374
|
-
* One method for both the size and the kind, because they are the same
|
|
375
|
-
* element saying two things and two methods would be two places to keep in
|
|
376
|
-
* step. Both parts are written out rather than passed in, and that is not
|
|
377
|
-
* style: a part name behind an interpolation is a part the manifest cannot
|
|
378
|
-
* see, so a stylesheet targeting it matches nothing.
|
|
379
|
-
*
|
|
380
|
-
* A method rather than a fragment inline, so a tier above can render its own,
|
|
381
|
-
* wrap this one, or leave it out without reimplementing the whole template.
|
|
382
|
-
* The tag is interpolated rather than written, so `tier` resolves it: this
|
|
383
|
-
* renders the headless element, and a styled subclass that sets `tier`
|
|
384
|
-
* renders the styled one from the same template.
|
|
385
|
-
*/
|
|
386
154
|
renderMeta(file) {
|
|
387
155
|
return html `<${this.tag('tag')} part="size"
|
|
388
156
|
exportparts="dismiss: size-dismiss, dismiss-mark: size-dismiss-mark, prefix: size-prefix">${size(file.size, this.locale)}</${this.tag('tag')}>
|
|
@@ -7,93 +7,17 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { customElement, elapsedTime, HelixElement, subscribe } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html, nothing } from 'lit';
|
|
9
9
|
import { property, state } from 'lit/decorators.js';
|
|
10
|
-
/**
|
|
11
|
-
* @summary A timestamp, written the way the reader's locale writes one.
|
|
12
|
-
*
|
|
13
|
-
* @description
|
|
14
|
-
* A date in a page is almost never wanted as an ISO string, and formatting one
|
|
15
|
-
* in the consumer's own code means every consumer ships a copy of the same
|
|
16
|
-
* `Intl` call and a different set of options. This is that call, as an element,
|
|
17
|
-
* so a date is written where it is shown.
|
|
18
|
-
*
|
|
19
|
-
* **`elapsed` is the mode a feed wants, and it stops being relative on
|
|
20
|
-
* purpose.** "42m", "3h", "1d" is what a row of activity reads as, and past a
|
|
21
|
-
* week the compact form is a lie: `Intl.NumberFormat` narrow renders both
|
|
22
|
-
* `minute` and `month` as `m` in en-US, so "3m" is three minutes at the top of a
|
|
23
|
-
* feed and three months further down it. Past six days this shows a date, which
|
|
24
|
-
* is shorter to read and impossible to misread. `elapsedTime` in `helix/core`
|
|
25
|
-
* carries the measurement.
|
|
26
|
-
*
|
|
27
|
-
* **The reading refreshes itself, from one timer the page shares.** A relative
|
|
28
|
-
* timestamp is correct for as long as its unit lasts and wrong afterwards, and a
|
|
29
|
-
* page left open is exactly where that shows. Each element asks to be woken when
|
|
30
|
-
* its own reading expires rather than polling, and an element showing a date
|
|
31
|
-
* asks for nothing at all: fifty rows and fifty intervals is fifty wakeups a
|
|
32
|
-
* second where one will do, and a feed of last month's notifications should cost
|
|
33
|
-
* nothing to leave open.
|
|
34
|
-
*
|
|
35
|
-
* **`<time datetime>` whatever the text says**, so a screen reader, a copy and
|
|
36
|
-
* paste, and anything parsing the page all get the real instant rather than
|
|
37
|
-
* "yesterday".
|
|
38
|
-
*
|
|
39
|
-
* @since 0.1.0
|
|
40
|
-
* @status experimental
|
|
41
|
-
* @category typography
|
|
42
|
-
* @aliases RelativeTime, Timestamp, TimeAgo
|
|
43
|
-
* @documentation https://helix.fusion.dev/components/format-date
|
|
44
|
-
*
|
|
45
|
-
* @csspart time - The `<time>` element carrying the reading.
|
|
46
|
-
*/
|
|
47
10
|
let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement {
|
|
48
11
|
constructor() {
|
|
49
12
|
super(...arguments);
|
|
50
|
-
/**
|
|
51
|
-
* The timestamp, as anything `Date` parses.
|
|
52
|
-
*
|
|
53
|
-
* An ISO string is what an API sends and what `<time datetime>` takes, so it
|
|
54
|
-
* is the shape that needs no translation at either end. Left unset this
|
|
55
|
-
* renders nothing rather than the epoch, which is what parsing an empty string
|
|
56
|
-
* would give.
|
|
57
|
-
*
|
|
58
|
-
* **The element's own text is read as a fallback, and only in a browser.**
|
|
59
|
-
* `<helix-format-date elapsed>2026-08-27T01:00:00Z</helix-format-date>` is the
|
|
60
|
-
* spelling that reads best in markup, and it cannot be the only one: measured
|
|
61
|
-
* against Lit's server shim, `textContent`, `children`, `firstChild` and
|
|
62
|
-
* `querySelector` are all `undefined` there, so a server has no way to see it.
|
|
63
|
-
* An element rendered on a server needs the attribute; one rendered in a
|
|
64
|
-
* browser may use either.
|
|
65
|
-
*/
|
|
66
13
|
this.date = '';
|
|
67
|
-
/**
|
|
68
|
-
* Show how long ago it was, compactly, instead of the date.
|
|
69
|
-
*
|
|
70
|
-
* Reflected, because it is the one property that decides which of two readings
|
|
71
|
-
* this is and a stylesheet has a reason to know: a compact reading is a fixed
|
|
72
|
-
* few characters and a date is not.
|
|
73
|
-
*/
|
|
74
14
|
this.elapsed = false;
|
|
75
|
-
/**
|
|
76
|
-
* Locale for the reading.
|
|
77
|
-
*
|
|
78
|
-
* Empty is the runtime's own.
|
|
79
|
-
*/
|
|
80
15
|
this.locale = '';
|
|
81
|
-
/**
|
|
82
|
-
* The clock this element last drew against.
|
|
83
|
-
*
|
|
84
|
-
* A state field and not a read of `Date.now()` inside `render`, which would
|
|
85
|
-
* make the template a function of something Lit cannot see change: the element
|
|
86
|
-
* would draw the right text once and never notice it had gone stale.
|
|
87
|
-
*/
|
|
88
16
|
this.clock = undefined;
|
|
89
|
-
/**
|
|
90
|
-
* The date read out of the light DOM, where no attribute carried one.
|
|
91
|
-
*/
|
|
92
17
|
this.written = '';
|
|
93
18
|
this.stopTicking = undefined;
|
|
94
19
|
this.watching = undefined;
|
|
95
20
|
}
|
|
96
|
-
/** The timestamp, or nothing where there is not one to read. */
|
|
97
21
|
get moment() {
|
|
98
22
|
const source = this.date || this.written;
|
|
99
23
|
if (source === '') {
|
|
@@ -102,7 +26,6 @@ let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement
|
|
|
102
26
|
const parsed = new Date(source);
|
|
103
27
|
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
|
104
28
|
}
|
|
105
|
-
/** Every `Intl.DateTimeFormat` option a consumer set, and none they did not. */
|
|
106
29
|
get options() {
|
|
107
30
|
const asked = {};
|
|
108
31
|
if (this.day) {
|
|
@@ -137,20 +60,12 @@ let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement
|
|
|
137
60
|
connectedCallback() {
|
|
138
61
|
super.connectedCallback();
|
|
139
62
|
this.tick();
|
|
140
|
-
/* Rebuilt on every connection rather than built once, because
|
|
141
|
-
`disconnectedCallback` takes it down and a framework moves nodes for
|
|
142
|
-
ordinary reasons: built in `firstUpdated` alone this comes back
|
|
143
|
-
permanently dead the first time a list is reordered. Guarded on
|
|
144
|
-
`hasUpdated` because the first connection happens before there is anything
|
|
145
|
-
to watch. */
|
|
146
63
|
if (this.hasUpdated) {
|
|
147
64
|
this.watch();
|
|
148
65
|
}
|
|
149
66
|
}
|
|
150
67
|
disconnectedCallback() {
|
|
151
68
|
super.disconnectedCallback();
|
|
152
|
-
/* Both outlive the element otherwise, and a page that mounts and unmounts a
|
|
153
|
-
feed leaks one of each every time it opens. */
|
|
154
69
|
this.stopTicking?.();
|
|
155
70
|
this.stopTicking = undefined;
|
|
156
71
|
this.watching?.disconnect();
|
|
@@ -167,7 +82,6 @@ let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement
|
|
|
167
82
|
this.tick();
|
|
168
83
|
}
|
|
169
84
|
}
|
|
170
|
-
/** The light DOM's own text, which is the other way to write the value. */
|
|
171
85
|
read() {
|
|
172
86
|
if (this.date !== '') {
|
|
173
87
|
return;
|
|
@@ -183,15 +97,6 @@ let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement
|
|
|
183
97
|
this.watching = new MutationObserver(() => this.read());
|
|
184
98
|
this.watching.observe(this, { characterData: true, childList: true, subtree: true });
|
|
185
99
|
}
|
|
186
|
-
/**
|
|
187
|
-
* Schedules the next reading, and only the next one.
|
|
188
|
-
*
|
|
189
|
-
* The formatter says when its own answer expires, so this asks for exactly
|
|
190
|
-
* that moment rather than polling: an item minutes old wakes once a minute, an
|
|
191
|
-
* item hours old once an hour, and an item showing a date never, which is the
|
|
192
|
-
* difference between a feed that costs nothing to leave open and one that
|
|
193
|
-
* does.
|
|
194
|
-
*/
|
|
195
100
|
tick() {
|
|
196
101
|
this.stopTicking?.();
|
|
197
102
|
this.stopTicking = undefined;
|
|
@@ -1,28 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Which tone a reading gets, and how many readings fit.
|
|
3
|
-
*
|
|
4
|
-
* Kept apart from the element because both answers are arithmetic and both are
|
|
5
|
-
* easy to get subtly wrong in ways a picture hides: a threshold comparison off
|
|
6
|
-
* by one boundary colors a handful of cells wrong out of ninety, and a fit
|
|
7
|
-
* calculation off by one drops a day silently. Neither is visible in a
|
|
8
|
-
* screenshot of a strip.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* The tone for one reading, from its own `data-tone` or from the thresholds.
|
|
12
|
-
*
|
|
13
|
-
* **The cell's own tone wins**, and that ordering is the whole reason both
|
|
14
|
-
* exist. A status page already knows a day was degraded; making it encode that
|
|
15
|
-
* as a number so a component can decode it again is work for nothing, and the
|
|
16
|
-
* encoding would have to agree with the thresholds or the colors would drift
|
|
17
|
-
* from the truth. A contribution graph has only counts and wants the buckets.
|
|
18
|
-
*
|
|
19
|
-
* Thresholds are read as lower bounds, in the order given, and the last one that
|
|
20
|
-
* a value reaches wins. `thresholds="0,1,3,6"` with `tones="muted,info,accent,success"`
|
|
21
|
-
* puts 0 in `muted`, 1 and 2 in `info`, 3 to 5 in `accent`, and 6 upward in
|
|
22
|
-
* `success`. A value below the first bound gets no tone rather than the first
|
|
23
|
-
* one: an empty cell in a contribution graph is a day with nothing in it, and
|
|
24
|
-
* painting it as the lowest bucket claims a reading that does not exist.
|
|
25
|
-
*/
|
|
26
1
|
export const toneOf = (reading, thresholds, tones) => {
|
|
27
2
|
if (reading.tone) {
|
|
28
3
|
return reading.tone;
|
|
@@ -38,48 +13,16 @@ export const toneOf = (reading, thresholds, tones) => {
|
|
|
38
13
|
}
|
|
39
14
|
return chosen;
|
|
40
15
|
};
|
|
41
|
-
/** A comma or space separated list of numbers, in the order written. */
|
|
42
16
|
export const numbers = (source) => source
|
|
43
17
|
.split(/[\s,]+/)
|
|
44
18
|
.filter((one) => one !== '')
|
|
45
19
|
.map(Number)
|
|
46
20
|
.filter((one) => Number.isFinite(one));
|
|
47
|
-
/** A comma or space separated list of names, in the order written. */
|
|
48
21
|
export const words = (source) => source.split(/[\s,]+/).filter((one) => one !== '');
|
|
49
|
-
/**
|
|
50
|
-
* How many readings fit across a given width, and which ones they are.
|
|
51
|
-
*
|
|
52
|
-
* **Ninety cells in 320 pixels is three and a half pixels each**, which is not a
|
|
53
|
-
* strip, it is a smear. So a narrow container shows fewer cells rather than
|
|
54
|
-
* thinner ones, and it shows the *most recent* ones, because a status strip that
|
|
55
|
-
* drops the newest days to keep the oldest is answering a question nobody asked.
|
|
56
|
-
*
|
|
57
|
-
* **The count it returns is what the summary has to be recomputed against.** A
|
|
58
|
-
* strip labeled "99.2% over 90 days" that is drawing 45 of them is telling the
|
|
59
|
-
* reader something false about the picture in front of them, and nothing about
|
|
60
|
-
* the picture says so. That is the one fault in this component that a screenshot
|
|
61
|
-
* cannot show, which is why the element reflects the number and announces it.
|
|
62
|
-
*
|
|
63
|
-
* **What crosses the width is a column, and `rows` is how many cells are in
|
|
64
|
-
* one.** Counting cells is the same number only while `rows` is 1. Divide by
|
|
65
|
-
* the wrong thing at `rows="7"` and a ten week graph comes back as fifty-nine
|
|
66
|
-
* readings in a 592px box: eleven days dropped to fit a strip that wanted 156
|
|
67
|
-
* pixels, and fifty-nine is not a multiple of seven, so every column after the
|
|
68
|
-
* cut is offset by a day and the day-of-week rows stop meaning anything. A
|
|
69
|
-
* calendar heatmap that silently rotates its own weekdays is worse than one
|
|
70
|
-
* that drops a week.
|
|
71
|
-
*
|
|
72
|
-
* The count is therefore always a whole number of columns, which is what keeps
|
|
73
|
-
* the tail slice aligned.
|
|
74
|
-
*/
|
|
75
22
|
export const fits = (total, width, minimum, gap, rows = 1) => {
|
|
76
23
|
if (!Number.isFinite(width) || width <= 0 || minimum <= 0) {
|
|
77
24
|
return total;
|
|
78
25
|
}
|
|
79
|
-
/* n columns need n widths and n-1 gaps, so solving for n gives the +gap on
|
|
80
|
-
both sides. Written as `width / (minimum + gap)` it is short by one column
|
|
81
|
-
at every size, which is the off-by-one that reads as "the strip has a
|
|
82
|
-
margin". */
|
|
83
26
|
const columns = Math.max(1, Math.floor((width + gap) / (minimum + gap)));
|
|
84
27
|
return Math.min(total, columns * Math.max(1, rows));
|
|
85
28
|
};
|