@slotted/styles 0.0.0-stage → 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +10 -2
- package/package.json +276 -3
- package/src/accordion/accordion.css +96 -0
- package/src/accordion/accordion.tokens.json +14 -0
- package/src/action-bar/action-bar.css +455 -0
- package/src/action-bar/action-bar.tokens.json +50 -0
- package/src/action-sheet/action-sheet.css +462 -0
- package/src/action-sheet/action-sheet.tokens.json +59 -0
- package/src/alert/alert.css +764 -0
- package/src/alert/alert.tokens.json +77 -0
- package/src/app-shell/app-shell.css +1899 -0
- package/src/app-shell/app-shell.tokens.json +88 -0
- package/src/aspect-ratio/aspect-ratio.css +131 -0
- package/src/aspect-ratio/aspect-ratio.tokens.json +1 -0
- package/src/attachment/attachment.css +906 -0
- package/src/attachment/attachment.tokens.json +85 -0
- package/src/avatar/avatar.css +598 -0
- package/src/avatar/avatar.tokens.json +64 -0
- package/src/avatar-group/avatar-group.css +268 -0
- package/src/avatar-group/avatar-group.tokens.json +16 -0
- package/src/badge/badge.css +462 -0
- package/src/badge/badge.tokens.json +58 -0
- package/src/breadcrumb/breadcrumb.css +441 -0
- package/src/breadcrumb/breadcrumb.tokens.json +50 -0
- package/src/button/button.css +754 -0
- package/src/button/button.tokens.json +72 -0
- package/src/button-group/button-group.css +455 -0
- package/src/button-group/button-group.tokens.json +11 -0
- package/src/calendar/calendar.css +846 -0
- package/src/calendar/calendar.tokens.json +67 -0
- package/src/card/card.css +602 -0
- package/src/card/card.tokens.json +68 -0
- package/src/carousel/carousel.css +860 -0
- package/src/carousel/carousel.tokens.json +62 -0
- package/src/cascader/cascader.css +1320 -0
- package/src/cascader/cascader.tokens.json +103 -0
- package/src/chart/chart.css +1473 -0
- package/src/chart/chart.tokens.json +107 -0
- package/src/checkbox/checkbox.css +1011 -0
- package/src/checkbox/checkbox.tokens.json +102 -0
- package/src/checkbox-group/checkbox-group.css +70 -0
- package/src/checkbox-group/checkbox-group.tokens.json +6 -0
- package/src/code-block/code-block.css +555 -0
- package/src/code-block/code-block.tokens.json +69 -0
- package/src/collapsible/collapsible.css +546 -0
- package/src/collapsible/collapsible.tokens.json +55 -0
- package/src/color-picker/color-picker.css +607 -0
- package/src/color-picker/color-picker.tokens.json +58 -0
- package/src/column-manager/column-manager.css +452 -0
- package/src/column-manager/column-manager.tokens.json +43 -0
- package/src/combobox/combobox.css +1048 -0
- package/src/combobox/combobox.tokens.json +112 -0
- package/src/command-palette/command-palette.css +1000 -0
- package/src/command-palette/command-palette.tokens.json +75 -0
- package/src/container/container.css +198 -0
- package/src/container/container.tokens.json +18 -0
- package/src/context-menu/context-menu.css +104 -0
- package/src/context-menu/context-menu.tokens.json +11 -0
- package/src/currency-field/currency-field.css +787 -0
- package/src/currency-field/currency-field.tokens.json +80 -0
- package/src/dashboard-grid/dashboard-grid.css +748 -0
- package/src/dashboard-grid/dashboard-grid.tokens.json +67 -0
- package/src/data-grid/data-grid.css +2484 -0
- package/src/data-grid/data-grid.tokens.json +100 -0
- package/src/data-list/data-list.css +455 -0
- package/src/data-list/data-list.tokens.json +35 -0
- package/src/date-field/date-field.css +560 -0
- package/src/date-field/date-field.tokens.json +80 -0
- package/src/date-picker/date-picker.css +420 -0
- package/src/date-picker/date-picker.tokens.json +49 -0
- package/src/date-range-picker/date-range-picker.css +983 -0
- package/src/date-range-picker/date-range-picker.tokens.json +109 -0
- package/src/datetime-field/datetime-field.css +850 -0
- package/src/datetime-field/datetime-field.tokens.json +91 -0
- package/src/datetime-picker/datetime-picker.css +843 -0
- package/src/datetime-picker/datetime-picker.tokens.json +78 -0
- package/src/datetime-range-picker/datetime-range-picker.css +1375 -0
- package/src/datetime-range-picker/datetime-range-picker.tokens.json +135 -0
- package/src/description-list/description-list.css +675 -0
- package/src/description-list/description-list.tokens.json +66 -0
- package/src/detail-panel/detail-panel.css +329 -0
- package/src/detail-panel/detail-panel.tokens.json +33 -0
- package/src/dialog/dialog.css +968 -0
- package/src/dialog/dialog.tokens.json +66 -0
- package/src/divider/divider.css +131 -0
- package/src/divider/divider.tokens.json +18 -0
- package/src/drawer/drawer.css +1303 -0
- package/src/drawer/drawer.tokens.json +75 -0
- package/src/entity-picker/entity-picker.css +1367 -0
- package/src/entity-picker/entity-picker.tokens.json +118 -0
- package/src/event-calendar/event-calendar.css +2148 -0
- package/src/event-calendar/event-calendar.tokens.json +87 -0
- package/src/field/field.css +707 -0
- package/src/field/field.tokens.json +58 -0
- package/src/field-group/field-group.css +226 -0
- package/src/field-group/field-group.tokens.json +33 -0
- package/src/filter-bar/filter-bar.css +1306 -0
- package/src/filter-bar/filter-bar.tokens.json +78 -0
- package/src/form/form.css +218 -0
- package/src/form/form.tokens.json +32 -0
- package/src/fullscreen/fullscreen.css +376 -0
- package/src/fullscreen/fullscreen.tokens.json +50 -0
- package/src/grid/grid.css +173 -0
- package/src/grid/grid.tokens.json +20 -0
- package/src/highlight/highlight.css +135 -0
- package/src/highlight/highlight.tokens.json +16 -0
- package/src/hover-card/hover-card.css +93 -0
- package/src/hover-card/hover-card.tokens.json +17 -0
- package/src/icon/icon.css +136 -0
- package/src/icon/icon.tokens.json +14 -0
- package/src/image/image.css +449 -0
- package/src/image/image.tokens.json +27 -0
- package/src/import-export-center/import-export-center.css +1175 -0
- package/src/import-export-center/import-export-center.tokens.json +84 -0
- package/src/inline/inline.css +149 -0
- package/src/inline/inline.tokens.json +14 -0
- package/src/inline-message/inline-message.css +396 -0
- package/src/inline-message/inline-message.tokens.json +19 -0
- package/src/kbd/kbd.css +490 -0
- package/src/kbd/kbd.tokens.json +40 -0
- package/src/link/link.css +237 -0
- package/src/link/link.tokens.json +25 -0
- package/src/list/list.css +240 -0
- package/src/list/list.tokens.json +42 -0
- package/src/list-item/list-item.css +937 -0
- package/src/list-item/list-item.tokens.json +114 -0
- package/src/listbox/listbox.css +674 -0
- package/src/listbox/listbox.tokens.json +72 -0
- package/src/loading-overlay/loading-overlay.css +730 -0
- package/src/loading-overlay/loading-overlay.tokens.json +48 -0
- package/src/markdown/markdown.css +168 -0
- package/src/markdown/markdown.tokens.json +18 -0
- package/src/menu/menu.css +817 -0
- package/src/menu/menu.tokens.json +78 -0
- package/src/menubar/menubar.css +170 -0
- package/src/menubar/menubar.tokens.json +32 -0
- package/src/message/message.css +1453 -0
- package/src/message/message.tokens.json +85 -0
- package/src/message-scroller/message-scroller.css +371 -0
- package/src/message-scroller/message-scroller.tokens.json +47 -0
- package/src/multi-select/multi-select.css +1427 -0
- package/src/multi-select/multi-select.tokens.json +114 -0
- package/src/navigation-menu/navigation-menu.css +1029 -0
- package/src/navigation-menu/navigation-menu.tokens.json +70 -0
- package/src/notification-center/notification-center.css +856 -0
- package/src/notification-center/notification-center.tokens.json +84 -0
- package/src/number-field/number-field.css +794 -0
- package/src/number-field/number-field.tokens.json +80 -0
- package/src/otp-input/otp-input.css +525 -0
- package/src/otp-input/otp-input.tokens.json +69 -0
- package/src/page/page.css +179 -0
- package/src/page/page.tokens.json +20 -0
- package/src/page-header/page-header.css +1181 -0
- package/src/page-header/page-header.tokens.json +71 -0
- package/src/pagination/pagination.css +500 -0
- package/src/pagination/pagination.tokens.json +56 -0
- package/src/popover/popover.css +526 -0
- package/src/popover/popover.tokens.json +60 -0
- package/src/progress-indicator/progress-indicator.css +1584 -0
- package/src/progress-indicator/progress-indicator.tokens.json +50 -0
- package/src/prompt-composer/prompt-composer.css +680 -0
- package/src/prompt-composer/prompt-composer.tokens.json +99 -0
- package/src/prose/prose.css +733 -0
- package/src/prose/prose.tokens.json +79 -0
- package/src/radio-group/radio-group.css +778 -0
- package/src/radio-group/radio-group.tokens.json +103 -0
- package/src/rating/rating.css +1071 -0
- package/src/rating/rating.tokens.json +88 -0
- package/src/result-state/result-state.css +980 -0
- package/src/result-state/result-state.tokens.json +63 -0
- package/src/runtime-properties.mjs +321 -0
- package/src/saved-views/saved-views.css +394 -0
- package/src/saved-views/saved-views.tokens.json +41 -0
- package/src/scroll-area/scroll-area.css +481 -0
- package/src/scroll-area/scroll-area.tokens.json +15 -0
- package/src/section/section.css +177 -0
- package/src/section/section.tokens.json +39 -0
- package/src/segmented-control/segmented-control.css +564 -0
- package/src/segmented-control/segmented-control.tokens.json +67 -0
- package/src/select/select.css +1593 -0
- package/src/select/select.tokens.json +110 -0
- package/src/side-navigation/side-navigation.css +1096 -0
- package/src/side-navigation/side-navigation.tokens.json +87 -0
- package/src/skeleton/skeleton.css +431 -0
- package/src/skeleton/skeleton.tokens.json +29 -0
- package/src/slider/slider.css +792 -0
- package/src/slider/slider.tokens.json +75 -0
- package/src/sparkline/sparkline.css +282 -0
- package/src/sparkline/sparkline.tokens.json +45 -0
- package/src/spinner/spinner.css +224 -0
- package/src/spinner/spinner.tokens.json +8 -0
- package/src/split-button/split-button.css +253 -0
- package/src/split-button/split-button.tokens.json +12 -0
- package/src/splitter/splitter.css +336 -0
- package/src/splitter/splitter.tokens.json +24 -0
- package/src/stack/stack.css +128 -0
- package/src/stack/stack.tokens.json +14 -0
- package/src/stat-card/stat-card.css +641 -0
- package/src/stat-card/stat-card.tokens.json +95 -0
- package/src/status-indicator/status-indicator.css +481 -0
- package/src/status-indicator/status-indicator.tokens.json +38 -0
- package/src/stepper/stepper.css +629 -0
- package/src/stepper/stepper.tokens.json +72 -0
- package/src/switch/switch.css +703 -0
- package/src/switch/switch.tokens.json +75 -0
- package/src/table/table.css +1482 -0
- package/src/table/table.tokens.json +69 -0
- package/src/table-of-contents/table-of-contents.css +533 -0
- package/src/table-of-contents/table-of-contents.tokens.json +53 -0
- package/src/tabs/tabs.css +998 -0
- package/src/tabs/tabs.tokens.json +82 -0
- package/src/tag/tag.css +768 -0
- package/src/tag/tag.tokens.json +85 -0
- package/src/tags-input/tags-input.css +757 -0
- package/src/tags-input/tags-input.tokens.json +84 -0
- package/src/text-field/text-field.css +900 -0
- package/src/text-field/text-field.tokens.json +87 -0
- package/src/textarea/textarea.css +365 -0
- package/src/textarea/textarea.tokens.json +59 -0
- package/src/time-field/time-field.css +719 -0
- package/src/time-field/time-field.tokens.json +86 -0
- package/src/time-picker/time-picker.css +665 -0
- package/src/time-picker/time-picker.tokens.json +66 -0
- package/src/time-range-picker/time-range-picker.css +1287 -0
- package/src/time-range-picker/time-range-picker.tokens.json +134 -0
- package/src/timeline/timeline.css +1596 -0
- package/src/timeline/timeline.tokens.json +96 -0
- package/src/toast/toast.css +943 -0
- package/src/toast/toast.tokens.json +90 -0
- package/src/toggle/toggle.css +290 -0
- package/src/toggle/toggle.tokens.json +54 -0
- package/src/toggle-group/toggle-group.css +463 -0
- package/src/toggle-group/toggle-group.tokens.json +54 -0
- package/src/toolbar/toolbar.css +434 -0
- package/src/toolbar/toolbar.tokens.json +49 -0
- package/src/tooltip/tooltip.css +368 -0
- package/src/tooltip/tooltip.tokens.json +39 -0
- package/src/top-navigation/top-navigation.css +1287 -0
- package/src/top-navigation/top-navigation.tokens.json +91 -0
- package/src/tour/tour.css +680 -0
- package/src/tour/tour.tokens.json +67 -0
- package/src/transfer/transfer.css +571 -0
- package/src/transfer/transfer.tokens.json +49 -0
- package/src/tree/tree.css +1000 -0
- package/src/tree/tree.tokens.json +80 -0
- package/src/typography/typography.css +211 -0
- package/src/typography/typography.tokens.json +31 -0
- package/src/upload/upload.css +1706 -0
- package/src/upload/upload.tokens.json +123 -0
- package/src/visually-hidden/visually-hidden.css +52 -0
- package/src/visually-hidden/visually-hidden.tokens.json +1 -0
- package/src/wizard/wizard.css +740 -0
- package/src/wizard/wizard.tokens.json +63 -0
|
@@ -0,0 +1,1706 @@
|
|
|
1
|
+
@layer slotted.components {
|
|
2
|
+
/**
|
|
3
|
+
* ── Files, and what happens to them ───────────────────────────────────
|
|
4
|
+
*
|
|
5
|
+
* One family for what `docs/component-features.md` §17 lists as three: a
|
|
6
|
+
* drop region, a file input, and the manager that shows what is being sent.
|
|
7
|
+
* `variant` decides which of the three shapes is drawn; everything below the
|
|
8
|
+
* region — the list, the rows, their bars and their controls — is the same
|
|
9
|
+
* in all three.
|
|
10
|
+
*
|
|
11
|
+
* **The trigger is the `<label>` and the region is not.** What the reader
|
|
12
|
+
* focuses is an `<input type="file">` clipped to one pixel inside the
|
|
13
|
+
* trigger, so the trigger is what draws the ring, through `:has()`. Putting
|
|
14
|
+
* the label around the trigger rather than around the whole region is what
|
|
15
|
+
* makes the input's accessible name three words instead of every word in the
|
|
16
|
+
* box; the reasoning and its cost are in
|
|
17
|
+
* `docs/history/designs/2026-09-10-upload-design.md` §2.
|
|
18
|
+
*
|
|
19
|
+
* **The family owns its own container.** Four of its layout decisions answer
|
|
20
|
+
* the width of the box rather than the width of the window — a row folds its
|
|
21
|
+
* size under its name, the region's mark and text stop sharing a line, the
|
|
22
|
+
* summary stacks, and a grid of previews changes its track count. A drop
|
|
23
|
+
* region goes in a sidebar, in a dialog and across a page in the same
|
|
24
|
+
* application, and none of those is a viewport.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* The private properties, on either box that stands on its own. The viewer is
|
|
28
|
+
* portalled to the end of the document — it must be, because the rule below
|
|
29
|
+
* makes this box a container and a container is a containing block for a
|
|
30
|
+
* fixed descendant — so it is not inside the family it belongs to, inherits
|
|
31
|
+
* none of these, and states the same set for itself. They are declared once,
|
|
32
|
+
* for both, rather than copied into a second block that drifts from the first.
|
|
33
|
+
*/
|
|
34
|
+
[data-ui='upload'][data-part='root'],
|
|
35
|
+
[data-ui='upload'][data-part='viewer'] {
|
|
36
|
+
/**
|
|
37
|
+
* Type, glyph, control height and inner gap come from the control scales,
|
|
38
|
+
* the only ones the theme restates per density, so an upload inside a dense
|
|
39
|
+
* form shrinks with the fields around it. The insets come from the space
|
|
40
|
+
* scale, which does not vary by density — the cost `card` and `alert` both
|
|
41
|
+
* accepted, and recorded there for the same reason.
|
|
42
|
+
*/
|
|
43
|
+
--_font-size: var(--slotted-control-font-size-md, 13px);
|
|
44
|
+
--_glyph-size: var(--slotted-control-icon-size-md, 16px);
|
|
45
|
+
--_inner-size: var(--slotted-control-inner-size-md, 24px);
|
|
46
|
+
--_control-height: var(--slotted-control-height-md, 34px);
|
|
47
|
+
--_control-inset: var(--slotted-control-padding-inline-md, 13px);
|
|
48
|
+
--_gap: var(--slotted-control-gap-md, 8px);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The square a row's preview stands in. Two glyphs wide, so it holds a
|
|
52
|
+
* thumbnail at a size worth looking at and still steps with the density
|
|
53
|
+
* that every other measure in the row steps with. A stated pixel size
|
|
54
|
+
* would have been a sixth scale for one box.
|
|
55
|
+
*/
|
|
56
|
+
--_preview-size: var(--slotted-upload-preview-size, calc(2 * var(--_glyph-size)));
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* How a control inside the family answers a pointer. It is a state layer
|
|
60
|
+
* rather than a step on the tone ramp, for the reason `tag` measured and
|
|
61
|
+
* `alert` restated: the tone's `subtle-hover` is calibrated against the
|
|
62
|
+
* page, and these controls sit on a row that may already be tinted by its
|
|
63
|
+
* own status. `currentcolor` is legible on whatever ground it has by
|
|
64
|
+
* construction — it is the colour the words beside it are read in.
|
|
65
|
+
*/
|
|
66
|
+
--_state-hover: color-mix(
|
|
67
|
+
in oklab,
|
|
68
|
+
currentcolor var(--slotted-upload-state-layer-hover, 8%),
|
|
69
|
+
transparent
|
|
70
|
+
);
|
|
71
|
+
--_state-active: color-mix(
|
|
72
|
+
in oklab,
|
|
73
|
+
currentcolor var(--slotted-upload-state-layer-active, 14%),
|
|
74
|
+
transparent
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Each measure is named once and a reach moves only the value it falls
|
|
79
|
+
* back to, so an application that retunes `--slotted-upload-item-gap`
|
|
80
|
+
* retunes every reach rather than discovering that the name it set is the
|
|
81
|
+
* one the default happens to read.
|
|
82
|
+
*
|
|
83
|
+
* Rows stand one step of air apart. At a quarter-rem the two edges of
|
|
84
|
+
* neighbouring rows read as one double rule rather than as two objects —
|
|
85
|
+
* what the `statuses` capture showed down a list of nine — and eight is
|
|
86
|
+
* the gap ReUI's attachment list measures (`c-attachment-5`).
|
|
87
|
+
*/
|
|
88
|
+
--_section-gap: var(--slotted-upload-section-gap, var(--slotted-space-5, 0.75rem));
|
|
89
|
+
--_item-gap: var(--slotted-upload-item-gap, var(--slotted-space-4, 0.5rem));
|
|
90
|
+
--_line-gap: var(--slotted-upload-line-gap, var(--slotted-space-1, 0.125rem));
|
|
91
|
+
--_item-inset-block: var(--slotted-upload-item-padding-block, var(--slotted-space-4, 0.5rem));
|
|
92
|
+
--_item-inset-inline: var(
|
|
93
|
+
--slotted-upload-item-padding-inline,
|
|
94
|
+
var(--slotted-space-5, 0.75rem)
|
|
95
|
+
);
|
|
96
|
+
--_radius: var(--slotted-upload-radius, var(--slotted-radius-md, 6px));
|
|
97
|
+
/*
|
|
98
|
+
* The row's corner is the region's and `list-item`'s. A row is a card-sized
|
|
99
|
+
* object holding a four-pixel tile eight pixels in, and a four-pixel outer
|
|
100
|
+
* corner around it was the same curve as the tile's, which reads as a box
|
|
101
|
+
* cut from sheet rather than as a surface.
|
|
102
|
+
*/
|
|
103
|
+
--_item-radius: var(--slotted-upload-item-radius, var(--slotted-radius-md, 6px));
|
|
104
|
+
--_border-width: var(--slotted-upload-border-width, var(--slotted-border-width-thin, 1px));
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The region's own frame. The border is dashed and one step thicker than a
|
|
108
|
+
* control's, which is the one convention every surveyed drop region shares:
|
|
109
|
+
* a dashed edge says the box is waiting for something, where a solid edge
|
|
110
|
+
* of the same weight would read as a fieldset.
|
|
111
|
+
*/
|
|
112
|
+
--_zone-inset-block: var(--slotted-upload-zone-padding-block, var(--slotted-space-9, 2rem));
|
|
113
|
+
--_zone-inset-inline: var(--slotted-upload-zone-padding-inline, var(--slotted-space-6, 1rem));
|
|
114
|
+
--_zone-border-width: var(
|
|
115
|
+
--slotted-upload-zone-border-width,
|
|
116
|
+
var(--slotted-border-width-medium, 1.5px)
|
|
117
|
+
);
|
|
118
|
+
/*
|
|
119
|
+
* The region at rest. A state of the whole family — invalid, disabled, the
|
|
120
|
+
* field shape — moves these two; a state of the region itself — the drag
|
|
121
|
+
* verdict, a pointer over it — moves the two the region paints with, which
|
|
122
|
+
* it derives from these, so the pointer can step a ground it did not choose.
|
|
123
|
+
*/
|
|
124
|
+
--_zone-edge: var(--slotted-upload-zone-border-color, var(--slotted-border-strong));
|
|
125
|
+
--_zone-ground: var(--slotted-upload-zone-background, var(--slotted-surface-sunken));
|
|
126
|
+
--_zone-glyph-size: var(--slotted-upload-zone-icon-size, calc(1.5 * var(--_glyph-size)));
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A row's controls are inner controls (R2): the glyph plus a step of air on
|
|
130
|
+
* each side, floored at WCAG 2.2 SC 2.5.8's twenty-four pixels so the
|
|
131
|
+
* target holds at every density. The glyph itself is floored at fourteen
|
|
132
|
+
* pixels for the reason the field families floor theirs — the control glyph
|
|
133
|
+
* scale falls to twelve at `dense`, and a cross that small beside a file
|
|
134
|
+
* name is a speck.
|
|
135
|
+
*/
|
|
136
|
+
--_action-glyph-size: max(var(--slotted-upload-icon-size-min, 14px), var(--_glyph-size));
|
|
137
|
+
--_action-size: var(
|
|
138
|
+
--slotted-upload-action-size,
|
|
139
|
+
max(
|
|
140
|
+
var(--slotted-target-min, 24px),
|
|
141
|
+
min(
|
|
142
|
+
calc(var(--_action-glyph-size) + 2 * var(--slotted-space-2, 0.25rem)),
|
|
143
|
+
var(--_inner-size)
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The status the row is in decides one colour, and every part of the row
|
|
150
|
+
* that says something about the status reads it. Neutral by default, so a
|
|
151
|
+
* row that has not been sent yet says nothing in colour at all.
|
|
152
|
+
*/
|
|
153
|
+
--_status-text: var(--slotted-text-muted);
|
|
154
|
+
--_item-surface: var(--slotted-upload-item-background, var(--slotted-surface));
|
|
155
|
+
--_item-border: var(--slotted-upload-item-border-color, var(--slotted-border-subtle));
|
|
156
|
+
|
|
157
|
+
/** What a file's tile is painted when nothing more specific claims it. */
|
|
158
|
+
--_kind-tile: var(--slotted-upload-kind-other);
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The leading, rounded up to a whole pixel. Thirteen at 1.5 is 19.5, and a
|
|
162
|
+
* row built from two lines and whole-pixel insets then stood at 59.5: every
|
|
163
|
+
* edge below the first fractional row landed between two device pixels and
|
|
164
|
+
* blurred at 1×. The rule is `timeline`'s `--_leading`. The computed
|
|
165
|
+
* `line-height` is a length and is inherited as that length, so a part set
|
|
166
|
+
* a step smaller reads `var(--_leading)` again: the custom property is
|
|
167
|
+
* inherited unresolved, and its `1em` resolves against that part's size.
|
|
168
|
+
*/
|
|
169
|
+
--_leading: round(up, calc(1em * var(--slotted-line-height-md, 1.5)), 1px);
|
|
170
|
+
|
|
171
|
+
color: var(--slotted-text);
|
|
172
|
+
font-family: var(--slotted-upload-font-family, var(--slotted-control-font-family));
|
|
173
|
+
font-size: var(--_font-size);
|
|
174
|
+
line-height: var(--_leading);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* The family itself: the column the reader sees in the page. */
|
|
178
|
+
[data-ui='upload'][data-part='root'] {
|
|
179
|
+
container-name: slotted-upload;
|
|
180
|
+
container-type: inline-size;
|
|
181
|
+
/**
|
|
182
|
+
* `container-type: inline-size` applies `contain: inline-size`, which makes
|
|
183
|
+
* the box's intrinsic width nought. In normal flow nothing is wrong; in a
|
|
184
|
+
* column that shrink-wraps its children — a flex column aligned to `start`,
|
|
185
|
+
* which is what the workbench stage is — the family would collapse to one
|
|
186
|
+
* character. `alert` found this in a rendered page rather than in a test,
|
|
187
|
+
* and the width is stated here for the same reason.
|
|
188
|
+
*/
|
|
189
|
+
inline-size: 100%;
|
|
190
|
+
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-direction: column;
|
|
193
|
+
gap: var(--_section-gap);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
[data-ui='upload'][data-size='sm'] {
|
|
197
|
+
--_font-size: var(--slotted-control-font-size-sm, 12px);
|
|
198
|
+
--_glyph-size: var(--slotted-control-icon-size-sm, 14px);
|
|
199
|
+
--_inner-size: var(--slotted-control-inner-size-sm, 22px);
|
|
200
|
+
--_control-height: var(--slotted-control-height-sm, 30px);
|
|
201
|
+
--_control-inset: var(--slotted-control-padding-inline-sm, 10px);
|
|
202
|
+
--_gap: var(--slotted-control-gap-sm, 6px);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
[data-ui='upload'][data-size='lg'] {
|
|
206
|
+
--_font-size: var(--slotted-control-font-size-lg, 14px);
|
|
207
|
+
--_glyph-size: var(--slotted-control-icon-size-lg, 18px);
|
|
208
|
+
--_inner-size: var(--slotted-control-inner-size-lg, 28px);
|
|
209
|
+
--_control-height: var(--slotted-control-height-lg, 38px);
|
|
210
|
+
--_control-inset: var(--slotted-control-padding-inline-lg, 16px);
|
|
211
|
+
--_gap: var(--slotted-control-gap-lg, 8px);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* ── The family's own label and guidance ───────────────────────────────
|
|
215
|
+
*
|
|
216
|
+
* Both are absent when the family sits inside a `field`, which owns the
|
|
217
|
+
* label row, the description and the error slot for every control in this
|
|
218
|
+
* library. They exist for the upload that stands on its own.
|
|
219
|
+
*/
|
|
220
|
+
[data-ui='upload'] [data-part='label'] {
|
|
221
|
+
color: var(--slotted-text);
|
|
222
|
+
flex: none;
|
|
223
|
+
font-weight: var(--slotted-upload-label-font-weight, var(--slotted-font-weight-medium, 500));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
[data-ui='upload'] [data-part='description'] {
|
|
227
|
+
color: var(--slotted-text-muted);
|
|
228
|
+
flex: none;
|
|
229
|
+
margin: 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* ── The region ────────────────────────────────────────────────────────
|
|
233
|
+
*
|
|
234
|
+
* A column: the mark, the sentence, and the trigger. The whole box forwards
|
|
235
|
+
* a click to the trigger when `clickToBrowse` is on, so the target is the
|
|
236
|
+
* box the reader is already looking at; the trigger is what the keyboard
|
|
237
|
+
* reaches, and WCAG 2.2 SC 2.5.7 is discharged by its being there rather
|
|
238
|
+
* than by the region pretending to be a control.
|
|
239
|
+
*/
|
|
240
|
+
[data-ui='upload'] [data-part='dropzone'] {
|
|
241
|
+
--_zone-border-color: var(--_zone-edge);
|
|
242
|
+
--_zone-surface: var(--_zone-ground);
|
|
243
|
+
|
|
244
|
+
align-items: center;
|
|
245
|
+
background-color: var(--_zone-surface);
|
|
246
|
+
border: var(--_zone-border-width) dashed var(--_zone-border-color);
|
|
247
|
+
border-radius: var(--_radius);
|
|
248
|
+
display: flex;
|
|
249
|
+
flex: none;
|
|
250
|
+
flex-direction: column;
|
|
251
|
+
gap: var(--_gap);
|
|
252
|
+
padding-block: var(--_zone-inset-block);
|
|
253
|
+
padding-inline: var(--_zone-inset-inline);
|
|
254
|
+
text-align: center;
|
|
255
|
+
transition:
|
|
256
|
+
background-color var(--slotted-control-transition-duration)
|
|
257
|
+
var(--slotted-control-transition-easing),
|
|
258
|
+
border-color var(--slotted-control-transition-duration)
|
|
259
|
+
var(--slotted-control-transition-easing);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The mark: an `Icon` holding the `upload` glyph, which the binding draws in
|
|
264
|
+
* the `dropzone` shape only. The `field` shape is one control's height and
|
|
265
|
+
* has no room for a second mark beside the trigger's.
|
|
266
|
+
*/
|
|
267
|
+
[data-ui='upload'] [data-part='dropzone'] > [data-ui='icon'] {
|
|
268
|
+
--slotted-icon-size: var(--_zone-glyph-size);
|
|
269
|
+
|
|
270
|
+
color: var(--slotted-text-muted);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The pointer is over the region and carrying something it will take. The
|
|
275
|
+
* accent border and its own tint are the pair `docs/token-foundation.md`
|
|
276
|
+
* requires for a state that has to be seen: the border holds the non-text
|
|
277
|
+
* ratio against the page and against the ground it encloses, and the ground
|
|
278
|
+
* alone would not.
|
|
279
|
+
*/
|
|
280
|
+
[data-ui='upload'] [data-part='dropzone'][data-drop-target] {
|
|
281
|
+
--_zone-border-color: var(--slotted-tone-accent-border);
|
|
282
|
+
--_zone-surface: var(--slotted-tone-accent-surface);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The pointer is over the region carrying something it will refuse, and the
|
|
287
|
+
* reader has not let go yet. Only three of the five rules can be answered
|
|
288
|
+
* before the drop — `DataTransfer.items` gives the kind and the type and not
|
|
289
|
+
* the name or the size — so this is painted for a refusal that is already
|
|
290
|
+
* certain and never for one that is merely possible.
|
|
291
|
+
*/
|
|
292
|
+
[data-ui='upload'] [data-part='dropzone'][data-drop-invalid] {
|
|
293
|
+
--_zone-border-color: var(--slotted-tone-danger-border);
|
|
294
|
+
--_zone-surface: var(--slotted-tone-danger-surface);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* A region that forwards a click is a target, and a target answers the
|
|
299
|
+
* pointer. The binding writes `data-browsable` only while the forward is
|
|
300
|
+
* live, so a region that does nothing when pressed does not promise
|
|
301
|
+
* anything on hover.
|
|
302
|
+
*
|
|
303
|
+
* The edge takes the muted ink and the ground takes a share of the text
|
|
304
|
+
* colour: four per cent on hover, eight on press. The shares are half the
|
|
305
|
+
* family's control state layer because the layer here covers a card-sized
|
|
306
|
+
* box rather than a glyph-sized one, and eight per cent across the whole
|
|
307
|
+
* region read as a disabled block. Four moves the sunken ground by 0.031 in
|
|
308
|
+
* OKLab L, above the 0.02 the theme holds every surface step to, and eight
|
|
309
|
+
* by 0.062.
|
|
310
|
+
*/
|
|
311
|
+
[data-ui='upload'] [data-part='dropzone'][data-browsable] {
|
|
312
|
+
cursor: pointer;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
[data-ui='upload']:not([data-disabled])
|
|
316
|
+
[data-part='dropzone'][data-browsable]:not([data-drop-target], [data-drop-invalid]):hover {
|
|
317
|
+
--_zone-surface: color-mix(
|
|
318
|
+
in oklab,
|
|
319
|
+
var(--slotted-text) var(--slotted-upload-zone-state-layer-hover, 4%),
|
|
320
|
+
var(--_zone-ground)
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
[data-ui='upload']:not([data-disabled])
|
|
325
|
+
[data-part='dropzone'][data-browsable]:not([data-drop-target], [data-drop-invalid]):active {
|
|
326
|
+
--_zone-surface: color-mix(
|
|
327
|
+
in oklab,
|
|
328
|
+
var(--slotted-text) var(--slotted-upload-zone-state-layer-active, 8%),
|
|
329
|
+
var(--_zone-ground)
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* An invalid region keeps its danger edge under the pointer. */
|
|
334
|
+
[data-ui='upload']:not([data-disabled], [data-invalid])
|
|
335
|
+
[data-part='dropzone'][data-browsable]:not([data-drop-target], [data-drop-invalid]):is(
|
|
336
|
+
:hover,
|
|
337
|
+
:active
|
|
338
|
+
) {
|
|
339
|
+
--_zone-border-color: var(--slotted-text-muted);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/*
|
|
343
|
+
* The trigger answers its own pointer. A hover over the trigger is also a
|
|
344
|
+
* hover over the region around it, and the two answering at once drew two
|
|
345
|
+
* targets for one press.
|
|
346
|
+
*/
|
|
347
|
+
[data-ui='upload']:not([data-disabled])
|
|
348
|
+
[data-part='dropzone'][data-browsable]:not([data-drop-target], [data-drop-invalid]):has(
|
|
349
|
+
[data-part='trigger']:hover
|
|
350
|
+
) {
|
|
351
|
+
--_zone-border-color: var(--_zone-edge);
|
|
352
|
+
--_zone-surface: var(--_zone-ground);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
[data-ui='upload'][data-disabled][data-part='root'] {
|
|
356
|
+
--_zone-edge: var(--slotted-disabled-border);
|
|
357
|
+
--_zone-ground: var(--slotted-disabled-background);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
[data-ui='upload'][data-disabled] [data-part='dropzone'] {
|
|
361
|
+
color: var(--slotted-disabled-foreground);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
[data-ui='upload'][data-invalid][data-part='root'] {
|
|
365
|
+
--_zone-edge: var(--slotted-tone-danger-border);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* ── The trigger, and the control inside it ────────────────────────────
|
|
369
|
+
*
|
|
370
|
+
* A `label` drawn as a control. It is not `button`: mounting another family
|
|
371
|
+
* here would give the label a second family's `data-ui` and a second
|
|
372
|
+
* family's contract, and what is wanted is one control-height box that opens
|
|
373
|
+
* the picker natively.
|
|
374
|
+
*/
|
|
375
|
+
[data-ui='upload'] [data-part='trigger'] {
|
|
376
|
+
align-items: center;
|
|
377
|
+
background-color: var(--slotted-upload-trigger-background, var(--slotted-surface));
|
|
378
|
+
block-size: var(--_control-height);
|
|
379
|
+
border: var(--slotted-control-border-width, 1px) solid var(--slotted-control-border-color);
|
|
380
|
+
border-radius: var(--slotted-control-radius, 4px);
|
|
381
|
+
box-shadow: var(--slotted-control-shadow);
|
|
382
|
+
color: var(--slotted-text);
|
|
383
|
+
cursor: pointer;
|
|
384
|
+
display: inline-flex;
|
|
385
|
+
flex: none;
|
|
386
|
+
font: inherit;
|
|
387
|
+
font-weight: var(--slotted-control-font-weight, 600);
|
|
388
|
+
gap: var(--_gap);
|
|
389
|
+
justify-content: center;
|
|
390
|
+
padding-inline: var(--_control-inset);
|
|
391
|
+
position: relative;
|
|
392
|
+
transition:
|
|
393
|
+
background-color var(--slotted-control-transition-duration)
|
|
394
|
+
var(--slotted-control-transition-easing),
|
|
395
|
+
border-color var(--slotted-control-transition-duration)
|
|
396
|
+
var(--slotted-control-transition-easing),
|
|
397
|
+
translate var(--slotted-duration-instant) var(--slotted-control-transition-easing);
|
|
398
|
+
white-space: nowrap;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* The binding draws the mark on the trigger only where the region does not:
|
|
403
|
+
* in the `dropzone` shape the region already carries it, and two of the same
|
|
404
|
+
* mark eighty pixels apart is one mark too many.
|
|
405
|
+
*/
|
|
406
|
+
[data-ui='upload'] [data-part='trigger'] > [data-ui='icon'] {
|
|
407
|
+
--slotted-icon-size: var(--_glyph-size);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
[data-ui='upload']:not([data-disabled]) [data-part='trigger']:hover {
|
|
411
|
+
background-color: var(--slotted-tone-secondary-subtle-hover);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/*
|
|
415
|
+
* The trigger is drawn as a control and presses like `button` does: the
|
|
416
|
+
* ground takes the next step and the box settles by a hairline, at once.
|
|
417
|
+
*/
|
|
418
|
+
[data-ui='upload']:not([data-disabled]) [data-part='trigger']:active {
|
|
419
|
+
background-color: var(--slotted-tone-secondary-subtle-active);
|
|
420
|
+
translate: 0 var(--slotted-border-width-thin, 1px);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
[data-ui='upload'][data-disabled] [data-part='trigger'] {
|
|
424
|
+
background-color: var(--slotted-disabled-background);
|
|
425
|
+
border-color: var(--slotted-disabled-border);
|
|
426
|
+
box-shadow: none;
|
|
427
|
+
color: var(--slotted-disabled-foreground);
|
|
428
|
+
cursor: default;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* The ring belongs to the trigger and the focus belongs to the input inside
|
|
433
|
+
* it. A ring around a one-pixel input would be a ring around nothing.
|
|
434
|
+
*/
|
|
435
|
+
[data-ui='upload'] [data-part='trigger']:has(:focus-visible) {
|
|
436
|
+
outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
|
|
437
|
+
outline-offset: var(--slotted-focus-ring-offset, 2px);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
[data-ui='upload'][data-invalid] [data-part='trigger'] {
|
|
441
|
+
--slotted-focus-ring-color: var(--slotted-focus-ring-color-invalid);
|
|
442
|
+
|
|
443
|
+
border-color: var(--slotted-tone-danger-border);
|
|
444
|
+
/* Width as well as colour (WCAG 1.4.1): in `high-contrast` the danger
|
|
445
|
+
border is the resting edge's black. The trigger is the control in both
|
|
446
|
+
shapes, so it carries the edge `text-field` draws — inside the border,
|
|
447
|
+
so nothing moves, in place of the resting lift; its focus is an outline
|
|
448
|
+
and leaves the edge alone. The region keeps its dashed line. */
|
|
449
|
+
--_invalid-edge-width: var(--slotted-control-invalid-edge-width, 1px);
|
|
450
|
+
|
|
451
|
+
box-shadow: inset 0 0 0 var(--_invalid-edge-width) var(--slotted-tone-danger-border);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Clipped, and removed from nothing else. `display: none` and
|
|
456
|
+
* `visibility: hidden` take the control out of the tab order and out of the
|
|
457
|
+
* accessibility tree, which discards the reason the label pairing exists;
|
|
458
|
+
* stretching a transparent input across the region takes the drop away from
|
|
459
|
+
* the region and makes the drag state unobservable.
|
|
460
|
+
*/
|
|
461
|
+
[data-ui='upload'] [data-part='input'] {
|
|
462
|
+
block-size: 1px;
|
|
463
|
+
border: 0;
|
|
464
|
+
clip-path: inset(50%);
|
|
465
|
+
inline-size: 1px;
|
|
466
|
+
inset-block-start: 50%;
|
|
467
|
+
inset-inline-start: 50%;
|
|
468
|
+
margin: -1px;
|
|
469
|
+
overflow: hidden;
|
|
470
|
+
padding: 0;
|
|
471
|
+
position: absolute;
|
|
472
|
+
white-space: nowrap;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* The family draws the ring on the trigger, so the one the browser would
|
|
477
|
+
* draw on the element holding focus has to go: `tree` moved its ring and
|
|
478
|
+
* forgot this, and a focused row carried two rings in two colours.
|
|
479
|
+
*/
|
|
480
|
+
[data-ui='upload'] [data-part='input']:focus-visible {
|
|
481
|
+
outline: none;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/* ── The summary ───────────────────────────────────────────────────────
|
|
485
|
+
*
|
|
486
|
+
* What the whole queue is doing, in one line: how many files, how far along,
|
|
487
|
+
* how much longer, and the controls that act on every row at once. It is
|
|
488
|
+
* drawn only when there is more than one file, because for a single file the
|
|
489
|
+
* row already says all of it.
|
|
490
|
+
*/
|
|
491
|
+
[data-ui='upload'] [data-part='summary'] {
|
|
492
|
+
align-items: center;
|
|
493
|
+
color: var(--slotted-text-muted);
|
|
494
|
+
display: flex;
|
|
495
|
+
flex: none;
|
|
496
|
+
gap: var(--_gap);
|
|
497
|
+
justify-content: space-between;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
[data-ui='upload'] [data-part='clear'] {
|
|
501
|
+
appearance: none;
|
|
502
|
+
flex: none;
|
|
503
|
+
white-space: nowrap;
|
|
504
|
+
background-color: transparent;
|
|
505
|
+
border: 0;
|
|
506
|
+
border-radius: var(--slotted-control-radius, 4px);
|
|
507
|
+
color: inherit;
|
|
508
|
+
cursor: pointer;
|
|
509
|
+
font: inherit;
|
|
510
|
+
padding-block: var(--slotted-space-1, 0.125rem);
|
|
511
|
+
padding-inline: var(--slotted-space-3, 0.375rem);
|
|
512
|
+
text-decoration-line: underline;
|
|
513
|
+
text-underline-offset: 0.2em;
|
|
514
|
+
transition: background-color var(--slotted-control-transition-duration)
|
|
515
|
+
var(--slotted-control-transition-easing);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
[data-ui='upload'] [data-part='clear']:enabled:hover {
|
|
519
|
+
background-color: var(--_state-hover);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
[data-ui='upload'] [data-part='clear']:enabled:active {
|
|
523
|
+
background-color: var(--_state-active);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
[data-ui='upload'] [data-part='clear']:disabled {
|
|
527
|
+
color: var(--slotted-disabled-foreground);
|
|
528
|
+
cursor: default;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
[data-ui='upload'] [data-part='clear']:focus-visible {
|
|
532
|
+
outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
|
|
533
|
+
outline-offset: var(--slotted-focus-ring-offset, 2px);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/* ── The list ──────────────────────────────────────────────────────────
|
|
537
|
+
*
|
|
538
|
+
* A directory selection produces four hundred files, and four hundred rows
|
|
539
|
+
* is not a list a person reads. The block size is bounded in rows and the
|
|
540
|
+
* platform scrolls the rest: no disclosure, no second state model, and a row
|
|
541
|
+
* left half-visible at the fold is what says there is more.
|
|
542
|
+
*
|
|
543
|
+
* `--slotted-upload-visible-rows` is written by the binding from `maxRows`,
|
|
544
|
+
* because it is a fact about this instance rather than a value a theme can
|
|
545
|
+
* hold — the category `--slotted-tree-depth` is in. `maxRows: 0` means no
|
|
546
|
+
* bound, and the binding says so by writing the number of files rather than
|
|
547
|
+
* by a second property: a cap that is the content's own height is a cap
|
|
548
|
+
* nothing reaches, and the sheet then needs one rule instead of two.
|
|
549
|
+
*
|
|
550
|
+
* The row height in the sum is a row at rest. A row that has grown a bar and
|
|
551
|
+
* a message is taller, so the fold falls inside the last row rather than
|
|
552
|
+
* between two — which is what says there is more below without a shadow, a
|
|
553
|
+
* gradient or a count.
|
|
554
|
+
*/
|
|
555
|
+
[data-ui='upload'] [data-part='list'] {
|
|
556
|
+
/** One row, from the parts a row is made of. Used twice below. */
|
|
557
|
+
--_row-block-size: calc(
|
|
558
|
+
max(var(--_preview-size), 2lh) + 2 * var(--_item-inset-block) + 2 * var(--_border-width) +
|
|
559
|
+
var(--_item-gap)
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
display: flex;
|
|
563
|
+
flex-direction: column;
|
|
564
|
+
gap: var(--_item-gap);
|
|
565
|
+
list-style: none;
|
|
566
|
+
margin: 0;
|
|
567
|
+
/*
|
|
568
|
+
* The list is the one part of the column that gives. Everything above it
|
|
569
|
+
* holds its size, so an 18rem panel takes the room out of the queue and
|
|
570
|
+
* not out of the region, the summary or the label — and the queue answers
|
|
571
|
+
* by scrolling. A flex item's floor is its content, so the floor is stated
|
|
572
|
+
* here: without it the list is what an outer bound pushes on, and it went
|
|
573
|
+
* to nought inside a short panel while the region kept every pixel.
|
|
574
|
+
*/
|
|
575
|
+
flex: 1 1 auto;
|
|
576
|
+
min-block-size: var(--_row-block-size);
|
|
577
|
+
max-block-size: calc(var(--slotted-upload-visible-rows, 6) * var(--_row-block-size));
|
|
578
|
+
overflow-y: auto;
|
|
579
|
+
padding: 0;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/* ── One file ──────────────────────────────────────────────────────────
|
|
583
|
+
*
|
|
584
|
+
* A row is one object and it is laid out as a grid, not as a flex line. Four
|
|
585
|
+
* things follow from that, and all four were wrong in the shape before it.
|
|
586
|
+
*
|
|
587
|
+
* **The marks are centred on the block they are about.** The chip and the
|
|
588
|
+
* controls sit in the first track; the sentence a failure carries spans a
|
|
589
|
+
* second track underneath. So a row that has something to explain grows
|
|
590
|
+
* downwards and moves nothing: centring is over the name and its detail,
|
|
591
|
+
* which is what the chip and the controls are about, rather than over a box
|
|
592
|
+
* whose height a two-line message decided.
|
|
593
|
+
*
|
|
594
|
+
* **The size left the name's line.** It stood at the far end of it, so the
|
|
595
|
+
* column of sizes moved with however many controls each row happened to
|
|
596
|
+
* offer, and the list read as ragged. It is secondary information and it now
|
|
597
|
+
* sits under the name with the status, where it lines up with the name above
|
|
598
|
+
* it and needs no reserved width anywhere.
|
|
599
|
+
*
|
|
600
|
+
* **The bar left the flow.** It is the row's own bottom edge, so a row is the
|
|
601
|
+
* same height whether it is waiting, sending or done, and progress is
|
|
602
|
+
* readable straight down a list of nine.
|
|
603
|
+
*
|
|
604
|
+
* **The row has a surface and an edge.** Before, a file was a naked line of
|
|
605
|
+
* text with icons beside it, and only a failed one had a box; a file is a
|
|
606
|
+
* thing the reader acts on, and it reads as one.
|
|
607
|
+
*/
|
|
608
|
+
[data-ui='upload'] [data-part='item'] {
|
|
609
|
+
align-items: center;
|
|
610
|
+
/*
|
|
611
|
+
* A row is never squeezed. Flex children shrink by default, so nine rows in
|
|
612
|
+
* the room for three were compressed to slivers — the tile flattened, the
|
|
613
|
+
* names spilled out of the rows that no longer had height for them, and
|
|
614
|
+
* nothing scrolled, because the content had been made to fit. Read at 420
|
|
615
|
+
* pixels with `--slotted-upload-visible-rows: 3` and nine files.
|
|
616
|
+
*/
|
|
617
|
+
flex: none;
|
|
618
|
+
background-color: var(--_item-surface);
|
|
619
|
+
border: var(--_border-width) solid var(--_item-border);
|
|
620
|
+
border-radius: var(--_item-radius);
|
|
621
|
+
column-gap: var(--_gap);
|
|
622
|
+
display: grid;
|
|
623
|
+
grid-template-areas:
|
|
624
|
+
'preview body actions'
|
|
625
|
+
'message message message';
|
|
626
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
627
|
+
/*
|
|
628
|
+
* The bar is a child positioned against this box, and the corner radius is
|
|
629
|
+
* what clips it to the row's own shape.
|
|
630
|
+
*/
|
|
631
|
+
overflow: hidden;
|
|
632
|
+
padding-block: var(--_item-inset-block);
|
|
633
|
+
padding-inline: var(--_item-inset-inline);
|
|
634
|
+
position: relative;
|
|
635
|
+
row-gap: var(--_line-gap);
|
|
636
|
+
transition:
|
|
637
|
+
background-color var(--slotted-control-transition-duration)
|
|
638
|
+
var(--slotted-control-transition-easing),
|
|
639
|
+
border-color var(--slotted-control-transition-duration)
|
|
640
|
+
var(--slotted-control-transition-easing),
|
|
641
|
+
opacity var(--slotted-duration-medium) var(--slotted-easing-enter),
|
|
642
|
+
translate var(--slotted-duration-medium) var(--slotted-easing-enter);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* A file arrives: it fades in from a step above its place, so a row that
|
|
647
|
+
* lands in a list of nine is seen landing rather than found there. It starts
|
|
648
|
+
* from the style below only on the frame the row is first rendered — a row
|
|
649
|
+
* that re-renders with a new status does not arrive again.
|
|
650
|
+
*
|
|
651
|
+
* A row that leaves does not animate. The binding removes it when the files
|
|
652
|
+
* change, and holding it for an exit would need a second list beside the
|
|
653
|
+
* controlled `files`; the promise is in `docs/process/build-order.md`.
|
|
654
|
+
*/
|
|
655
|
+
@starting-style {
|
|
656
|
+
[data-ui='upload'] [data-part='item'] {
|
|
657
|
+
opacity: 0;
|
|
658
|
+
translate: 0 calc(-1 * var(--slotted-space-2, 0.25rem));
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
[data-ui='upload'] [data-part='item'][data-status='success'] {
|
|
663
|
+
--_status-text: var(--slotted-tone-success-text);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* A file the reader has chosen and not yet sent has a dashed edge: the edge
|
|
668
|
+
* the region has, which already means *waiting for something* in this
|
|
669
|
+
* family. It is said in line rather than in colour, which keeps the rule
|
|
670
|
+
* above — a row not yet sent says nothing in colour — and it holds under
|
|
671
|
+
* forced colours, where the line survives and a tint does not. ReUI draws
|
|
672
|
+
* its `idle` attachment the same way (`c-attachment-5`).
|
|
673
|
+
*/
|
|
674
|
+
[data-ui='upload'] [data-part='item'][data-status='ready'] {
|
|
675
|
+
border-style: dashed;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
[data-ui='upload'] [data-part='item'][data-status='paused'],
|
|
679
|
+
[data-ui='upload'] [data-part='item'][data-status='stalled'] {
|
|
680
|
+
--_status-text: var(--slotted-tone-warning-text);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* The three failures, and the two of them that tint their row. A file that
|
|
685
|
+
* was refused before it entered the queue and a file whose transfer failed
|
|
686
|
+
* both need the reader to do something; a file the reader stopped themselves
|
|
687
|
+
* does not, so it stays neutral and only its word changes.
|
|
688
|
+
*/
|
|
689
|
+
[data-ui='upload'] [data-part='item'][data-status='rejected'],
|
|
690
|
+
[data-ui='upload'] [data-part='item'][data-status='error'] {
|
|
691
|
+
--_status-text: var(--slotted-tone-danger-text);
|
|
692
|
+
--_item-surface: var(--slotted-tone-danger-surface);
|
|
693
|
+
--_item-border: var(--slotted-tone-danger-border);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
[data-ui='upload'] [data-part='item'][data-status='canceled'] {
|
|
697
|
+
--_status-text: var(--slotted-text-muted);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
[data-ui='upload'][data-disabled] [data-part='item'] {
|
|
701
|
+
color: var(--slotted-disabled-foreground);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/* ── The chip ──────────────────────────────────────────────────────────
|
|
705
|
+
*
|
|
706
|
+
* A solid tile in the file's own colour with the mark knocked out of it.
|
|
707
|
+
*
|
|
708
|
+
* The shape this replaced was a grey square with a coloured mark inside, and
|
|
709
|
+
* the reviewer's reading of it was the right one: a wash of a tone at twelve
|
|
710
|
+
* per cent over a white page is not a colour, it is a grey box with a colour
|
|
711
|
+
* near it. A tile is a decision; a tint is an apology for not making one.
|
|
712
|
+
*
|
|
713
|
+
* The colours are the ones a reader has already learnt from every product
|
|
714
|
+
* they use — a PDF red, a word-processed document blue, a spreadsheet green,
|
|
715
|
+
* a deck orange, a film indigo — and they are a scale of the theme's rather
|
|
716
|
+
* than five tones stretched over nine kinds, because the tones hold no second
|
|
717
|
+
* blue and no indigo at all. The argument, the values and the measurements
|
|
718
|
+
* are under **File kind** in `docs/token-foundation.md`.
|
|
719
|
+
*
|
|
720
|
+
* One ratio answers both questions the pair raises, because the mark is the
|
|
721
|
+
* tile's own foreground: the tile against the page, and the mark against the
|
|
722
|
+
* tile. `packages/themes/test/default-theme.test.mjs` measures all
|
|
723
|
+
* twenty-seven.
|
|
724
|
+
*/
|
|
725
|
+
[data-ui='upload'] [data-part='preview'] {
|
|
726
|
+
align-items: center;
|
|
727
|
+
background-color: var(--_kind-tile);
|
|
728
|
+
block-size: var(--_preview-size);
|
|
729
|
+
border-radius: var(--slotted-upload-preview-radius, var(--slotted-radius-sm, 4px));
|
|
730
|
+
color: var(--slotted-upload-kind-foreground);
|
|
731
|
+
display: inline-flex;
|
|
732
|
+
flex-direction: column;
|
|
733
|
+
grid-area: preview;
|
|
734
|
+
inline-size: var(--_preview-size);
|
|
735
|
+
justify-content: center;
|
|
736
|
+
overflow: hidden;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
[data-ui='upload'] [data-part='preview'] > img {
|
|
740
|
+
block-size: 100%;
|
|
741
|
+
inline-size: 100%;
|
|
742
|
+
object-fit: cover;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/* The kind's glyph, drawn by the binding when there is no picture to show. */
|
|
746
|
+
[data-ui='upload'] [data-part='preview'] > [data-ui='icon'] {
|
|
747
|
+
--slotted-icon-size: var(--_glyph-size);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
[data-ui='upload'] [data-part='preview'][data-kind='pdf'] {
|
|
751
|
+
--_kind-tile: var(--slotted-upload-kind-pdf);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
[data-ui='upload'] [data-part='preview'][data-kind='document'] {
|
|
755
|
+
--_kind-tile: var(--slotted-upload-kind-document);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
[data-ui='upload'] [data-part='preview'][data-kind='spreadsheet'] {
|
|
759
|
+
--_kind-tile: var(--slotted-upload-kind-spreadsheet);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
[data-ui='upload'] [data-part='preview'][data-kind='presentation'] {
|
|
763
|
+
--_kind-tile: var(--slotted-upload-kind-presentation);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
[data-ui='upload'] [data-part='preview'][data-kind='image'] {
|
|
767
|
+
--_kind-tile: var(--slotted-upload-kind-image);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
[data-ui='upload'] [data-part='preview'][data-kind='video'] {
|
|
771
|
+
--_kind-tile: var(--slotted-upload-kind-video);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
[data-ui='upload'] [data-part='preview'][data-kind='audio'] {
|
|
775
|
+
--_kind-tile: var(--slotted-upload-kind-audio);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
[data-ui='upload'] [data-part='preview'][data-kind='archive'] {
|
|
779
|
+
--_kind-tile: var(--slotted-upload-kind-archive);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/*
|
|
783
|
+
* A tinted row keeps its tiles. The rule here was the other way round for one
|
|
784
|
+
* capture — the status took the tile's colour, on the argument that what went
|
|
785
|
+
* wrong outranks what the file is — and the capture showed what it cost: a
|
|
786
|
+
* refused archive turned red on the one row whose message says archives are
|
|
787
|
+
* not accepted, so the colour stopped answering the question the reader had.
|
|
788
|
+
*
|
|
789
|
+
* The status has four signals already: the row's ground, its edge, its word
|
|
790
|
+
* and its sentence. The tile is about the file. Measured rather than assumed,
|
|
791
|
+
* every kind clears 4.03 : 1 against a failed row's ground in `light`,
|
|
792
|
+
* 6.46 : 1 in `dark` and 8.31 : 1 in `high-contrast`, against a 3 : 1 floor —
|
|
793
|
+
* held by `packages/themes/test/default-theme.test.mjs`.
|
|
794
|
+
*/
|
|
795
|
+
|
|
796
|
+
/* ── What the row says ─────────────────────────────────────────────────
|
|
797
|
+
*
|
|
798
|
+
* Two lines: the name, and everything secondary about it on one line under
|
|
799
|
+
* it. The body is the only part of the row with `min-inline-size: 0`, which
|
|
800
|
+
* is what makes a long name truncate instead of pushing the controls off the
|
|
801
|
+
* end.
|
|
802
|
+
*/
|
|
803
|
+
[data-ui='upload'] [data-part='item'] > [data-part='body'] {
|
|
804
|
+
display: flex;
|
|
805
|
+
flex-direction: column;
|
|
806
|
+
gap: var(--_line-gap);
|
|
807
|
+
grid-area: body;
|
|
808
|
+
min-inline-size: 0;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* The name and its extension are one run, and the run is laid out in the
|
|
813
|
+
* name's own direction rather than the document's. The binding writes
|
|
814
|
+
* `dir="auto"` here, so `claim-form-2026-0914.pdf` reads in that order inside
|
|
815
|
+
* a right-to-left page: without the wrapper the two are separate flex items,
|
|
816
|
+
* the container places them right to left, and the reader is shown
|
|
817
|
+
* `pdf.claim-form-2026-0914`. Found in the harness capture at `dir="rtl"`,
|
|
818
|
+
* where nothing failed and the name was backwards.
|
|
819
|
+
*/
|
|
820
|
+
[data-ui='upload'] [data-part='filename'] {
|
|
821
|
+
display: flex;
|
|
822
|
+
/*
|
|
823
|
+
* The name is the row's title and the detail line under it is at the same
|
|
824
|
+
* weight as body text, so the two lines differed by one pixel of size and
|
|
825
|
+
* nothing else. Medium is the weight `label` already uses for the family's
|
|
826
|
+
* own heading.
|
|
827
|
+
*/
|
|
828
|
+
font-weight: var(--slotted-upload-name-font-weight, var(--slotted-font-weight-medium, 500));
|
|
829
|
+
min-inline-size: 0;
|
|
830
|
+
unicode-bidi: isolate;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* The name opens the file. A reader who has just attached a scan clicks the
|
|
835
|
+
* scan, which is what every product they already use does, and it is why the
|
|
836
|
+
* eye that used to sit in the controls is gone: a third affordance for an
|
|
837
|
+
* action the name already implies, in a row that has enough in it.
|
|
838
|
+
*
|
|
839
|
+
* It is a real `button` when the file can be previewed and a plain `span`
|
|
840
|
+
* when it cannot, so the affordance is present exactly when the action is.
|
|
841
|
+
* Nothing about it looks like a control until the pointer is on it — a list
|
|
842
|
+
* of nine underlined names is a list nobody can read.
|
|
843
|
+
*/
|
|
844
|
+
[data-ui='upload'] button[data-part='filename'] {
|
|
845
|
+
appearance: none;
|
|
846
|
+
background: none;
|
|
847
|
+
border: 0;
|
|
848
|
+
color: inherit;
|
|
849
|
+
cursor: pointer;
|
|
850
|
+
/* Everything but the weight, which the name states for itself above. */
|
|
851
|
+
font-family: inherit;
|
|
852
|
+
font-size: inherit;
|
|
853
|
+
line-height: inherit;
|
|
854
|
+
padding: 0;
|
|
855
|
+
text-align: start;
|
|
856
|
+
transition: translate var(--slotted-duration-instant) var(--slotted-control-transition-easing);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
[data-ui='upload'] button[data-part='filename']:enabled:hover {
|
|
860
|
+
text-decoration-line: underline;
|
|
861
|
+
text-underline-offset: 0.2em;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/*
|
|
865
|
+
* A name has no ground to step, and its hover already drew the underline, so
|
|
866
|
+
* a press answers in place: the name settles by a hairline — the answer
|
|
867
|
+
* `timeline` gives its group heading.
|
|
868
|
+
*/
|
|
869
|
+
[data-ui='upload'] button[data-part='filename']:enabled:active {
|
|
870
|
+
text-decoration-line: underline;
|
|
871
|
+
text-underline-offset: 0.2em;
|
|
872
|
+
translate: 0 var(--slotted-border-width-thin, 1px);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
[data-ui='upload'] button[data-part='filename']:focus-visible {
|
|
876
|
+
border-radius: var(--slotted-control-radius, 4px);
|
|
877
|
+
outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
|
|
878
|
+
outline-offset: var(--slotted-focus-ring-offset, 2px);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/* In a wall of pictures the picture is the thing the reader is pointing at. */
|
|
882
|
+
[data-ui='upload'][data-layout='grid'] button[data-part='preview'] {
|
|
883
|
+
appearance: none;
|
|
884
|
+
border: 0;
|
|
885
|
+
cursor: pointer;
|
|
886
|
+
padding: 0;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* The name shrinks to an ellipsis and the extension does not, so
|
|
891
|
+
* `quarterly-report-consolidated-final-v7.pdf` becomes
|
|
892
|
+
* `quarterly-report-conso….pdf` rather than losing the part that says what
|
|
893
|
+
* the file is. CSS cannot truncate in the middle; the split is made at the
|
|
894
|
+
* last dot in the binding, and this is the half of the answer the sheet owes.
|
|
895
|
+
*/
|
|
896
|
+
[data-ui='upload'] [data-part='name'] {
|
|
897
|
+
flex: 0 1 auto;
|
|
898
|
+
min-inline-size: 0;
|
|
899
|
+
overflow: hidden;
|
|
900
|
+
text-overflow: ellipsis;
|
|
901
|
+
white-space: nowrap;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
[data-ui='upload'] [data-part='extension'] {
|
|
905
|
+
flex: none;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/** The second line: the size, then the word for what is happening to it. */
|
|
909
|
+
[data-ui='upload'] [data-part='detail'] {
|
|
910
|
+
align-items: baseline;
|
|
911
|
+
color: var(--slotted-text-muted);
|
|
912
|
+
display: flex;
|
|
913
|
+
flex-wrap: wrap;
|
|
914
|
+
font-size: var(--slotted-font-size-xs, 0.75rem);
|
|
915
|
+
line-height: var(--_leading);
|
|
916
|
+
min-inline-size: 0;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
[data-ui='upload'] [data-part='meta'] {
|
|
920
|
+
font-variant-numeric: tabular-nums;
|
|
921
|
+
white-space: nowrap;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* The word, in the colour the status set. A status said in colour alone fails
|
|
926
|
+
* WCAG 2.2 SC 1.4.1, which is why it is a word first and a colour second.
|
|
927
|
+
*/
|
|
928
|
+
[data-ui='upload'] [data-part='status'] {
|
|
929
|
+
color: var(--_status-text);
|
|
930
|
+
font-weight: var(--slotted-upload-status-font-weight, var(--slotted-font-weight-medium, 500));
|
|
931
|
+
min-inline-size: 0;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* The separator between the two, and it is a character because it is one: a
|
|
936
|
+
* middot between two pieces of text is punctuation, not a mark the family
|
|
937
|
+
* draws. `breadcrumb` writes its own the same way.
|
|
938
|
+
*
|
|
939
|
+
* It hangs off the size rather than off the word, so that a detail line which
|
|
940
|
+
* wraps breaks *after* the dot and not before it. Written the other way round
|
|
941
|
+
* it left an orphaned middot at the start of the second line, which is what
|
|
942
|
+
* the card at 720 pixels showed.
|
|
943
|
+
*/
|
|
944
|
+
[data-ui='upload'] [data-part='meta']:has(+ [data-part='status'])::after {
|
|
945
|
+
color: var(--slotted-text-muted);
|
|
946
|
+
content: ' ·';
|
|
947
|
+
margin-inline-end: 0.4em;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
[data-ui='upload'] [data-part='message'] {
|
|
951
|
+
color: var(--_status-text);
|
|
952
|
+
font-size: var(--slotted-font-size-xs, 0.75rem);
|
|
953
|
+
line-height: var(--_leading);
|
|
954
|
+
grid-area: message;
|
|
955
|
+
margin: 0;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* The bar is the row's own bottom edge. It is `progress-indicator`, drawn
|
|
960
|
+
* where the family put it: the family says how much room a bar gets and
|
|
961
|
+
* nothing about how it is drawn, because a second bar here would be a second
|
|
962
|
+
* answer to one question, drifting from the first the moment either is
|
|
963
|
+
* retuned.
|
|
964
|
+
*/
|
|
965
|
+
[data-ui='upload'] [data-part='progress'] {
|
|
966
|
+
display: block;
|
|
967
|
+
inset-block-end: 0;
|
|
968
|
+
inset-inline: 0;
|
|
969
|
+
position: absolute;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/* ── A row's controls ──────────────────────────────────────────────────
|
|
973
|
+
*
|
|
974
|
+
* Inner controls (R2): the glyph plus a step of air, no frame and no ground
|
|
975
|
+
* at rest, one ground step on hover and one more on press, the theme's ring
|
|
976
|
+
* on focus, and a target that reaches the WCAG minimum without the glyph
|
|
977
|
+
* growing to fill it.
|
|
978
|
+
*/
|
|
979
|
+
/**
|
|
980
|
+
* Two slots, whatever this row happens to offer. A row with one control and a
|
|
981
|
+
* row with two would otherwise end their bodies fifty pixels apart, and the
|
|
982
|
+
* column of sizes down the list read as ragged rather than as a column —
|
|
983
|
+
* which is what the first capture at 420 pixels showed.
|
|
984
|
+
*/
|
|
985
|
+
/**
|
|
986
|
+
* Packed to the end of the row and reserving nothing. The reservation this
|
|
987
|
+
* replaced existed to keep a column of sizes straight while the size lived at
|
|
988
|
+
* the far end of the name's line; the size is on its own line now, so there
|
|
989
|
+
* is no column to keep and a row that offers one control no longer carries
|
|
990
|
+
* two empty slots to prove it.
|
|
991
|
+
*/
|
|
992
|
+
[data-ui='upload'] [data-part='actions'] {
|
|
993
|
+
display: inline-flex;
|
|
994
|
+
gap: var(--slotted-space-1, 0.125rem);
|
|
995
|
+
grid-area: actions;
|
|
996
|
+
justify-content: flex-end;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
[data-ui='upload'] [data-part='viewerExternal'],
|
|
1000
|
+
[data-ui='upload'] [data-part='viewerClose'],
|
|
1001
|
+
[data-ui='upload'] [data-part='start'],
|
|
1002
|
+
[data-ui='upload'] [data-part='pause'],
|
|
1003
|
+
[data-ui='upload'] [data-part='retry'],
|
|
1004
|
+
[data-ui='upload'] [data-part='cancel'],
|
|
1005
|
+
[data-ui='upload'] [data-part='remove'] {
|
|
1006
|
+
align-items: center;
|
|
1007
|
+
appearance: none;
|
|
1008
|
+
background-color: transparent;
|
|
1009
|
+
block-size: var(--_action-size);
|
|
1010
|
+
border: 0;
|
|
1011
|
+
border-radius: var(--slotted-upload-action-radius, var(--slotted-control-radius, 4px));
|
|
1012
|
+
color: inherit;
|
|
1013
|
+
cursor: pointer;
|
|
1014
|
+
display: inline-flex;
|
|
1015
|
+
flex: none;
|
|
1016
|
+
font: inherit;
|
|
1017
|
+
inline-size: var(--_action-size);
|
|
1018
|
+
justify-content: center;
|
|
1019
|
+
padding: 0;
|
|
1020
|
+
transition:
|
|
1021
|
+
background-color var(--slotted-control-transition-duration)
|
|
1022
|
+
var(--slotted-control-transition-easing),
|
|
1023
|
+
color var(--slotted-control-transition-duration) var(--slotted-control-transition-easing);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Each control holds the `Icon` its binding chose for it: external, x, play,
|
|
1028
|
+
* pause, rotate-ccw, x and trash. A cross means *stop this* and a bin means
|
|
1029
|
+
* *take it away*, because the marks appear together on one row.
|
|
1030
|
+
*/
|
|
1031
|
+
[data-ui='upload'] [data-part='viewerExternal'] > [data-ui='icon'],
|
|
1032
|
+
[data-ui='upload'] [data-part='viewerClose'] > [data-ui='icon'],
|
|
1033
|
+
[data-ui='upload'] [data-part='start'] > [data-ui='icon'],
|
|
1034
|
+
[data-ui='upload'] [data-part='pause'] > [data-ui='icon'],
|
|
1035
|
+
[data-ui='upload'] [data-part='retry'] > [data-ui='icon'],
|
|
1036
|
+
[data-ui='upload'] [data-part='cancel'] > [data-ui='icon'],
|
|
1037
|
+
[data-ui='upload'] [data-part='remove'] > [data-ui='icon'] {
|
|
1038
|
+
--slotted-icon-size: var(--_action-glyph-size);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* On a finger the target is the comfortable size in both axes and the box
|
|
1043
|
+
* is the one a mouse sees: a transparent `::after` centred on each control
|
|
1044
|
+
* reaches 44 pixels, and the ground and the ring stay square. The controls
|
|
1045
|
+
* of one row stand two pixels apart, so none reaches across the edge it
|
|
1046
|
+
* shares with the one beside it; between two of them each keeps its own
|
|
1047
|
+
* width and is 44 tall.
|
|
1048
|
+
*/
|
|
1049
|
+
@media (pointer: coarse) {
|
|
1050
|
+
[data-ui='upload']
|
|
1051
|
+
:is(
|
|
1052
|
+
[data-part='viewerExternal'],
|
|
1053
|
+
[data-part='viewerClose'],
|
|
1054
|
+
[data-part='start'],
|
|
1055
|
+
[data-part='pause'],
|
|
1056
|
+
[data-part='retry'],
|
|
1057
|
+
[data-part='cancel'],
|
|
1058
|
+
[data-part='remove']
|
|
1059
|
+
) {
|
|
1060
|
+
position: relative;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
[data-ui='upload']
|
|
1064
|
+
:is(
|
|
1065
|
+
[data-part='viewerExternal'],
|
|
1066
|
+
[data-part='viewerClose'],
|
|
1067
|
+
[data-part='start'],
|
|
1068
|
+
[data-part='pause'],
|
|
1069
|
+
[data-part='retry'],
|
|
1070
|
+
[data-part='cancel'],
|
|
1071
|
+
[data-part='remove']
|
|
1072
|
+
)::after {
|
|
1073
|
+
content: '';
|
|
1074
|
+
inset: min(0px, calc((100% - var(--slotted-target-comfortable, 44px)) / 2));
|
|
1075
|
+
position: absolute;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
[data-ui='upload']
|
|
1079
|
+
:is(
|
|
1080
|
+
[data-part='viewerExternal'],
|
|
1081
|
+
[data-part='start'],
|
|
1082
|
+
[data-part='pause'],
|
|
1083
|
+
[data-part='retry'],
|
|
1084
|
+
[data-part='cancel']
|
|
1085
|
+
):has(
|
|
1086
|
+
+ :is(
|
|
1087
|
+
[data-part='viewerClose'],
|
|
1088
|
+
[data-part='pause'],
|
|
1089
|
+
[data-part='retry'],
|
|
1090
|
+
[data-part='cancel'],
|
|
1091
|
+
[data-part='remove']
|
|
1092
|
+
):not([hidden])
|
|
1093
|
+
)::after {
|
|
1094
|
+
inset-inline-end: 0;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
[data-ui='upload']
|
|
1098
|
+
:is(
|
|
1099
|
+
[data-part='viewerExternal'],
|
|
1100
|
+
[data-part='start'],
|
|
1101
|
+
[data-part='pause'],
|
|
1102
|
+
[data-part='retry'],
|
|
1103
|
+
[data-part='cancel']
|
|
1104
|
+
):not([hidden])
|
|
1105
|
+
+ :is(
|
|
1106
|
+
[data-part='viewerClose'],
|
|
1107
|
+
[data-part='pause'],
|
|
1108
|
+
[data-part='retry'],
|
|
1109
|
+
[data-part='cancel'],
|
|
1110
|
+
[data-part='remove']
|
|
1111
|
+
)::after {
|
|
1112
|
+
inset-inline-start: 0;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* `:enabled` is the guard `packages/styles/src/states.test.mjs` requires,
|
|
1118
|
+
* and the reason it exists: a control the browser has already refused must
|
|
1119
|
+
* not answer a pointer.
|
|
1120
|
+
*/
|
|
1121
|
+
[data-ui='upload'] [data-part='viewerExternal']:enabled:hover,
|
|
1122
|
+
[data-ui='upload'] [data-part='viewerClose']:enabled:hover,
|
|
1123
|
+
[data-ui='upload'] [data-part='start']:enabled:hover,
|
|
1124
|
+
[data-ui='upload'] [data-part='pause']:enabled:hover,
|
|
1125
|
+
[data-ui='upload'] [data-part='retry']:enabled:hover,
|
|
1126
|
+
[data-ui='upload'] [data-part='cancel']:enabled:hover,
|
|
1127
|
+
[data-ui='upload'] [data-part='remove']:enabled:hover {
|
|
1128
|
+
background-color: var(--_state-hover);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
[data-ui='upload'] [data-part='viewerExternal']:enabled:active,
|
|
1132
|
+
[data-ui='upload'] [data-part='viewerClose']:enabled:active,
|
|
1133
|
+
[data-ui='upload'] [data-part='start']:enabled:active,
|
|
1134
|
+
[data-ui='upload'] [data-part='pause']:enabled:active,
|
|
1135
|
+
[data-ui='upload'] [data-part='retry']:enabled:active,
|
|
1136
|
+
[data-ui='upload'] [data-part='cancel']:enabled:active,
|
|
1137
|
+
[data-ui='upload'] [data-part='remove']:enabled:active {
|
|
1138
|
+
background-color: var(--_state-active);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
[data-ui='upload'] [data-part='viewerExternal']:disabled,
|
|
1142
|
+
[data-ui='upload'] [data-part='viewerClose']:disabled,
|
|
1143
|
+
[data-ui='upload'] [data-part='start']:disabled,
|
|
1144
|
+
[data-ui='upload'] [data-part='pause']:disabled,
|
|
1145
|
+
[data-ui='upload'] [data-part='retry']:disabled,
|
|
1146
|
+
[data-ui='upload'] [data-part='cancel']:disabled,
|
|
1147
|
+
[data-ui='upload'] [data-part='remove']:disabled {
|
|
1148
|
+
color: var(--slotted-disabled-foreground);
|
|
1149
|
+
cursor: default;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
[data-ui='upload'] [data-part='viewerExternal']:focus-visible,
|
|
1153
|
+
[data-ui='upload'] [data-part='viewerClose']:focus-visible,
|
|
1154
|
+
[data-ui='upload'] [data-part='start']:focus-visible,
|
|
1155
|
+
[data-ui='upload'] [data-part='pause']:focus-visible,
|
|
1156
|
+
[data-ui='upload'] [data-part='retry']:focus-visible,
|
|
1157
|
+
[data-ui='upload'] [data-part='cancel']:focus-visible,
|
|
1158
|
+
[data-ui='upload'] [data-part='remove']:focus-visible {
|
|
1159
|
+
outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
|
|
1160
|
+
outline-offset: calc(-1 * var(--slotted-focus-ring-width, 2px));
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/* ── The viewer ────────────────────────────────────────────────────────
|
|
1164
|
+
*
|
|
1165
|
+
* A file, full screen, on a surface of its own. `dialog` owns the layer, the
|
|
1166
|
+
* scrim, the focus containment, the scroll lock and Escape; what this family
|
|
1167
|
+
* owns is the stage, the header and the two controls that move through the
|
|
1168
|
+
* set.
|
|
1169
|
+
*
|
|
1170
|
+
* A preview that left the page was the first answer and it was the wrong one:
|
|
1171
|
+
* it loses the form the reader was in the middle of filling in. The new tab
|
|
1172
|
+
* survives as the escape hatch inside the viewer, where a PDF is better read
|
|
1173
|
+
* in the browser's own full window.
|
|
1174
|
+
*/
|
|
1175
|
+
[data-ui='upload'][data-part='viewer'] {
|
|
1176
|
+
background-color: var(--slotted-surface);
|
|
1177
|
+
block-size: 100%;
|
|
1178
|
+
display: grid;
|
|
1179
|
+
/*
|
|
1180
|
+
* The dialog's panel is a three-row grid — header, body, footer — and this
|
|
1181
|
+
* family brings a header of its own. Written into the first row the viewer
|
|
1182
|
+
* is as tall as its own content, with the rest of a full-screen layer left
|
|
1183
|
+
* blank under it; spanning the three makes the layer the viewer's surface.
|
|
1184
|
+
* The declaration is ignored wherever the parent is not a grid, which is
|
|
1185
|
+
* what lets the same box be photographed on its own in the harness.
|
|
1186
|
+
*/
|
|
1187
|
+
grid-row: 1 / -1;
|
|
1188
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
1189
|
+
inline-size: 100%;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
[data-ui='upload'] [data-part='viewerHeader'] {
|
|
1193
|
+
align-items: center;
|
|
1194
|
+
border-block-end: var(--_border-width) solid var(--slotted-border-subtle);
|
|
1195
|
+
column-gap: var(--_gap);
|
|
1196
|
+
display: grid;
|
|
1197
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
1198
|
+
padding-block: var(--slotted-space-4, 0.5rem);
|
|
1199
|
+
padding-inline: var(--slotted-space-6, 1rem);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
[data-ui='upload'] [data-part='viewerTitle'] {
|
|
1203
|
+
font-size: var(--slotted-font-size-md, 0.875rem);
|
|
1204
|
+
line-height: var(--_leading);
|
|
1205
|
+
font-weight: var(--slotted-font-weight-semibold, 600);
|
|
1206
|
+
margin: 0;
|
|
1207
|
+
overflow: hidden;
|
|
1208
|
+
text-overflow: ellipsis;
|
|
1209
|
+
white-space: nowrap;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
[data-ui='upload'] [data-part='viewerMeta'] {
|
|
1213
|
+
color: var(--slotted-text-muted);
|
|
1214
|
+
font-size: var(--slotted-font-size-xs, 0.75rem);
|
|
1215
|
+
line-height: var(--_leading);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
[data-ui='upload'] [data-part='viewerActions'] {
|
|
1219
|
+
align-items: center;
|
|
1220
|
+
display: inline-flex;
|
|
1221
|
+
gap: var(--slotted-space-1, 0.125rem);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* The stage. A sunken ground rather than the page's own, because what stands
|
|
1226
|
+
* on it is the consumer's content and a photograph with a white border on a
|
|
1227
|
+
* white page has no edge at all.
|
|
1228
|
+
*/
|
|
1229
|
+
[data-ui='upload'] [data-part='viewerStage'] {
|
|
1230
|
+
background-color: var(--slotted-surface-sunken);
|
|
1231
|
+
display: grid;
|
|
1232
|
+
min-block-size: 0;
|
|
1233
|
+
/*
|
|
1234
|
+
* The stage clips. Read at 720 pixels without it, an SVG with a `viewBox`
|
|
1235
|
+
* and no intrinsic size filled the width, ran its height past the surface
|
|
1236
|
+
* and covered the header of the next one: a percentage maximum against an
|
|
1237
|
+
* auto height is not a maximum.
|
|
1238
|
+
*/
|
|
1239
|
+
overflow: hidden;
|
|
1240
|
+
padding: var(--slotted-space-6, 1rem);
|
|
1241
|
+
place-items: center;
|
|
1242
|
+
position: relative;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/*
|
|
1246
|
+
* The picture takes the whole stage and is contained inside it. It is
|
|
1247
|
+
* stretched into its grid area rather than given `block-size: 100%`, because
|
|
1248
|
+
* a percentage needs an area that is definite to resolve against and falls
|
|
1249
|
+
* back to the picture's own height where it is not: a portrait scan four
|
|
1250
|
+
* thousand pixels tall then grew downwards and out of the layer, which is what
|
|
1251
|
+
* a reader saw as an image running off the screen. `contain` is what keeps a
|
|
1252
|
+
* portrait scan and a wide dashcam frame both whole in the same box.
|
|
1253
|
+
*/
|
|
1254
|
+
[data-ui='upload'] [data-part='viewerStage'] > img,
|
|
1255
|
+
[data-ui='upload'] [data-part='viewerStage'] > iframe {
|
|
1256
|
+
align-self: stretch;
|
|
1257
|
+
block-size: auto;
|
|
1258
|
+
inline-size: auto;
|
|
1259
|
+
justify-self: stretch;
|
|
1260
|
+
min-block-size: 0;
|
|
1261
|
+
min-inline-size: 0;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
[data-ui='upload'] [data-part='viewerStage'] > img {
|
|
1265
|
+
object-fit: contain;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/*
|
|
1269
|
+
* A PDF frame carries no `sandbox` and a text frame does, and the asymmetry is
|
|
1270
|
+
* measured: Chrome's PDF viewer is blocked in a sandboxed frame either way,
|
|
1271
|
+
* and an HTML document served as `application/pdf` in an unsandboxed one runs
|
|
1272
|
+
* no script. Both bindings state it; it is written here because a reader of
|
|
1273
|
+
* this file will ask why one frame differs from the other.
|
|
1274
|
+
*/
|
|
1275
|
+
[data-ui='upload'] [data-part='viewerStage'] > iframe {
|
|
1276
|
+
background-color: var(--slotted-surface);
|
|
1277
|
+
border: 0;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/* Sized by the film rather than by the stage, and never past it. */
|
|
1281
|
+
[data-ui='upload'] [data-part='viewerStage'] > video,
|
|
1282
|
+
[data-ui='upload'] [data-part='viewerStage'] > audio {
|
|
1283
|
+
max-block-size: 100%;
|
|
1284
|
+
max-inline-size: 100%;
|
|
1285
|
+
min-block-size: 0;
|
|
1286
|
+
min-inline-size: 0;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* What a kind the viewer cannot render gets: its own tile at the size of the
|
|
1291
|
+
* stage, its name, and one press to the file. `.docx` and `.xlsx` are here
|
|
1292
|
+
* because a browser cannot draw them and drawing them needs a server or a
|
|
1293
|
+
* megabyte of engine — which is the honest answer rather than an empty box.
|
|
1294
|
+
*/
|
|
1295
|
+
[data-ui='upload'] [data-part='viewerStage'] > [data-part='preview'] {
|
|
1296
|
+
block-size: var(--slotted-upload-viewer-tile-size, 6rem);
|
|
1297
|
+
inline-size: var(--slotted-upload-viewer-tile-size, 6rem);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
[data-ui='upload'] [data-part='viewerStage'] > [data-part='preview'] > [data-ui='icon'] {
|
|
1301
|
+
--slotted-icon-size: calc(3 * var(--_glyph-size));
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/*
|
|
1305
|
+
* The one sentence that says what is different about this file. It is a label
|
|
1306
|
+
* the defaults provider holds, so an application translates it once; the name
|
|
1307
|
+
* and the way out are in the header, where they are for every other file.
|
|
1308
|
+
*/
|
|
1309
|
+
[data-ui='upload'] [data-part='viewerStage'] > p {
|
|
1310
|
+
color: var(--slotted-text-muted);
|
|
1311
|
+
margin: 0;
|
|
1312
|
+
max-inline-size: 28ch;
|
|
1313
|
+
text-align: center;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/*
|
|
1317
|
+
* The unsupported stage is a column rather than a grid. Read at 720 pixels as
|
|
1318
|
+
* a grid, the tile and its sentence landed in two cells that centred against
|
|
1319
|
+
* different axes and sat diagonally apart; a column of two things centred is
|
|
1320
|
+
* one rule with one meaning.
|
|
1321
|
+
*/
|
|
1322
|
+
[data-ui='upload'] [data-part='viewerStage']:has(> [data-part='preview']) {
|
|
1323
|
+
align-items: center;
|
|
1324
|
+
display: flex;
|
|
1325
|
+
flex-direction: column;
|
|
1326
|
+
gap: var(--slotted-space-5, 0.75rem);
|
|
1327
|
+
justify-content: center;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* The two that move through the set. They sit over the stage rather than in
|
|
1332
|
+
* the header, where the reader's pointer already is, and they are held off
|
|
1333
|
+
* the edge so a photograph that fills the stage is not covered at its corner.
|
|
1334
|
+
*/
|
|
1335
|
+
[data-ui='upload'] [data-part='viewerPrevious'],
|
|
1336
|
+
[data-ui='upload'] [data-part='viewerNext'] {
|
|
1337
|
+
align-items: center;
|
|
1338
|
+
appearance: none;
|
|
1339
|
+
background-color: color-mix(
|
|
1340
|
+
in oklab,
|
|
1341
|
+
var(--slotted-surface) var(--slotted-upload-viewer-control-opacity, 92%),
|
|
1342
|
+
transparent
|
|
1343
|
+
);
|
|
1344
|
+
block-size: var(--slotted-upload-viewer-control-size, 2.5rem);
|
|
1345
|
+
border: 0;
|
|
1346
|
+
border-radius: var(--slotted-radius-full, 9999px);
|
|
1347
|
+
box-shadow: var(--slotted-shadow-sm);
|
|
1348
|
+
color: var(--slotted-text);
|
|
1349
|
+
cursor: pointer;
|
|
1350
|
+
display: inline-flex;
|
|
1351
|
+
inline-size: var(--slotted-upload-viewer-control-size, 2.5rem);
|
|
1352
|
+
justify-content: center;
|
|
1353
|
+
padding: 0;
|
|
1354
|
+
position: absolute;
|
|
1355
|
+
transition:
|
|
1356
|
+
background-color var(--slotted-control-transition-duration)
|
|
1357
|
+
var(--slotted-control-transition-easing),
|
|
1358
|
+
translate var(--slotted-duration-instant) var(--slotted-control-transition-easing);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
[data-ui='upload'] [data-part='viewerPrevious'] {
|
|
1362
|
+
inset-inline-start: var(--slotted-space-5, 0.75rem);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
[data-ui='upload'] [data-part='viewerNext'] {
|
|
1366
|
+
inset-inline-end: var(--slotted-space-5, 0.75rem);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* One mark for both, turned for the one that goes back — which is the rule
|
|
1371
|
+
* every chevron in this library follows, and the reason the scale holds one
|
|
1372
|
+
* chevron rather than a pair. The binding marks both icons `directional`, so
|
|
1373
|
+
* they mirror where reading runs right to left.
|
|
1374
|
+
*/
|
|
1375
|
+
[data-ui='upload'] [data-part='viewerPrevious'] > [data-ui='icon'],
|
|
1376
|
+
[data-ui='upload'] [data-part='viewerNext'] > [data-ui='icon'] {
|
|
1377
|
+
--slotted-icon-size: var(--_glyph-size);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
[data-ui='upload'] [data-part='viewerPrevious'] > [data-ui='icon'] {
|
|
1381
|
+
rotate: 180deg;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
/*
|
|
1385
|
+
* The plate is already nearly opaque, so going to fully opaque on hover moved
|
|
1386
|
+
* the ground by 0.002 in OKLab L — no answer at all. The family's state layer
|
|
1387
|
+
* is mixed into the plate instead, which keeps it opaque over the photograph
|
|
1388
|
+
* and steps it by the amount every other control in the family steps.
|
|
1389
|
+
*/
|
|
1390
|
+
[data-ui='upload'] [data-part='viewerPrevious']:enabled:hover,
|
|
1391
|
+
[data-ui='upload'] [data-part='viewerNext']:enabled:hover {
|
|
1392
|
+
background-color: color-mix(
|
|
1393
|
+
in oklab,
|
|
1394
|
+
currentcolor var(--slotted-upload-state-layer-hover, 8%),
|
|
1395
|
+
var(--slotted-surface)
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
[data-ui='upload'] [data-part='viewerPrevious']:enabled:active,
|
|
1400
|
+
[data-ui='upload'] [data-part='viewerNext']:enabled:active {
|
|
1401
|
+
background-color: color-mix(
|
|
1402
|
+
in oklab,
|
|
1403
|
+
currentcolor var(--slotted-upload-state-layer-active, 14%),
|
|
1404
|
+
var(--slotted-surface)
|
|
1405
|
+
);
|
|
1406
|
+
translate: 0 var(--slotted-border-width-thin, 1px);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
[data-ui='upload'] [data-part='viewerPrevious']:disabled,
|
|
1410
|
+
[data-ui='upload'] [data-part='viewerNext']:disabled {
|
|
1411
|
+
color: var(--slotted-disabled-foreground);
|
|
1412
|
+
cursor: default;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
[data-ui='upload'] [data-part='viewerPrevious']:focus-visible,
|
|
1416
|
+
[data-ui='upload'] [data-part='viewerNext']:focus-visible {
|
|
1417
|
+
outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
|
|
1418
|
+
outline-offset: var(--slotted-focus-ring-offset, 2px);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/* ── Inside a field ────────────────────────────────────────────────────
|
|
1422
|
+
*
|
|
1423
|
+
* A field owns the label row, the description, the necessity mark and the
|
|
1424
|
+
* error slot for every control in this library, so an upload under one draws
|
|
1425
|
+
* none of its own: two labels on one control and two messages under it is
|
|
1426
|
+
* what a form gets otherwise, and the second of each is the one no other
|
|
1427
|
+
* control in that form has.
|
|
1428
|
+
*
|
|
1429
|
+
* The rule is written here as well as in both bindings, because the harness
|
|
1430
|
+
* is a third place the DOM is written and a scene that puts an upload in a
|
|
1431
|
+
* field has to look like the form does.
|
|
1432
|
+
*/
|
|
1433
|
+
[data-ui='field'] [data-ui='upload'] > [data-part='label'],
|
|
1434
|
+
[data-ui='field'] [data-ui='upload'] > [data-part='description'],
|
|
1435
|
+
[data-ui='field'] [data-ui='upload'] > [data-part='message'] {
|
|
1436
|
+
display: none;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* A form is a column of rows the same height, and an upload in it must not be
|
|
1441
|
+
* the one that pushes the rhythm out. Inside a field the sections close up by
|
|
1442
|
+
* one step: the region, the summary and the list are one block rather than
|
|
1443
|
+
* three things with a paragraph's air between them.
|
|
1444
|
+
*/
|
|
1445
|
+
[data-ui='field'] [data-ui='upload'][data-part='root'] {
|
|
1446
|
+
--_section-gap: var(--slotted-upload-field-section-gap, var(--slotted-space-4, 0.5rem));
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/* ── The bare trigger ──────────────────────────────────────────────────
|
|
1450
|
+
*
|
|
1451
|
+
* No region at all. The trigger still takes a drop and a paste, because it
|
|
1452
|
+
* is still a target; what it does not do is claim a box the size of a card
|
|
1453
|
+
* for an application that wanted a button.
|
|
1454
|
+
*/
|
|
1455
|
+
[data-ui='upload'][data-variant='button'] [data-part='trigger'] {
|
|
1456
|
+
align-self: start;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/* ── The field shape ───────────────────────────────────────────────────
|
|
1460
|
+
*
|
|
1461
|
+
* One control-height row: the trigger at the inline start and what has been
|
|
1462
|
+
* chosen beside it, inside the frame a text field would have. This is the
|
|
1463
|
+
* shape `file-input` was, and the reason it is a variant rather than a
|
|
1464
|
+
* family is that everything under the frame is the same list.
|
|
1465
|
+
*/
|
|
1466
|
+
/*
|
|
1467
|
+
* The frame a text field has, at rest. It moves the region's resting pair
|
|
1468
|
+
* rather than painting over it, so the family's states — invalid, disabled,
|
|
1469
|
+
* the drag verdict, a pointer — reach this shape as they reach the other:
|
|
1470
|
+
* written as a border colour on the region, it held one colour through all
|
|
1471
|
+
* of them. `:where` keeps it below the invalid and disabled pair.
|
|
1472
|
+
*/
|
|
1473
|
+
[data-ui='upload'][data-variant='field']:where([data-part='root']) {
|
|
1474
|
+
--_zone-edge: var(--slotted-control-border-color);
|
|
1475
|
+
--_zone-ground: var(--slotted-control-surface, Field);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
[data-ui='upload'][data-variant='field'] [data-part='dropzone'] {
|
|
1479
|
+
align-items: center;
|
|
1480
|
+
border-style: solid;
|
|
1481
|
+
border-width: var(--slotted-control-border-width, 1px);
|
|
1482
|
+
flex-direction: row;
|
|
1483
|
+
gap: var(--_gap);
|
|
1484
|
+
padding-block: var(--slotted-space-2, 0.25rem);
|
|
1485
|
+
padding-inline: var(--slotted-space-2, 0.25rem);
|
|
1486
|
+
text-align: start;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
/* ── A wall of pictures ────────────────────────────────────────────────
|
|
1490
|
+
*
|
|
1491
|
+
* The same rows, arranged. An image set is read by looking rather than by
|
|
1492
|
+
* reading, so the preview takes the card and the name sits under it; the
|
|
1493
|
+
* controls move to the corner of the picture, where they are over the one
|
|
1494
|
+
* thing the reader is pointing at.
|
|
1495
|
+
*/
|
|
1496
|
+
[data-ui='upload'][data-layout='grid'] [data-part='list'] {
|
|
1497
|
+
display: grid;
|
|
1498
|
+
gap: var(--_gap);
|
|
1499
|
+
grid-template-columns: repeat(auto-fill, minmax(var(--slotted-upload-card-measure, 9rem), 1fr));
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
[data-ui='upload'][data-layout='grid'] [data-part='item'] {
|
|
1503
|
+
/*
|
|
1504
|
+
* Cards in one row share its height, and the spare height of the shorter
|
|
1505
|
+
* ones goes under their content rather than between the picture and the
|
|
1506
|
+
* name: without this, a card whose neighbour carried a two-line message
|
|
1507
|
+
* set its name lower than the name beside it.
|
|
1508
|
+
*/
|
|
1509
|
+
align-content: start;
|
|
1510
|
+
align-items: stretch;
|
|
1511
|
+
/* So the picture can be sized from the card's own width; see `preview`. */
|
|
1512
|
+
container-type: inline-size;
|
|
1513
|
+
grid-template-areas:
|
|
1514
|
+
'preview'
|
|
1515
|
+
'body'
|
|
1516
|
+
'message';
|
|
1517
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
/*
|
|
1521
|
+
* Four by three, rounded down to a whole pixel. The card's width is a share
|
|
1522
|
+
* of the row and is fractional, and `aspect-ratio` carried the fraction into
|
|
1523
|
+
* the height: 117.66 at 720 pixels, with every edge under it blurred. The
|
|
1524
|
+
* card is a container, so `cqi` is the width the picture fills.
|
|
1525
|
+
*/
|
|
1526
|
+
[data-ui='upload'][data-layout='grid'] [data-part='preview'] {
|
|
1527
|
+
block-size: round(down, calc(100cqi * 3 / 4), 1px);
|
|
1528
|
+
inline-size: 100%;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* Over the picture, the controls need a ground of their own: the consumer's
|
|
1533
|
+
* photograph is any colour, and a dark mark on a mid-blue sky was the reading
|
|
1534
|
+
* the first capture at 720 pixels gave. The ground is on the group rather than
|
|
1535
|
+
* on each control, so the five keep the frameless rest state R2 requires of
|
|
1536
|
+
* them and the reader still gets one legible plate.
|
|
1537
|
+
*/
|
|
1538
|
+
/**
|
|
1539
|
+
* Over the picture, and inside it. Three things the first capture at 720
|
|
1540
|
+
* pixels got wrong and this rule fixes: the plate stood at the item's own
|
|
1541
|
+
* edge rather than inside the photograph, so it read as stuck to the corner;
|
|
1542
|
+
* it was translucent enough for a bright sky to come through the marks; and
|
|
1543
|
+
* it reserved three slots it did not need, which pushed it off the picture.
|
|
1544
|
+
*
|
|
1545
|
+
* The ground is on the group rather than on each control, so the five keep
|
|
1546
|
+
* the frameless rest state R2 requires of them and the reader still gets one
|
|
1547
|
+
* legible plate.
|
|
1548
|
+
*/
|
|
1549
|
+
[data-ui='upload'][data-layout='grid'] [data-part='actions'] {
|
|
1550
|
+
background-color: color-mix(
|
|
1551
|
+
in oklab,
|
|
1552
|
+
var(--slotted-surface) var(--slotted-upload-card-actions-opacity, 96%),
|
|
1553
|
+
transparent
|
|
1554
|
+
);
|
|
1555
|
+
border-radius: var(--_item-radius);
|
|
1556
|
+
box-shadow: var(--slotted-shadow-xs);
|
|
1557
|
+
inset-block-start: calc(var(--_item-inset-block) + var(--slotted-space-3, 0.375rem));
|
|
1558
|
+
inset-inline-end: calc(var(--_item-inset-inline) + var(--slotted-space-3, 0.375rem));
|
|
1559
|
+
margin-block-start: 0;
|
|
1560
|
+
min-inline-size: 0;
|
|
1561
|
+
padding: var(--slotted-space-1, 0.125rem);
|
|
1562
|
+
position: absolute;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/**
|
|
1566
|
+
* A card has room for the extension in words, and a reader scanning a wall of
|
|
1567
|
+
* thumbnails for the one PDF among nine photographs finds it faster by the
|
|
1568
|
+
* three letters than by the silhouette. It is drawn from the attribute the
|
|
1569
|
+
* binding already writes, so the label costs no element.
|
|
1570
|
+
*/
|
|
1571
|
+
[data-ui='upload'][data-layout='grid'] [data-part='preview']:not(:has(img))::after {
|
|
1572
|
+
content: attr(data-extension);
|
|
1573
|
+
font-size: var(--slotted-font-size-sm, 0.8125rem);
|
|
1574
|
+
font-weight: var(--slotted-font-weight-semibold, 600);
|
|
1575
|
+
letter-spacing: 0.06em;
|
|
1576
|
+
margin-block-start: var(--slotted-space-3, 0.375rem);
|
|
1577
|
+
text-transform: uppercase;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/* A card's tile carries a mark the size of the card, not the size of a row. */
|
|
1581
|
+
[data-ui='upload'][data-layout='grid'] [data-part='preview'] > [data-ui='icon'] {
|
|
1582
|
+
--slotted-icon-size: calc(2 * var(--_glyph-size));
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/* ── Composition that answers the box, not the window ──────────────────
|
|
1586
|
+
*
|
|
1587
|
+
* Below 26rem the row has no room for a name, a size and four controls on
|
|
1588
|
+
* one line, so the size leaves the name's line and joins the status under
|
|
1589
|
+
* it, and the region's mark stops asking for a column of its own.
|
|
1590
|
+
*/
|
|
1591
|
+
/**
|
|
1592
|
+
* The summary is a sentence and a control, and the sentence is the longer of
|
|
1593
|
+
* the two by a factor of ten. It stacks a step earlier than everything else
|
|
1594
|
+
* below, because at 420 pixels the row still fitted and the control's two
|
|
1595
|
+
* words broke across two lines with the underline broken between them.
|
|
1596
|
+
*/
|
|
1597
|
+
@container slotted-upload (max-width: 30rem) {
|
|
1598
|
+
[data-ui='upload'] [data-part='summary'] {
|
|
1599
|
+
align-items: start;
|
|
1600
|
+
flex-direction: column;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
@container slotted-upload (max-width: 26rem) {
|
|
1605
|
+
/*
|
|
1606
|
+
* The chip keeps the start of the row and the controls drop under the text,
|
|
1607
|
+
* because at this width three of them beside a name leave the name seven
|
|
1608
|
+
* characters. The chip spans both rows, so the block it is centred on is
|
|
1609
|
+
* still the block it is about.
|
|
1610
|
+
*/
|
|
1611
|
+
[data-ui='upload'] [data-part='item'] {
|
|
1612
|
+
grid-template-areas:
|
|
1613
|
+
'preview body'
|
|
1614
|
+
'preview actions'
|
|
1615
|
+
'message message';
|
|
1616
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
[data-ui='upload'] [data-part='preview'] {
|
|
1620
|
+
align-self: start;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
[data-ui='upload'] [data-part='actions'] {
|
|
1624
|
+
justify-content: flex-start;
|
|
1625
|
+
margin-inline-start: calc(-1 * var(--slotted-space-2, 0.25rem));
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
[data-ui='upload'] [data-part='dropzone'] {
|
|
1629
|
+
padding-block: var(--slotted-space-6, 1rem);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
/* ── Forced colours ────────────────────────────────────────────────────
|
|
1634
|
+
*
|
|
1635
|
+
* The theme remaps every semantic token to a system colour, so a family that
|
|
1636
|
+
* reads tokens needs no rule of its own for colour. What it does need is the
|
|
1637
|
+
* region's dashed edge, whose colour comes from a token that becomes
|
|
1638
|
+
* `CanvasText` — correct — and the row tint, which the system flattens to
|
|
1639
|
+
* `Canvas` and which then leaves the failed row saying nothing. The border
|
|
1640
|
+
* carries it there instead.
|
|
1641
|
+
*/
|
|
1642
|
+
@media (forced-colors: active) {
|
|
1643
|
+
[data-ui='upload'] [data-part='item'][data-status='rejected'],
|
|
1644
|
+
[data-ui='upload'] [data-part='item'][data-status='error'] {
|
|
1645
|
+
--_item-border: CanvasText;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/*
|
|
1649
|
+
* The theme maps every kind to `ButtonFace` and the foreground to
|
|
1650
|
+
* `ButtonText`, so the tile is still a tile and the mark is still knocked
|
|
1651
|
+
* out of it — in the system's two colours rather than in nine. What kind a
|
|
1652
|
+
* file is, is then said by its shape, which is what survives this mode.
|
|
1653
|
+
*/
|
|
1654
|
+
[data-ui='upload'] [data-part='preview'] {
|
|
1655
|
+
border: var(--_border-width) solid ButtonBorder;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
[data-ui='upload'] [data-part='dropzone'][data-drop-target],
|
|
1659
|
+
[data-ui='upload'] [data-part='dropzone'][data-drop-invalid] {
|
|
1660
|
+
--_zone-border-color: Highlight;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/*
|
|
1664
|
+
* The state layer is a mix of `currentcolor` into nothing, and the system
|
|
1665
|
+
* paints every ground `Canvas`, so a pointer over a control changed nothing
|
|
1666
|
+
* the reader could see. The system's own pair answers instead — the one
|
|
1667
|
+
* `alert` gives its dismiss.
|
|
1668
|
+
*/
|
|
1669
|
+
[data-ui='upload']
|
|
1670
|
+
:is(
|
|
1671
|
+
[data-part='clear'],
|
|
1672
|
+
[data-part='viewerExternal'],
|
|
1673
|
+
[data-part='viewerClose'],
|
|
1674
|
+
[data-part='viewerPrevious'],
|
|
1675
|
+
[data-part='viewerNext'],
|
|
1676
|
+
[data-part='start'],
|
|
1677
|
+
[data-part='pause'],
|
|
1678
|
+
[data-part='retry'],
|
|
1679
|
+
[data-part='cancel'],
|
|
1680
|
+
[data-part='remove']
|
|
1681
|
+
):enabled:is(:hover, :active) {
|
|
1682
|
+
background-color: Highlight;
|
|
1683
|
+
color: HighlightText;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
/*
|
|
1688
|
+
* Forced colours remove every box shadow, and the edge with them. The width
|
|
1689
|
+
* comes back as an outline inside the border, and where focus draws the ring
|
|
1690
|
+
* on the same box an invalid ring is double, which a valid one is not.
|
|
1691
|
+
*/
|
|
1692
|
+
@media (forced-colors: active) {
|
|
1693
|
+
[data-ui='upload'][data-invalid] [data-part='trigger'] {
|
|
1694
|
+
outline: var(--_invalid-edge-width) solid CanvasText;
|
|
1695
|
+
outline-offset: calc(
|
|
1696
|
+
-1 * (var(--slotted-control-border-width, 1px) + var(--_invalid-edge-width))
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
[data-ui='upload'][data-invalid] [data-part='trigger']:has(:focus-visible) {
|
|
1701
|
+
outline: calc(var(--slotted-focus-ring-width, 2px) + var(--_invalid-edge-width)) double
|
|
1702
|
+
var(--slotted-focus-ring-color, Highlight);
|
|
1703
|
+
outline-offset: var(--slotted-focus-ring-offset, 2px);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|