@mystravil/hardpoint 0.1.0 → 0.2.1

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 (67) hide show
  1. package/CHANGELOG.md +42 -18
  2. package/LICENSE +25 -25
  3. package/README.md +67 -64
  4. package/dist/base.css +625 -346
  5. package/dist/components/AppShell.svelte +698 -698
  6. package/dist/components/Button.svelte +174 -174
  7. package/dist/components/Checkbox.svelte +235 -198
  8. package/dist/components/Checkbox.svelte.d.ts +9 -2
  9. package/dist/components/CheckboxGroup.svelte +207 -99
  10. package/dist/components/CheckboxGroup.svelte.d.ts +23 -4
  11. package/dist/components/Combobox.svelte +901 -824
  12. package/dist/components/Combobox.svelte.d.ts +10 -1
  13. package/dist/components/Dialog.svelte +359 -356
  14. package/dist/components/Disclosure.svelte +201 -201
  15. package/dist/components/EmptyState.svelte +88 -88
  16. package/dist/components/ErrorState.svelte +135 -135
  17. package/dist/components/ImageViewer.svelte +764 -0
  18. package/dist/components/ImageViewer.svelte.d.ts +55 -0
  19. package/dist/components/KeyValueList.svelte +114 -114
  20. package/dist/components/ListDetail.svelte +218 -0
  21. package/dist/components/ListDetail.svelte.d.ts +37 -0
  22. package/dist/components/LoadRegion.svelte +219 -219
  23. package/dist/components/MediaFrame.svelte +110 -110
  24. package/dist/components/MediaImage.svelte +256 -256
  25. package/dist/components/Menu.svelte +258 -258
  26. package/dist/components/MenuCheckboxItem.svelte +119 -119
  27. package/dist/components/MenuGroup.svelte +50 -50
  28. package/dist/components/MenuItem.svelte +94 -94
  29. package/dist/components/MenuRadioGroup.svelte +62 -62
  30. package/dist/components/MenuRadioItem.svelte +110 -110
  31. package/dist/components/MenuSeparator.svelte +28 -28
  32. package/dist/components/Meter.svelte +283 -0
  33. package/dist/components/Meter.svelte.d.ts +66 -0
  34. package/dist/components/NativeSelect.svelte +18 -2
  35. package/dist/components/NativeSelect.svelte.d.ts +6 -0
  36. package/dist/components/Notice.svelte +262 -0
  37. package/dist/components/Notice.svelte.d.ts +30 -0
  38. package/dist/components/Popover.svelte +222 -222
  39. package/dist/components/Progress.svelte +157 -157
  40. package/dist/components/RadioGroup.svelte +440 -0
  41. package/dist/components/RadioGroup.svelte.d.ts +50 -0
  42. package/dist/components/Select.svelte +471 -405
  43. package/dist/components/Select.svelte.d.ts +6 -0
  44. package/dist/components/Skeleton.svelte +149 -149
  45. package/dist/components/Spinner.svelte +118 -117
  46. package/dist/components/Stat.svelte +253 -253
  47. package/dist/components/Steps.svelte +189 -189
  48. package/dist/components/Switch.svelte +222 -222
  49. package/dist/components/Table.svelte +504 -504
  50. package/dist/components/Tabs.svelte +246 -246
  51. package/dist/components/Tag.svelte +40 -40
  52. package/dist/components/TextArea.svelte +272 -0
  53. package/dist/components/TextArea.svelte.d.ts +28 -0
  54. package/dist/components/TextField.svelte +256 -256
  55. package/dist/components/Timeline.svelte +156 -156
  56. package/dist/components/Toaster.svelte +411 -411
  57. package/dist/components/Tooltip.svelte +200 -200
  58. package/dist/components/TooltipGroup.svelte +46 -46
  59. package/dist/components/internal/Scrim.svelte +52 -52
  60. package/dist/components/internal/Sheet.svelte +263 -263
  61. package/dist/components/internal/groups.d.ts +14 -0
  62. package/dist/components/internal/groups.js +23 -0
  63. package/dist/fonts.css +71 -71
  64. package/dist/index.d.ts +6 -0
  65. package/dist/index.js +6 -0
  66. package/dist/tokens.css +283 -279
  67. package/package.json +81 -76
@@ -6,6 +6,12 @@ export type SelectItem = {
6
6
  label: string;
7
7
  /** A second line in `--muted`. */
8
8
  description?: string;
9
+ /**
10
+ * The heading the option sits under, as written. Options with the same
11
+ * group share one heading, in the order each group first appears;
12
+ * options without one come first.
13
+ */
14
+ group?: string;
9
15
  disabled?: boolean;
10
16
  };
11
17
  export type SelectItemSnippetProps = {
@@ -1,149 +1,149 @@
1
- <script lang="ts" module>
2
- import type { HTMLAttributes } from 'svelte/elements';
3
-
4
- /**
5
- * `text` (default): bars that stand for lines of text, each one line box
6
- * high in the surrounding type, so content replaces them without moving
7
- * anything. `block`: one solid box, for a control, a figure or a mark.
8
- */
9
- export type SkeletonVariant = 'text' | 'block';
10
-
11
- export type SkeletonProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'> & {
12
- variant?: SkeletonVariant;
13
- /**
14
- * Any CSS width, such as `8ch` for an identifier in a mono cell or
15
- * `60%`. Defaults to `100%`, and never exceeds its container's width.
16
- * With several `lines`, the width of the full lines; the last line is
17
- * shorter.
18
- */
19
- width?: string;
20
- /** `block` only: any CSS height. Defaults to `--control-height`. */
21
- height?: string;
22
- /** `text` only: how many lines of text the bars stand for. Defaults to 1. */
23
- lines?: number;
24
- };
25
-
26
- // Blocks that mount together appear together: every skeleton that mounts
27
- // in the same task with the same delay shares one timer, so a page of
28
- // blocks shows in one frame instead of rippling in.
29
- let batch: Map<number, Set<() => void>> | null = null;
30
-
31
- function afterDelay(delay: number, show: () => void): () => void {
32
- if (!batch) {
33
- batch = new Map();
34
- queueMicrotask(() => (batch = null));
35
- }
36
- let members = batch.get(delay);
37
- if (!members) {
38
- const waiting = new Set<() => void>();
39
- batch.set(delay, waiting);
40
- setTimeout(() => waiting.forEach((fn) => fn()), delay);
41
- members = waiting;
42
- }
43
- members.add(show);
44
- const own = members;
45
- return () => own.delete(show);
46
- }
47
- </script>
48
-
49
- <script lang="ts">
50
- import { duration } from '../motion/transitions.js';
51
-
52
- let {
53
- variant = 'text',
54
- width,
55
- height,
56
- lines = 1,
57
- class: className,
58
- ...rest
59
- }: SkeletonProps = $props();
60
-
61
- const count = $derived(variant === 'text' ? Math.max(1, Math.floor(lines)) : 0);
62
-
63
- let shown = $state(false);
64
-
65
- // Motion rule 2: blocks only while waiting. Nothing shows until
66
- // --skeleton-delay (300ms) has passed, so fast content never flashes
67
- // blocks; the box keeps its size meanwhile. The delay is read from the
68
- // element, so an app that already waited can set it to 0ms. It is not
69
- // motion, so reduced motion keeps it.
70
- function appear(node: HTMLElement) {
71
- const delay = duration(node, '--skeleton-delay');
72
- if (delay <= 0) {
73
- shown = true;
74
- return;
75
- }
76
- return afterDelay(delay, () => (shown = true));
77
- }
78
- </script>
79
-
80
- <!--
81
- A static placeholder that mirrors the content it stands for (BRIEF, motion
82
- rule 2): no shimmer and no transition of its own. Render it in a
83
- LoadRegion's `placeholder`, which swaps it for the content as
84
- `--dur-reveal` sets (0ms: a cut). It is aria-hidden: the region says what
85
- loads itself (aria-busy and a status line). A `text` skeleton takes the
86
- type of its context, so give it the content's type class
87
- (`hp-page-title`, `hp-label`) or put it where the content goes, such as a
88
- table cell. Extra attributes and `class` go to the root
89
- `span.hp-skeleton`, which carries `data-variant` and `data-state`
90
- waiting | visible.
91
- -->
92
- <span
93
- {...rest}
94
- class={['hp-skeleton', className]}
95
- aria-hidden="true"
96
- data-variant={variant}
97
- data-state={shown ? 'visible' : 'waiting'}
98
- style:width
99
- style:height={variant === 'block' ? height : undefined}
100
- {@attach appear}
101
- >
102
- {#each { length: count }}
103
- <span class="hp-skeleton-line"></span>
104
- {/each}
105
- </span>
106
-
107
- <style>
108
- /* Never wider than its container. In a grid or flex item a fixed width
109
- still counts toward the item's minimum width, as an image's does: let
110
- the item shrink (minmax(0, 1fr), min-width: 0). */
111
- .hp-skeleton {
112
- width: 100%;
113
- max-width: 100%;
114
- }
115
- /* Hidden, not removed, while it waits: the box keeps its size. */
116
- .hp-skeleton[data-state='waiting'] {
117
- visibility: hidden;
118
- }
119
-
120
- /* ---- Text: one bar per line box ------------------------------------ */
121
-
122
- /* Top-aligned and as tall as its lines, so it never changes the height
123
- of the line it sits in. */
124
- .hp-skeleton[data-variant='text'] {
125
- display: inline-block;
126
- vertical-align: top;
127
- }
128
- /* The bar is three quarters of the font size, rounded down to an even
129
- number of pixels, centred in a line box of the context's line height.
130
- Even bars in even line heights leave whole pixels above and below. */
131
- .hp-skeleton-line {
132
- display: block;
133
- height: 1lh;
134
- background: linear-gradient(var(--surface-band) 0 0) 0 50% / 100% round(down, 0.75em, 2px)
135
- no-repeat;
136
- }
137
- /* The last of several lines is shorter, as a paragraph's last line is. */
138
- .hp-skeleton-line:last-child:not(:first-child) {
139
- width: 60%;
140
- }
141
-
142
- /* ---- Block: one solid box ------------------------------------------- */
143
-
144
- .hp-skeleton[data-variant='block'] {
145
- display: block;
146
- height: var(--control-height);
147
- background: var(--surface-band);
148
- }
149
- </style>
1
+ <script lang="ts" module>
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+
4
+ /**
5
+ * `text` (default): bars that stand for lines of text, each one line box
6
+ * high in the surrounding type, so content replaces them without moving
7
+ * anything. `block`: one solid box, for a control, a figure or a mark.
8
+ */
9
+ export type SkeletonVariant = 'text' | 'block';
10
+
11
+ export type SkeletonProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'> & {
12
+ variant?: SkeletonVariant;
13
+ /**
14
+ * Any CSS width, such as `8ch` for an identifier in a mono cell or
15
+ * `60%`. Defaults to `100%`, and never exceeds its container's width.
16
+ * With several `lines`, the width of the full lines; the last line is
17
+ * shorter.
18
+ */
19
+ width?: string;
20
+ /** `block` only: any CSS height. Defaults to `--control-height`. */
21
+ height?: string;
22
+ /** `text` only: how many lines of text the bars stand for. Defaults to 1. */
23
+ lines?: number;
24
+ };
25
+
26
+ // Blocks that mount together appear together: every skeleton that mounts
27
+ // in the same task with the same delay shares one timer, so a page of
28
+ // blocks shows in one frame instead of rippling in.
29
+ let batch: Map<number, Set<() => void>> | null = null;
30
+
31
+ function afterDelay(delay: number, show: () => void): () => void {
32
+ if (!batch) {
33
+ batch = new Map();
34
+ queueMicrotask(() => (batch = null));
35
+ }
36
+ let members = batch.get(delay);
37
+ if (!members) {
38
+ const waiting = new Set<() => void>();
39
+ batch.set(delay, waiting);
40
+ setTimeout(() => waiting.forEach((fn) => fn()), delay);
41
+ members = waiting;
42
+ }
43
+ members.add(show);
44
+ const own = members;
45
+ return () => own.delete(show);
46
+ }
47
+ </script>
48
+
49
+ <script lang="ts">
50
+ import { duration } from '../motion/transitions.js';
51
+
52
+ let {
53
+ variant = 'text',
54
+ width,
55
+ height,
56
+ lines = 1,
57
+ class: className,
58
+ ...rest
59
+ }: SkeletonProps = $props();
60
+
61
+ const count = $derived(variant === 'text' ? Math.max(1, Math.floor(lines)) : 0);
62
+
63
+ let shown = $state(false);
64
+
65
+ // Motion rule 2: blocks only while waiting. Nothing shows until
66
+ // --skeleton-delay (300ms) has passed, so fast content never flashes
67
+ // blocks; the box keeps its size meanwhile. The delay is read from the
68
+ // element, so an app that already waited can set it to 0ms. It is not
69
+ // motion, so reduced motion keeps it.
70
+ function appear(node: HTMLElement) {
71
+ const delay = duration(node, '--skeleton-delay');
72
+ if (delay <= 0) {
73
+ shown = true;
74
+ return;
75
+ }
76
+ return afterDelay(delay, () => (shown = true));
77
+ }
78
+ </script>
79
+
80
+ <!--
81
+ A static placeholder that mirrors the content it stands for (BRIEF, motion
82
+ rule 2): no shimmer and no transition of its own. Render it in a
83
+ LoadRegion's `placeholder`, which swaps it for the content as
84
+ `--dur-reveal` sets (0ms: a cut). It is aria-hidden: the region says what
85
+ loads itself (aria-busy and a status line). A `text` skeleton takes the
86
+ type of its context, so give it the content's type class
87
+ (`hp-page-title`, `hp-label`) or put it where the content goes, such as a
88
+ table cell. Extra attributes and `class` go to the root
89
+ `span.hp-skeleton`, which carries `data-variant` and `data-state`
90
+ waiting | visible.
91
+ -->
92
+ <span
93
+ {...rest}
94
+ class={['hp-skeleton', className]}
95
+ aria-hidden="true"
96
+ data-variant={variant}
97
+ data-state={shown ? 'visible' : 'waiting'}
98
+ style:width
99
+ style:height={variant === 'block' ? height : undefined}
100
+ {@attach appear}
101
+ >
102
+ {#each { length: count }}
103
+ <span class="hp-skeleton-line"></span>
104
+ {/each}
105
+ </span>
106
+
107
+ <style>
108
+ /* Never wider than its container. In a grid or flex item a fixed width
109
+ still counts toward the item's minimum width, as an image's does: let
110
+ the item shrink (minmax(0, 1fr), min-width: 0). */
111
+ .hp-skeleton {
112
+ width: 100%;
113
+ max-width: 100%;
114
+ }
115
+ /* Hidden, not removed, while it waits: the box keeps its size. */
116
+ .hp-skeleton[data-state='waiting'] {
117
+ visibility: hidden;
118
+ }
119
+
120
+ /* ---- Text: one bar per line box ------------------------------------ */
121
+
122
+ /* Top-aligned and as tall as its lines, so it never changes the height
123
+ of the line it sits in. */
124
+ .hp-skeleton[data-variant='text'] {
125
+ display: inline-block;
126
+ vertical-align: top;
127
+ }
128
+ /* The bar is three quarters of the font size, rounded down to an even
129
+ number of pixels, centred in a line box of the context's line height.
130
+ Even bars in even line heights leave whole pixels above and below. */
131
+ .hp-skeleton-line {
132
+ display: block;
133
+ height: 1lh;
134
+ background: linear-gradient(var(--surface-band) 0 0) 0 50% / 100% round(down, 0.75em, 2px)
135
+ no-repeat;
136
+ }
137
+ /* The last of several lines is shorter, as a paragraph's last line is. */
138
+ .hp-skeleton-line:last-child:not(:first-child) {
139
+ width: 60%;
140
+ }
141
+
142
+ /* ---- Block: one solid box ------------------------------------------- */
143
+
144
+ .hp-skeleton[data-variant='block'] {
145
+ display: block;
146
+ height: var(--control-height);
147
+ background: var(--surface-band);
148
+ }
149
+ </style>
@@ -1,117 +1,118 @@
1
- <script lang="ts" module>
2
- import type { HTMLAttributes } from 'svelte/elements';
3
-
4
- export type SpinnerProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'> & {
5
- /**
6
- * What the app is waiting for, such as `Loading projects`. Shown beside
7
- * the glyph and announced politely once the spinner appears.
8
- */
9
- label: string;
10
- /** The root element. */
11
- ref?: HTMLSpanElement | null;
12
- };
13
- </script>
14
-
15
- <script lang="ts">
16
- import { duration } from '../motion/transitions.js';
17
-
18
- // The reel's line spinner (da3-10): bar, slash, dash, backslash.
19
- const glyphs = ['|', '/', '-', '\\'];
20
-
21
- let { label, class: className, ref = $bindable(null), ...rest }: SpinnerProps = $props();
22
-
23
- let shown = $state(false);
24
- let step = $state(0);
25
-
26
- // A timer, not CSS keyframes (keyframes are for expressive entrances
27
- // only). It reads both durations from the element, so an app can retune
28
- // them, also per spinner (`style="--spinner-delay: 0ms"`). It keeps
29
- // stepping under reduced motion: spinners and progress stay.
30
- function run(node: HTMLElement) {
31
- const delay = duration(node, '--spinner-delay');
32
- const every = duration(node, '--spinner-step');
33
- let interval: ReturnType<typeof setInterval> | undefined;
34
- const timeout = setTimeout(() => {
35
- shown = true;
36
- if (every > 0) interval = setInterval(() => (step = (step + 1) % glyphs.length), every);
37
- }, delay);
38
- return () => {
39
- clearTimeout(timeout);
40
- clearInterval(interval);
41
- };
42
- }
43
- </script>
44
-
45
- <!--
46
- A stepped spinner (BRIEF, visual rule 7): `|`, `/`, `-`, `\` in Geist
47
- Pixel Square at 26.32px, one state per `--spinner-step` (167ms). Nothing
48
- shows until `--spinner-delay` (300ms) has passed, so fast work never
49
- flashes it; the row keeps its height meanwhile. The glyph is aria-hidden
50
- and sits outside the live region; the label is a `role="status"` region
51
- that is empty until the spinner appears, so screen readers announce it
52
- once. Extra attributes and `class` go to the root `span.hp-spinner`, which
53
- carries `data-state` waiting | visible. Place it start-aligned: centring
54
- it in a wider box can put the glyph between pixels.
55
- -->
56
- <span
57
- {...rest}
58
- bind:this={ref}
59
- class={['hp-spinner', className]}
60
- data-state={shown ? 'visible' : 'waiting'}
61
- {@attach run}
62
- >
63
- <span class="hp-spinner-glyph" aria-hidden="true" data-step={shown ? step : undefined}
64
- >{shown ? glyphs[step] : ''}</span
65
- >
66
- <span class="hp-spinner-label" role="status">{shown ? label : ''}</span>
67
- </span>
68
-
69
- <style>
70
- /* One row the height of the pixel line, so the spinner does not move
71
- anything when it appears. The label line sits an even number of pixels
72
- from the top, so the glyph's top edge stays on a whole pixel. */
73
- .hp-spinner {
74
- display: inline-flex;
75
- align-items: center;
76
- gap: var(--space-3);
77
- min-height: var(--type-pixel-leading);
78
- vertical-align: top;
79
- }
80
-
81
- /* Geist Pixel is proportional: at 26.32px (a 1px module) the four glyphs
82
- advance 6, 11, 10 and 11px. A fixed 12px box keeps the label still
83
- while the glyph changes. Each glyph starts at the box's edge plus a
84
- whole-pixel inset that centres it as nearly as whole pixels allow,
85
- because centring in odd free space would put its edges on half pixels
86
- (BRIEF, Crisp pixel text). The insets are font metrics. */
87
- .hp-spinner-glyph {
88
- flex: none;
89
- width: 12px;
90
- font-family: var(--pixel);
91
- font-size: var(--type-pixel);
92
- font-weight: 400;
93
- line-height: var(--type-pixel-leading);
94
- letter-spacing: 0;
95
- text-align: start;
96
- color: var(--text);
97
- }
98
- .hp-spinner-glyph[data-step='0'] {
99
- padding-inline-start: 3px;
100
- }
101
- .hp-spinner-glyph[data-step='2'] {
102
- padding-inline-start: 1px;
103
- }
104
-
105
- /* 16px on touch, the dense 14px on fine pointers. */
106
- .hp-spinner-label {
107
- color: var(--muted);
108
- font-size: var(--type-body);
109
- line-height: 24px;
110
- }
111
- @media (pointer: fine) {
112
- .hp-spinner-label {
113
- font-size: var(--type-dense);
114
- line-height: 20px;
115
- }
116
- }
117
- </style>
1
+ <script lang="ts" module>
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+
4
+ export type SpinnerProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'> & {
5
+ /**
6
+ * What the app is waiting for, such as `Loading projects`. Shown beside
7
+ * the glyph and announced politely once the spinner appears.
8
+ */
9
+ label: string;
10
+ /** The root element. */
11
+ ref?: HTMLSpanElement | null;
12
+ };
13
+ </script>
14
+
15
+ <script lang="ts">
16
+ import { duration } from '../motion/transitions.js';
17
+
18
+ // The reel's line spinner (da3-10): bar, slash, dash, backslash.
19
+ const glyphs = ['|', '/', '-', '\\'];
20
+
21
+ let { label, class: className, ref = $bindable(null), ...rest }: SpinnerProps = $props();
22
+
23
+ let shown = $state(false);
24
+ let step = $state(0);
25
+
26
+ // A timer, not CSS keyframes (keyframes are for expressive entrances
27
+ // only). It reads both durations from the element, so an app can retune
28
+ // them, also per spinner (a class on the Spinner whose rule sets
29
+ // `--spinner-delay: 0ms`). It keeps stepping under reduced motion:
30
+ // spinners and progress stay.
31
+ function run(node: HTMLElement) {
32
+ const delay = duration(node, '--spinner-delay');
33
+ const every = duration(node, '--spinner-step');
34
+ let interval: ReturnType<typeof setInterval> | undefined;
35
+ const timeout = setTimeout(() => {
36
+ shown = true;
37
+ if (every > 0) interval = setInterval(() => (step = (step + 1) % glyphs.length), every);
38
+ }, delay);
39
+ return () => {
40
+ clearTimeout(timeout);
41
+ clearInterval(interval);
42
+ };
43
+ }
44
+ </script>
45
+
46
+ <!--
47
+ A stepped spinner (BRIEF, visual rule 7): `|`, `/`, `-`, `\` in Geist
48
+ Pixel Square at 26.32px, one state per `--spinner-step` (167ms). Nothing
49
+ shows until `--spinner-delay` (300ms) has passed, so fast work never
50
+ flashes it; the row keeps its height meanwhile. The glyph is aria-hidden
51
+ and sits outside the live region; the label is a `role="status"` region
52
+ that is empty until the spinner appears, so screen readers announce it
53
+ once. Extra attributes and `class` go to the root `span.hp-spinner`, which
54
+ carries `data-state` waiting | visible. Place it start-aligned: centring
55
+ it in a wider box can put the glyph between pixels.
56
+ -->
57
+ <span
58
+ {...rest}
59
+ bind:this={ref}
60
+ class={['hp-spinner', className]}
61
+ data-state={shown ? 'visible' : 'waiting'}
62
+ {@attach run}
63
+ >
64
+ <span class="hp-spinner-glyph" aria-hidden="true" data-step={shown ? step : undefined}
65
+ >{shown ? glyphs[step] : ''}</span
66
+ >
67
+ <span class="hp-spinner-label" role="status">{shown ? label : ''}</span>
68
+ </span>
69
+
70
+ <style>
71
+ /* One row the height of the pixel line, so the spinner does not move
72
+ anything when it appears. The label line sits an even number of pixels
73
+ from the top, so the glyph's top edge stays on a whole pixel. */
74
+ .hp-spinner {
75
+ display: inline-flex;
76
+ align-items: center;
77
+ gap: var(--space-3);
78
+ min-height: var(--type-pixel-leading);
79
+ vertical-align: top;
80
+ }
81
+
82
+ /* Geist Pixel is proportional: at 26.32px (a 1px module) the four glyphs
83
+ advance 6, 11, 10 and 11px. A fixed 12px box keeps the label still
84
+ while the glyph changes. Each glyph starts at the box's edge plus a
85
+ whole-pixel inset that centres it as nearly as whole pixels allow,
86
+ because centring in odd free space would put its edges on half pixels
87
+ (BRIEF, Crisp pixel text). The insets are font metrics. */
88
+ .hp-spinner-glyph {
89
+ flex: none;
90
+ width: 12px;
91
+ font-family: var(--pixel);
92
+ font-size: var(--type-pixel);
93
+ font-weight: 400;
94
+ line-height: var(--type-pixel-leading);
95
+ letter-spacing: 0;
96
+ text-align: start;
97
+ color: var(--text);
98
+ }
99
+ .hp-spinner-glyph[data-step='0'] {
100
+ padding-inline-start: 3px;
101
+ }
102
+ .hp-spinner-glyph[data-step='2'] {
103
+ padding-inline-start: 1px;
104
+ }
105
+
106
+ /* 16px on touch, the dense 14px on fine pointers. */
107
+ .hp-spinner-label {
108
+ color: var(--muted);
109
+ font-size: var(--type-body);
110
+ line-height: 24px;
111
+ }
112
+ @media (pointer: fine) {
113
+ .hp-spinner-label {
114
+ font-size: var(--type-dense);
115
+ line-height: 20px;
116
+ }
117
+ }
118
+ </style>