@mystravil/hardpoint 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +25 -0
  3. package/README.md +64 -3
  4. package/dist/base.css +346 -0
  5. package/dist/components/AppShell.svelte +698 -0
  6. package/dist/components/AppShell.svelte.d.ts +77 -0
  7. package/dist/components/Button.svelte +174 -0
  8. package/dist/components/Button.svelte.d.ts +20 -0
  9. package/dist/components/Checkbox.svelte +198 -0
  10. package/dist/components/Checkbox.svelte.d.ts +16 -0
  11. package/dist/components/CheckboxGroup.svelte +99 -0
  12. package/dist/components/CheckboxGroup.svelte.d.ts +25 -0
  13. package/dist/components/Combobox.svelte +824 -0
  14. package/dist/components/Combobox.svelte.d.ts +63 -0
  15. package/dist/components/Dialog.svelte +356 -0
  16. package/dist/components/Dialog.svelte.d.ts +43 -0
  17. package/dist/components/Disclosure.svelte +201 -0
  18. package/dist/components/Disclosure.svelte.d.ts +27 -0
  19. package/dist/components/EmptyState.svelte +88 -0
  20. package/dist/components/EmptyState.svelte.d.ts +22 -0
  21. package/dist/components/ErrorState.svelte +135 -0
  22. package/dist/components/ErrorState.svelte.d.ts +26 -0
  23. package/dist/components/KeyValueList.svelte +114 -0
  24. package/dist/components/KeyValueList.svelte.d.ts +43 -0
  25. package/dist/components/LoadRegion.svelte +219 -0
  26. package/dist/components/LoadRegion.svelte.d.ts +27 -0
  27. package/dist/components/MediaFrame.svelte +110 -0
  28. package/dist/components/MediaFrame.svelte.d.ts +20 -0
  29. package/dist/components/MediaImage.svelte +256 -0
  30. package/dist/components/MediaImage.svelte.d.ts +16 -0
  31. package/dist/components/Menu.svelte +258 -0
  32. package/dist/components/Menu.svelte.d.ts +36 -0
  33. package/dist/components/MenuCheckboxItem.svelte +119 -0
  34. package/dist/components/MenuCheckboxItem.svelte.d.ts +15 -0
  35. package/dist/components/MenuGroup.svelte +50 -0
  36. package/dist/components/MenuGroup.svelte.d.ts +15 -0
  37. package/dist/components/MenuItem.svelte +94 -0
  38. package/dist/components/MenuItem.svelte.d.ts +19 -0
  39. package/dist/components/MenuRadioGroup.svelte +62 -0
  40. package/dist/components/MenuRadioGroup.svelte.d.ts +20 -0
  41. package/dist/components/MenuRadioItem.svelte +110 -0
  42. package/dist/components/MenuRadioItem.svelte.d.ts +17 -0
  43. package/dist/components/MenuSeparator.svelte +28 -0
  44. package/dist/components/MenuSeparator.svelte.d.ts +6 -0
  45. package/dist/components/NativeSelect.svelte +119 -0
  46. package/dist/components/NativeSelect.svelte.d.ts +46 -0
  47. package/dist/components/Pagination.svelte +132 -0
  48. package/dist/components/Pagination.svelte.d.ts +18 -0
  49. package/dist/components/Popover.svelte +222 -0
  50. package/dist/components/Popover.svelte.d.ts +35 -0
  51. package/dist/components/Progress.svelte +157 -0
  52. package/dist/components/Progress.svelte.d.ts +34 -0
  53. package/dist/components/Select.svelte +405 -0
  54. package/dist/components/Select.svelte.d.ts +42 -0
  55. package/dist/components/Skeleton.svelte +149 -0
  56. package/dist/components/Skeleton.svelte.d.ts +24 -0
  57. package/dist/components/Spinner.svelte +117 -0
  58. package/dist/components/Spinner.svelte.d.ts +13 -0
  59. package/dist/components/Stat.svelte +253 -0
  60. package/dist/components/Stat.svelte.d.ts +46 -0
  61. package/dist/components/Steps.svelte +189 -0
  62. package/dist/components/Steps.svelte.d.ts +31 -0
  63. package/dist/components/Switch.svelte +222 -0
  64. package/dist/components/Switch.svelte.d.ts +18 -0
  65. package/dist/components/Table.svelte +504 -0
  66. package/dist/components/Table.svelte.d.ts +92 -0
  67. package/dist/components/Tabs.svelte +246 -0
  68. package/dist/components/Tabs.svelte.d.ts +44 -0
  69. package/dist/components/Tag.svelte +40 -0
  70. package/dist/components/Tag.svelte.d.ts +15 -0
  71. package/dist/components/TextField.svelte +256 -0
  72. package/dist/components/TextField.svelte.d.ts +32 -0
  73. package/dist/components/Timeline.svelte +156 -0
  74. package/dist/components/Timeline.svelte.d.ts +30 -0
  75. package/dist/components/Toaster.svelte +411 -0
  76. package/dist/components/Toaster.svelte.d.ts +11 -0
  77. package/dist/components/Tooltip.svelte +200 -0
  78. package/dist/components/Tooltip.svelte.d.ts +38 -0
  79. package/dist/components/TooltipGroup.svelte +46 -0
  80. package/dist/components/TooltipGroup.svelte.d.ts +29 -0
  81. package/dist/components/internal/Scrim.svelte +52 -0
  82. package/dist/components/internal/Scrim.svelte.d.ts +15 -0
  83. package/dist/components/internal/Sheet.svelte +263 -0
  84. package/dist/components/internal/Sheet.svelte.d.ts +36 -0
  85. package/dist/components/internal/fade-in.d.ts +10 -0
  86. package/dist/components/internal/fade-in.js +20 -0
  87. package/dist/components/internal/resolve.d.ts +8 -0
  88. package/dist/components/internal/resolve.js +247 -0
  89. package/dist/components/internal/swipe-dismiss.d.ts +47 -0
  90. package/dist/components/internal/swipe-dismiss.js +220 -0
  91. package/dist/components/internal/visual-viewport.svelte.d.ts +35 -0
  92. package/dist/components/internal/visual-viewport.svelte.js +79 -0
  93. package/dist/components/toast.svelte.d.ts +57 -0
  94. package/dist/components/toast.svelte.js +84 -0
  95. package/dist/fonts/OFL-geist-pixel.txt +92 -0
  96. package/dist/fonts/OFL.txt +93 -0
  97. package/dist/fonts/geist-latin-variable.woff2 +0 -0
  98. package/dist/fonts/geist-mono-latin-variable.woff2 +0 -0
  99. package/dist/fonts/geist-pixel-square.woff2 +0 -0
  100. package/dist/fonts.css +71 -0
  101. package/dist/index.d.ts +39 -0
  102. package/dist/index.js +39 -0
  103. package/dist/motion/Motion.svelte +15 -0
  104. package/dist/motion/Motion.svelte.d.ts +9 -0
  105. package/dist/motion/context.d.ts +18 -0
  106. package/dist/motion/context.js +26 -0
  107. package/dist/motion/index.d.ts +3 -0
  108. package/dist/motion/index.js +3 -0
  109. package/dist/motion/transitions.d.ts +31 -0
  110. package/dist/motion/transitions.js +112 -0
  111. package/dist/tokens.css +279 -0
  112. package/package.json +76 -6
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ Hardpoint follows semantic versioning. Before 1.0.0, a breaking change raises the minor version.
4
+
5
+ ## 0.1.0 (2026-11-11)
6
+
7
+ The first public release. It needs Svelte 5.57 or newer.
8
+
9
+ - **Components** (Svelte 5 on Bits UI):
10
+ - Actions and forms: Button, TextField, NativeSelect, Select, Combobox, Checkbox, CheckboxGroup, Switch.
11
+ - Overlays: Dialog (centred, bottom sheet or full screen), Popover, the Menu family, Tooltip with TooltipGroup, and Toaster with `toast()`.
12
+ - Navigation: AppShell (header, bottom bar or drawer, rail or sidebar), Tabs, Steps, Pagination.
13
+ - Data and display: Table, KeyValueList, Stat, Tag, Timeline, MediaFrame with MediaImage, Disclosure.
14
+ - Feedback: Progress, Spinner, Skeleton, LoadRegion, EmptyState, ErrorState.
15
+ - **Motion** (`@mystravil/hardpoint/motion`): `<Motion tier>` and `useMotion()`, the overlay transitions `pop`, `sheet`, `fade` and `drawer`, and timing tokens for every duration. Quiet motion is the default; expressive effects run only inside `<Motion tier="expressive">` and never under reduced motion.
16
+ - **CSS**, framework-free: `fonts.css` (Geist, Geist Mono and Geist Pixel Square with size-matched fallback faces), `tokens.css` and `base.css`.
17
+ - **Themes**: dark by default; light through `data-theme="light"` on `<html>`, or `data-theme="system"` to follow the device.
18
+ - **Phones**: 44 px touch targets on coarse pointers, 16 px inputs, bottom sheets on compact widths that stay above the on-screen keyboard, and safe-area insets.
package/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MYSTRAVIL
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ The fonts in src/lib/fonts (shipped as dist/fonts) are not covered by this
24
+ licence. Geist, Geist Mono and Geist Pixel Square are licensed under the SIL
25
+ Open Font License 1.1; see OFL.txt and OFL-geist-pixel.txt beside them.
package/README.md CHANGED
@@ -1,3 +1,64 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # Hardpoint
2
+
3
+ Hardpoint is a Svelte 5 design system on [Bits UI](https://www.bits-ui.com/). Its look and motion come from Dan Neuenhaus's MONOLITH reel, adapted for ordinary apps: AMOLED black, titanium ink, the Geist type family, square solid blocks for actions and tags, banded tables of mono data, and expressive motion only where an app asks for it. Bits UI supplies the behaviour and accessibility; Hardpoint supplies the look and the motion.
4
+
5
+ It works on desktop and on phones: 44 px touch targets on coarse pointers, 16 px inputs, bottom sheets on compact widths that stay above the on-screen keyboard, and safe-area insets. Dark is the default theme, and a light theme is built in. Status colours (red, yellow, green) mean status only; no hue is a brand colour.
6
+
7
+ The package imports nothing from SvelteKit, so plain Svelte apps can use it. The three CSS files are framework-free, so apps without Svelte can use the tokens and base styles.
8
+
9
+ ## Components
10
+
11
+ - Actions and forms: `Button`, `TextField`, `NativeSelect`, `Select`, `Combobox`, `Checkbox`, `CheckboxGroup`, `Switch`.
12
+ - Overlays: `Dialog` (centred, bottom sheet or full screen), `Popover`, `Menu` with `MenuItem`, `MenuCheckboxItem`, `MenuRadioGroup`, `MenuRadioItem`, `MenuGroup` and `MenuSeparator`, `Tooltip` with `TooltipGroup`, and `Toaster` with `toast()`.
13
+ - Navigation: `AppShell` (header, bottom bar or drawer, rail or sidebar), `Tabs`, `Steps`, `Pagination`.
14
+ - Data and display: `Table`, `KeyValueList`, `Stat`, `Tag`, `Timeline`, `MediaFrame` with `MediaImage`, `Disclosure`.
15
+ - Feedback: `Progress`, `Spinner`, `Skeleton`, `LoadRegion`, `EmptyState`, `ErrorState`.
16
+
17
+ The source repository has a showcase app with a docs page for each component, the foundations, the motion tokens and a specimen app.
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ npm install @mystravil/hardpoint
23
+ ```
24
+
25
+ Svelte 5.57 or newer is a peer dependency. Bits UI comes with the package.
26
+
27
+ ## Use
28
+
29
+ ```js
30
+ import '@mystravil/hardpoint/fonts.css';
31
+ import '@mystravil/hardpoint/tokens.css';
32
+ import '@mystravil/hardpoint/base.css';
33
+
34
+ // Svelte components
35
+ import { Button, Dialog, Table, Pagination, TextField, toast } from '@mystravil/hardpoint';
36
+ // Motion: <Motion tier="expressive"> for infrequent moments, and the overlay transitions
37
+ import { Motion, useMotion, pop, sheet, fade, drawer } from '@mystravil/hardpoint/motion';
38
+ ```
39
+
40
+ Skip `fonts.css` if the app already serves Geist.
41
+
42
+ The theme is an attribute on `<html>`: `data-theme="light"`, `"dark"` or `"system"` (follows the device). Without it the theme is dark. Set it before first paint, and set `<meta name="theme-color">` to the canvas colour, `#000000` for dark and `#f7f5f0` for light.
43
+
44
+ ## Docs
45
+
46
+ The design docs are in the source repository's `docs/` folder, which is private: `BRIEF.md` is the spec, with every decision and its reason; `ux-research.md` holds the UI/UX research and standards behind the app and phone rules; `elements-da3.md`, `measurements-da3.md` and `reference/da3/` measure the reference reel. `CHANGELOG.md` in the package lists the changes in each version.
47
+
48
+ ## Develop
49
+
50
+ ```sh
51
+ npm install
52
+ npm run dev # showcase app in src/routes
53
+ npm run check # svelte-check, and no SvelteKit imports in src/lib
54
+ npm run build # showcase build, then svelte-package and publint into dist/
55
+ npm run audit # targets, overflow, 16px inputs, contrast on every route at four viewports, dark and light
56
+ npm run shot -- /foundations shots/f.png --serve # screenshot with console-error check
57
+ npm run shot -- /foundations shots/f-phone.png --w 390 --h 844 --touch --full --serve
58
+ ```
59
+
60
+ `src/lib` is the library. `src/routes` is the showcase.
61
+
62
+ ## Licence
63
+
64
+ The code is MIT licensed (`LICENSE`). The fonts are not: Geist, Geist Mono and Geist Pixel Square ship in `dist/fonts` under the SIL Open Font License 1.1 (`OFL.txt`, `OFL-geist-pixel.txt`).
package/dist/base.css ADDED
@@ -0,0 +1,346 @@
1
+ /* Hardpoint base. Framework-free like tokens.css: element defaults use
2
+ :where() so any app rule overrides them, and helpers carry an hp- prefix.
3
+ Import after tokens.css. */
4
+
5
+ /* ---- Document ---------------------------------------------------------- */
6
+
7
+ *,
8
+ *::before,
9
+ *::after {
10
+ box-sizing: border-box;
11
+ }
12
+
13
+ /* hidden="until-found" (Disclosure's closed content) stays out of this rule:
14
+ the browser hides it with content-visibility so find in page can search it. */
15
+ [hidden]:not([hidden='until-found' i]) {
16
+ display: none !important;
17
+ }
18
+
19
+ html {
20
+ -webkit-text-size-adjust: 100%;
21
+ text-size-adjust: 100%;
22
+ scrollbar-color: var(--border) transparent;
23
+ }
24
+
25
+ body {
26
+ margin: 0;
27
+ background: var(--canvas);
28
+ color: var(--text);
29
+ font-family: var(--sans);
30
+ font-size: var(--type-body);
31
+ font-weight: 400;
32
+ line-height: 1.5;
33
+ -webkit-font-smoothing: antialiased;
34
+ -moz-osx-font-smoothing: grayscale;
35
+ text-rendering: optimizeLegibility;
36
+ }
37
+
38
+ :where(h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd) {
39
+ margin: 0;
40
+ }
41
+
42
+ :where(h1, h2, h3, h4, h5, h6) {
43
+ font-size: inherit;
44
+ font-weight: 500;
45
+ }
46
+
47
+ /* Line breaks. Reading text never ends on a lone word; headings and title
48
+ roles balance their lines (this rule comes second, so a title set on a
49
+ <p> balances). The longhand leaves the wrap mode alone, so a nowrap or
50
+ truncating parent still holds; browsers without it wrap as before. */
51
+ :where(p, li, dd, blockquote, figcaption) {
52
+ text-wrap-style: pretty;
53
+ }
54
+
55
+ :where(h1, h2, h3, h4, h5, h6, .hp-page-title, .hp-section-title, .hp-display) {
56
+ text-wrap-style: balance;
57
+ }
58
+
59
+ /* Reading measure: a paragraph stops at --measure, about 70 characters. */
60
+ :where(p) {
61
+ max-width: var(--measure);
62
+ }
63
+
64
+ /* Square corners everywhere: no cuts, no radii, also on native controls. */
65
+ :where(button, input, select, textarea) {
66
+ font: inherit;
67
+ letter-spacing: inherit;
68
+ color: inherit;
69
+ border-radius: 0;
70
+ }
71
+
72
+ :where(button, a, input, select, textarea, summary, label) {
73
+ touch-action: manipulation;
74
+ }
75
+
76
+ :where(img, svg, video, canvas) {
77
+ display: block;
78
+ max-width: 100%;
79
+ }
80
+
81
+ :where(a) {
82
+ color: inherit;
83
+ text-decoration-thickness: 1px;
84
+ text-underline-offset: 0.2em;
85
+ }
86
+
87
+ :where(code, kbd, samp, pre) {
88
+ font-family: var(--mono);
89
+ }
90
+
91
+ /* Inline code in running text moves to the next line whole, so a token
92
+ never splits at a hyphen or a space (--control-height, aria-label).
93
+ One wider than the line wraps inside its own box: at a hyphen or space
94
+ first, anywhere if it must, so it never overflows at 320px. Only direct
95
+ children of reading blocks: an atomic box in a truncating cell (a table
96
+ dd) would vanish whole into the ellipsis, and a link's underline does
97
+ not reach into one. */
98
+ :where(p, li, blockquote, figcaption) > :where(code, kbd, samp) {
99
+ display: inline-block;
100
+ overflow-wrap: anywhere;
101
+ }
102
+
103
+ ::selection {
104
+ background: var(--text);
105
+ color: var(--canvas);
106
+ }
107
+
108
+ /* ---- Focus ------------------------------------------------------------- */
109
+
110
+ /* A 2px titanium contour 4px outside the control, square like everything
111
+ else. Selection never looks like this: selection is an edge marker and a
112
+ tint. */
113
+ :where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
114
+ outline: 2px solid var(--focus);
115
+ outline-offset: 4px;
116
+ }
117
+
118
+ /* For rows and items clipped by a scroll container: draw the contour inside. */
119
+ .hp-focus-inset:focus-visible {
120
+ outline-offset: -2px;
121
+ }
122
+
123
+ /* ---- Type roles -------------------------------------------------------- */
124
+
125
+ /* Tracked caps for short fixed labels: buttons, tabs, tags, column headers,
126
+ field labels, key-value labels. Never for headings, list items, messages or
127
+ user data. */
128
+ .hp-label {
129
+ font-family: var(--sans);
130
+ font-size: var(--type-label);
131
+ font-weight: 500;
132
+ line-height: 16px;
133
+ letter-spacing: var(--label-tracking);
134
+ text-transform: var(--label-transform);
135
+ }
136
+
137
+ .hp-caption {
138
+ font-size: var(--type-caption);
139
+ line-height: 18px;
140
+ }
141
+
142
+ /* Dense UI on fine pointers. Phones keep 16px body text. */
143
+ .hp-dense {
144
+ font-size: var(--type-body);
145
+ line-height: 24px;
146
+ }
147
+
148
+ @media (pointer: fine) {
149
+ .hp-dense {
150
+ font-size: var(--type-dense);
151
+ line-height: 20px;
152
+ }
153
+ }
154
+
155
+ .hp-body {
156
+ font-size: var(--type-body);
157
+ line-height: 24px;
158
+ }
159
+
160
+ .hp-section-title {
161
+ font-size: var(--type-section);
162
+ font-weight: 500;
163
+ line-height: 28px;
164
+ letter-spacing: -0.01em;
165
+ }
166
+
167
+ .hp-page-title {
168
+ font-size: var(--type-page);
169
+ font-weight: 500;
170
+ line-height: 1.25;
171
+ letter-spacing: -0.015em;
172
+ }
173
+
174
+ /* The view title in the shell header: a fixed string the app defines, so it
175
+ takes tracked caps (20px, 16px on compact). */
176
+ .hp-view-title {
177
+ font-family: var(--sans);
178
+ font-size: var(--type-view);
179
+ font-weight: 500;
180
+ line-height: 24px;
181
+ letter-spacing: var(--label-tracking);
182
+ text-transform: var(--label-transform);
183
+ }
184
+
185
+ /* One per view, and only for the subject: an identifier (add .hp-mono) or a
186
+ key figure. Names a user wrote stay at the page-title size. */
187
+ .hp-display {
188
+ font-size: var(--type-display);
189
+ font-weight: 500;
190
+ line-height: round(up, 1.1em, 1px);
191
+ letter-spacing: -0.02em;
192
+ font-variant-numeric: tabular-nums;
193
+ }
194
+
195
+ /* Identifiers, hashes, timestamps, code. */
196
+ .hp-mono {
197
+ font-family: var(--mono);
198
+ }
199
+
200
+ /* Numbers in Geist that line up and do not change width as they tick. */
201
+ .hp-num {
202
+ font-variant-numeric: tabular-nums;
203
+ }
204
+
205
+ /* Geist Pixel Square: the wordmark and at most one short fixed display line
206
+ per screen. Never labels, values, body text, page titles or user data.
207
+ Its module lands on whole pixels only if its top edge does too: do not
208
+ centre it in a box whose free space is odd. */
209
+ .hp-pixel {
210
+ font-family: var(--pixel);
211
+ font-size: var(--type-pixel);
212
+ font-weight: 400;
213
+ line-height: var(--type-pixel-leading);
214
+ letter-spacing: 0;
215
+ }
216
+
217
+ .hp-pixel-lg {
218
+ font-family: var(--pixel);
219
+ font-size: var(--type-pixel-lg);
220
+ font-weight: 400;
221
+ line-height: var(--type-pixel-lg-leading);
222
+ letter-spacing: 0;
223
+ }
224
+
225
+ /* ---- Wells ------------------------------------------------------------- */
226
+
227
+ /* The input box: a black well with a 3:1 boundary, never a filled block,
228
+ because a filled select reads as a button (BRIEF, visual rule 2). Text
229
+ is 16px at every width, or iOS zooms on focus. Native selects, rich
230
+ select triggers and text fields share it. line-height: normal makes the
231
+ line box the glyph box, which the well centres: a text field already
232
+ centres its text that way, and with the inherited 24px line a select or
233
+ a trigger set its text 1px higher at every pixel ratio. */
234
+ .hp-well {
235
+ appearance: none;
236
+ box-sizing: border-box;
237
+ height: var(--control-height);
238
+ padding: 0 var(--space-3);
239
+ border: 1px solid var(--border-control);
240
+ border-radius: 0;
241
+ background-color: var(--canvas);
242
+ color: var(--text);
243
+ font-family: var(--sans);
244
+ font-size: var(--type-body);
245
+ line-height: normal;
246
+ text-overflow: ellipsis;
247
+ transition: background-color var(--dur-hover) var(--settle);
248
+ }
249
+
250
+ /* A chevron at the end of a select's well. Wrap the well in
251
+ <span class="hp-well-chevron">: the chevron is the wrapper's ::after,
252
+ because a <select> draws no pseudo-elements and a background image cannot
253
+ rotate. It turns to point up while the list is open, with the list's own
254
+ timing: --dur-pop opening, --dur-pop-exit closing. A rich select's trigger
255
+ says it is open with aria-expanded; a native select matches :open where
256
+ the browser supports it (Chromium 133+). The wrapper sets no width; the
257
+ component sizes it. */
258
+ .hp-well-chevron {
259
+ position: relative;
260
+ display: block;
261
+ }
262
+ .hp-well-chevron > .hp-well {
263
+ padding-right: var(--space-10);
264
+ }
265
+ .hp-well-chevron::after {
266
+ content: '';
267
+ position: absolute;
268
+ top: 50%;
269
+ /* The well's 1px border, plus the inset a background image had. */
270
+ right: calc(var(--space-3) + 1px);
271
+ width: 16px;
272
+ height: 16px;
273
+ margin-top: -8px;
274
+ background-color: var(--muted);
275
+ mask: var(--select-chevron) center / 16px 16px no-repeat;
276
+ pointer-events: none;
277
+ transition: transform var(--dur-pop-exit) var(--settle);
278
+ }
279
+ .hp-well-chevron:has(> [aria-expanded='true'])::after {
280
+ transform: rotate(180deg);
281
+ transition-duration: var(--dur-pop);
282
+ }
283
+ /* Its own rule: a browser without :open drops only this one. */
284
+ .hp-well-chevron:has(> :open)::after {
285
+ transform: rotate(180deg);
286
+ transition-duration: var(--dur-pop);
287
+ }
288
+ @media (prefers-reduced-motion: reduce) {
289
+ .hp-well-chevron::after {
290
+ transition: none;
291
+ }
292
+ }
293
+ @media (hover: hover) and (pointer: fine) {
294
+ .hp-well:hover:not(:disabled, [aria-disabled='true']) {
295
+ background-color: var(--surface-hover);
296
+ }
297
+ }
298
+ /* Disabled: muted text and the quieter --border, as on a disabled outlined
299
+ button. Chromium also sets a disabled <select> to 70% opacity; cancelled,
300
+ so a native and a rich select look the same when disabled. */
301
+ .hp-well:disabled,
302
+ .hp-well[aria-disabled='true'] {
303
+ border-color: var(--border);
304
+ color: var(--muted);
305
+ cursor: not-allowed;
306
+ opacity: 1;
307
+ }
308
+
309
+ /* ---- Expressive entrance ---------------------------------------------- */
310
+
311
+ /* Stagger (motion rule 7): a component that renders its first items in the
312
+ expressive tier adds .hp-enter and sets --i to the item's index. Items
313
+ from the sixth on share the sixth's delay. Only on first appearance:
314
+ never on items added later by filtering, sorting or paging. */
315
+ .hp-enter {
316
+ animation: hp-enter var(--dur-enter) var(--settle) both;
317
+ animation-delay: calc(min(var(--i, 0), 5) * var(--stagger-step));
318
+ }
319
+
320
+ @keyframes hp-enter {
321
+ from {
322
+ opacity: 0;
323
+ transform: translateY(4px);
324
+ }
325
+ }
326
+
327
+ @media (prefers-reduced-motion: reduce) {
328
+ .hp-enter {
329
+ animation: none;
330
+ }
331
+ }
332
+
333
+ /* ---- Utilities --------------------------------------------------------- */
334
+
335
+ /* Hidden from sight, kept in the accessibility tree. */
336
+ .hp-visually-hidden {
337
+ position: absolute;
338
+ width: 1px;
339
+ height: 1px;
340
+ margin: -1px;
341
+ padding: 0;
342
+ overflow: hidden;
343
+ clip-path: inset(50%);
344
+ white-space: nowrap;
345
+ border: 0;
346
+ }