@mystravil/hardpoint 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +51 -38
  2. package/LICENSE +25 -25
  3. package/README.md +67 -64
  4. package/dist/base.css +625 -625
  5. package/dist/components/AppShell.svelte +698 -698
  6. package/dist/components/Button.svelte +221 -174
  7. package/dist/components/Button.svelte.d.ts +18 -2
  8. package/dist/components/Checkbox.svelte +235 -235
  9. package/dist/components/CheckboxGroup.svelte +207 -207
  10. package/dist/components/Combobox.svelte +901 -901
  11. package/dist/components/Dialog.svelte +359 -359
  12. package/dist/components/Disclosure.svelte +201 -201
  13. package/dist/components/EmptyState.svelte +88 -88
  14. package/dist/components/ErrorState.svelte +135 -135
  15. package/dist/components/ImageViewer.svelte +764 -764
  16. package/dist/components/KeyValueList.svelte +114 -114
  17. package/dist/components/ListDetail.svelte +218 -218
  18. package/dist/components/LoadRegion.svelte +219 -219
  19. package/dist/components/MediaFrame.svelte +110 -110
  20. package/dist/components/MediaImage.svelte +256 -256
  21. package/dist/components/Menu.svelte +275 -258
  22. package/dist/components/MenuCheckboxItem.svelte +119 -119
  23. package/dist/components/MenuGroup.svelte +50 -50
  24. package/dist/components/MenuItem.svelte +94 -94
  25. package/dist/components/MenuRadioGroup.svelte +62 -62
  26. package/dist/components/MenuRadioItem.svelte +110 -110
  27. package/dist/components/MenuSeparator.svelte +28 -28
  28. package/dist/components/Meter.svelte +283 -283
  29. package/dist/components/NativeSelect.svelte +135 -135
  30. package/dist/components/Notice.svelte +262 -262
  31. package/dist/components/Popover.svelte +222 -222
  32. package/dist/components/Progress.svelte +157 -157
  33. package/dist/components/RadioGroup.svelte +440 -440
  34. package/dist/components/Select.svelte +471 -471
  35. package/dist/components/Skeleton.svelte +149 -149
  36. package/dist/components/Spinner.svelte +118 -118
  37. package/dist/components/Stat.svelte +253 -253
  38. package/dist/components/Steps.svelte +189 -189
  39. package/dist/components/Switch.svelte +222 -222
  40. package/dist/components/Table.svelte +504 -504
  41. package/dist/components/Tabs.svelte +246 -246
  42. package/dist/components/Tag.svelte +40 -40
  43. package/dist/components/TextArea.svelte +272 -272
  44. package/dist/components/TextField.svelte +396 -256
  45. package/dist/components/TextField.svelte.d.ts +16 -1
  46. package/dist/components/Timeline.svelte +156 -156
  47. package/dist/components/Toaster.svelte +411 -411
  48. package/dist/components/Tooltip.svelte +200 -200
  49. package/dist/components/TooltipGroup.svelte +46 -46
  50. package/dist/components/internal/Scrim.svelte +52 -52
  51. package/dist/components/internal/Sheet.svelte +263 -263
  52. package/dist/fonts.css +71 -71
  53. package/dist/tokens.css +283 -283
  54. package/package.json +81 -76
@@ -1,263 +1,263 @@
1
- <script lang="ts" module>
2
- import type { Snippet } from 'svelte';
3
- import type { HTMLAttributes } from 'svelte/elements';
4
- import type { OverlayLayer } from './Scrim.svelte';
5
-
6
- /** Attributes and handlers not listed here go to the panel. */
7
- export type SheetProps = Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'children'> & {
8
- /**
9
- * Shown while true. Pass the `open` that the Bits UI content's
10
- * `child` snippet receives (render the content with `forceMount`), so
11
- * the sheet and its scrim slide and fade out before they unmount.
12
- */
13
- open: boolean;
14
- /** The title row's text, in tracked caps. */
15
- title: string;
16
- /** The title element's `id`, so the content can be labelled by it. */
17
- titleId?: string;
18
- /** The title element: `p`, or `h2` when the sheet is a dialog. */
19
- titleTag?: 'p' | 'h2';
20
- /** The z-index token: `dialog` for dialogs, `popover` for everything else. */
21
- layer: OverlayLayer;
22
- /**
23
- * Called by the close button and by a swipe down. A tap on the scrim
24
- * and Escape close through Bits UI.
25
- */
26
- onclose: () => void;
27
- /** The close button's accessible name. */
28
- closeLabel?: string;
29
- /** The panel element. */
30
- ref?: HTMLElement | null;
31
- /** The body, under the title row. It scrolls if it does not fit. */
32
- children: Snippet;
33
- /** A row pinned under the body, such as a dialog's actions. */
34
- footer?: Snippet;
35
- };
36
- </script>
37
-
38
- <script lang="ts">
39
- import { sheet } from '../../motion/index.js';
40
- import Scrim from './Scrim.svelte';
41
- import { swipeDismiss } from './swipe-dismiss.js';
42
- import { followVisualViewport } from './visual-viewport.svelte.js';
43
-
44
- let {
45
- open,
46
- title,
47
- titleId,
48
- titleTag = 'p',
49
- layer,
50
- onclose,
51
- closeLabel = 'Close',
52
- ref = $bindable(null),
53
- children,
54
- footer,
55
- class: className,
56
- ...rest
57
- }: SheetProps = $props();
58
-
59
- let body = $state<HTMLElement | null>(null);
60
-
61
- // The sheet follows the visual viewport while it is open (decided
62
- // 2026-11-10; internal/visual-viewport.svelte.ts): while the on-screen
63
- // keyboard shortens it, the sheet's bottom edge sits on its bottom edge
64
- // and the sheet may grow to its height less the header row's
65
- // (--header-height and the top safe area), where it was half the screen.
66
- // The focused field in the body stays in sight.
67
- const viewport = followVisualViewport(() => open, () => body);
68
-
69
- // The press that opened the sheet never acts on what the sheet now shows
70
- // under the finger (decided 2026-11-10). On touch, Bits UI's Menu and
71
- // Select triggers open on pointerup, and the click that follows lands on
72
- // the sheet, which under reduced motion fades in where it ends up; a
73
- // mouse opens them on pointerdown, and its pointerup on an item counts
74
- // as a choice. So until a press starts inside the sheet, a pointerup or a
75
- // pointer's click inside it stops here. A click from Enter, Space or
76
- // element.click() has detail 0 and always passes.
77
- function ignoreOpeningPress(node: HTMLElement) {
78
- let pressed = false;
79
- const press = () => (pressed = true);
80
- const guard = (e: MouseEvent) => {
81
- if (pressed || (e.type === 'click' && e.detail === 0)) return;
82
- e.preventDefault();
83
- e.stopImmediatePropagation();
84
- };
85
- const capture = { capture: true };
86
- node.addEventListener('pointerdown', press, capture);
87
- node.addEventListener('pointerup', guard, capture);
88
- node.addEventListener('click', guard, capture);
89
- return () => {
90
- node.removeEventListener('pointerdown', press, capture);
91
- node.removeEventListener('pointerup', guard, capture);
92
- node.removeEventListener('click', guard, capture);
93
- };
94
- }
95
- </script>
96
-
97
- <!--
98
- The bottom sheet every overlay uses on compact widths (BRIEF, Apps and
99
- phones: Overlays): a scrim, then a panel at the bottom edge with a handle,
100
- a caps title, a close button, the body and an optional footer. It opens at
101
- half the screen at most, with no drag to expand; the body scrolls inside.
102
- A swipe down dismisses it (internal/swipe-dismiss.ts). While the visual
103
- viewport is shorter than the layout viewport (the on-screen keyboard), it
104
- sits on top of it and may fill the space above it. The press that opened
105
- it does not act inside it.
106
-
107
- Render it inside the Bits UI content's `child` snippet and spread the
108
- snippet's `props` onto it when the panel is the content element (a
109
- dialog), or onto an element in the body when the content is a list (a
110
- menu, a select): then pass `onInteractOutside` to the content and call
111
- `preventDefault()` for presses inside `ref`, so the title row does not
112
- count as outside.
113
- -->
114
- <Scrim {open} {layer} />
115
- {#if open}
116
- {@const lifted = viewport.current}
117
- <div
118
- bind:this={ref}
119
- {...rest}
120
- class={['hp-sheet', className]}
121
- data-layer={layer}
122
- style:bottom={lifted ? `calc(100% - ${lifted.bottom}px)` : undefined}
123
- style:padding-bottom={lifted ? '0px' : undefined}
124
- style:--sheet-max-height={lifted
125
- ? `calc(${lifted.height}px - var(--header-height) - env(safe-area-inset-top))`
126
- : undefined}
127
- transition:sheet
128
- {@attach ignoreOpeningPress}
129
- {@attach swipeDismiss(() => onclose())}
130
- >
131
- <div class="hp-sheet-head">
132
- <span class="hp-sheet-handle" aria-hidden="true"></span>
133
- <svelte:element this={titleTag} id={titleId} class="hp-sheet-title">{title}</svelte:element>
134
- <button type="button" class="hp-sheet-close" aria-label={closeLabel} onclick={() => onclose()}>
135
- <svg
136
- viewBox="0 0 16 16"
137
- fill="none"
138
- stroke="currentColor"
139
- stroke-width="1.5"
140
- stroke-linecap="square"
141
- aria-hidden="true"
142
- >
143
- <path d="M3 3l10 10M13 3L3 13" />
144
- </svg>
145
- </button>
146
- </div>
147
- <div class="hp-sheet-body" bind:this={body}>
148
- {@render children()}
149
- </div>
150
- {#if footer}
151
- <div class="hp-sheet-footer">
152
- {@render footer()}
153
- </div>
154
- {/if}
155
- </div>
156
- {/if}
157
-
158
- <style>
159
- /* While a floating list is open Bits UI sets pointer-events: none on
160
- <body>; the panel takes presses again. */
161
- .hp-sheet {
162
- /* The height cap: half the screen, or the room above the on-screen
163
- keyboard while it is up (set inline). A list that must keep its
164
- height while it filters asks for this. */
165
- --sheet-max-height: 50dvh;
166
-
167
- position: fixed;
168
- inset: auto 0 0;
169
- z-index: var(--layer-dialog);
170
- display: flex;
171
- flex-direction: column;
172
- box-sizing: border-box;
173
- /* Opens at half the screen at most; the body scrolls inside. */
174
- max-height: var(--sheet-max-height);
175
- padding: 0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
176
- border-top: 1px solid var(--border-control);
177
- background: var(--canvas);
178
- color: var(--text);
179
- overscroll-behavior: contain;
180
- pointer-events: auto;
181
- }
182
- .hp-sheet[data-layer='popover'] {
183
- z-index: var(--layer-popover);
184
- }
185
-
186
- /* The grab area: handle, title and close button. The swipe owns touches
187
- here; the body below keeps its scroll until it reaches the top. */
188
- .hp-sheet-head {
189
- flex: none;
190
- display: grid;
191
- grid-template-columns: minmax(0, 1fr) auto;
192
- align-items: start;
193
- padding: 0 var(--space-1) 0 var(--gutter);
194
- touch-action: none;
195
- }
196
- .hp-sheet-handle {
197
- grid-column: 1 / -1;
198
- justify-self: center;
199
- width: var(--space-8);
200
- height: var(--space-1);
201
- margin-top: var(--space-2);
202
- background: var(--border-control);
203
- }
204
- /* Tracked caps at the compact view-title size. A long title wraps (200
205
- percent zoom); the padding centres its first line on the 44px close
206
- button, so a single line sits where it always did. The clip stays: it
207
- keeps the first glyph's antialiasing inside the text edge, as before. */
208
- .hp-sheet-title {
209
- overflow: hidden;
210
- padding-block: calc((44px - 1lh) / 2);
211
- font-size: var(--type-body);
212
- font-weight: 500;
213
- line-height: 24px;
214
- letter-spacing: var(--label-tracking);
215
- text-transform: var(--label-transform);
216
- overflow-wrap: anywhere;
217
- }
218
- .hp-sheet-close {
219
- display: grid;
220
- place-items: center;
221
- width: 44px;
222
- height: 44px;
223
- padding: 0;
224
- border: 0;
225
- background: transparent;
226
- color: var(--text);
227
- cursor: pointer;
228
- -webkit-tap-highlight-color: transparent;
229
- }
230
- .hp-sheet-close svg {
231
- width: var(--space-4);
232
- height: var(--space-4);
233
- }
234
- .hp-sheet-close:active {
235
- background: var(--surface-raised);
236
- }
237
- @media (hover: hover) and (pointer: fine) {
238
- .hp-sheet-close {
239
- transition: background-color var(--dur-hover) var(--settle);
240
- }
241
- .hp-sheet-close:hover {
242
- background: var(--surface-hover);
243
- }
244
- .hp-sheet-close:active {
245
- background: var(--surface-raised);
246
- transition-duration: var(--dur-press);
247
- }
248
- }
249
-
250
- /* A flex column, so a list inside can take the rest of the height
251
- (flex: 1 1 auto; min-height: 0) and scroll on its own. */
252
- .hp-sheet-body {
253
- display: flex;
254
- flex-direction: column;
255
- flex: 1 1 auto;
256
- min-height: 0;
257
- overflow-y: auto;
258
- overscroll-behavior: contain;
259
- }
260
- .hp-sheet-footer {
261
- flex: none;
262
- }
263
- </style>
1
+ <script lang="ts" module>
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import type { OverlayLayer } from './Scrim.svelte';
5
+
6
+ /** Attributes and handlers not listed here go to the panel. */
7
+ export type SheetProps = Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'children'> & {
8
+ /**
9
+ * Shown while true. Pass the `open` that the Bits UI content's
10
+ * `child` snippet receives (render the content with `forceMount`), so
11
+ * the sheet and its scrim slide and fade out before they unmount.
12
+ */
13
+ open: boolean;
14
+ /** The title row's text, in tracked caps. */
15
+ title: string;
16
+ /** The title element's `id`, so the content can be labelled by it. */
17
+ titleId?: string;
18
+ /** The title element: `p`, or `h2` when the sheet is a dialog. */
19
+ titleTag?: 'p' | 'h2';
20
+ /** The z-index token: `dialog` for dialogs, `popover` for everything else. */
21
+ layer: OverlayLayer;
22
+ /**
23
+ * Called by the close button and by a swipe down. A tap on the scrim
24
+ * and Escape close through Bits UI.
25
+ */
26
+ onclose: () => void;
27
+ /** The close button's accessible name. */
28
+ closeLabel?: string;
29
+ /** The panel element. */
30
+ ref?: HTMLElement | null;
31
+ /** The body, under the title row. It scrolls if it does not fit. */
32
+ children: Snippet;
33
+ /** A row pinned under the body, such as a dialog's actions. */
34
+ footer?: Snippet;
35
+ };
36
+ </script>
37
+
38
+ <script lang="ts">
39
+ import { sheet } from '../../motion/index.js';
40
+ import Scrim from './Scrim.svelte';
41
+ import { swipeDismiss } from './swipe-dismiss.js';
42
+ import { followVisualViewport } from './visual-viewport.svelte.js';
43
+
44
+ let {
45
+ open,
46
+ title,
47
+ titleId,
48
+ titleTag = 'p',
49
+ layer,
50
+ onclose,
51
+ closeLabel = 'Close',
52
+ ref = $bindable(null),
53
+ children,
54
+ footer,
55
+ class: className,
56
+ ...rest
57
+ }: SheetProps = $props();
58
+
59
+ let body = $state<HTMLElement | null>(null);
60
+
61
+ // The sheet follows the visual viewport while it is open (decided
62
+ // 2026-11-10; internal/visual-viewport.svelte.ts): while the on-screen
63
+ // keyboard shortens it, the sheet's bottom edge sits on its bottom edge
64
+ // and the sheet may grow to its height less the header row's
65
+ // (--header-height and the top safe area), where it was half the screen.
66
+ // The focused field in the body stays in sight.
67
+ const viewport = followVisualViewport(() => open, () => body);
68
+
69
+ // The press that opened the sheet never acts on what the sheet now shows
70
+ // under the finger (decided 2026-11-10). On touch, Bits UI's Menu and
71
+ // Select triggers open on pointerup, and the click that follows lands on
72
+ // the sheet, which under reduced motion fades in where it ends up; a
73
+ // mouse opens them on pointerdown, and its pointerup on an item counts
74
+ // as a choice. So until a press starts inside the sheet, a pointerup or a
75
+ // pointer's click inside it stops here. A click from Enter, Space or
76
+ // element.click() has detail 0 and always passes.
77
+ function ignoreOpeningPress(node: HTMLElement) {
78
+ let pressed = false;
79
+ const press = () => (pressed = true);
80
+ const guard = (e: MouseEvent) => {
81
+ if (pressed || (e.type === 'click' && e.detail === 0)) return;
82
+ e.preventDefault();
83
+ e.stopImmediatePropagation();
84
+ };
85
+ const capture = { capture: true };
86
+ node.addEventListener('pointerdown', press, capture);
87
+ node.addEventListener('pointerup', guard, capture);
88
+ node.addEventListener('click', guard, capture);
89
+ return () => {
90
+ node.removeEventListener('pointerdown', press, capture);
91
+ node.removeEventListener('pointerup', guard, capture);
92
+ node.removeEventListener('click', guard, capture);
93
+ };
94
+ }
95
+ </script>
96
+
97
+ <!--
98
+ The bottom sheet every overlay uses on compact widths (BRIEF, Apps and
99
+ phones: Overlays): a scrim, then a panel at the bottom edge with a handle,
100
+ a caps title, a close button, the body and an optional footer. It opens at
101
+ half the screen at most, with no drag to expand; the body scrolls inside.
102
+ A swipe down dismisses it (internal/swipe-dismiss.ts). While the visual
103
+ viewport is shorter than the layout viewport (the on-screen keyboard), it
104
+ sits on top of it and may fill the space above it. The press that opened
105
+ it does not act inside it.
106
+
107
+ Render it inside the Bits UI content's `child` snippet and spread the
108
+ snippet's `props` onto it when the panel is the content element (a
109
+ dialog), or onto an element in the body when the content is a list (a
110
+ menu, a select): then pass `onInteractOutside` to the content and call
111
+ `preventDefault()` for presses inside `ref`, so the title row does not
112
+ count as outside.
113
+ -->
114
+ <Scrim {open} {layer} />
115
+ {#if open}
116
+ {@const lifted = viewport.current}
117
+ <div
118
+ bind:this={ref}
119
+ {...rest}
120
+ class={['hp-sheet', className]}
121
+ data-layer={layer}
122
+ style:bottom={lifted ? `calc(100% - ${lifted.bottom}px)` : undefined}
123
+ style:padding-bottom={lifted ? '0px' : undefined}
124
+ style:--sheet-max-height={lifted
125
+ ? `calc(${lifted.height}px - var(--header-height) - env(safe-area-inset-top))`
126
+ : undefined}
127
+ transition:sheet
128
+ {@attach ignoreOpeningPress}
129
+ {@attach swipeDismiss(() => onclose())}
130
+ >
131
+ <div class="hp-sheet-head">
132
+ <span class="hp-sheet-handle" aria-hidden="true"></span>
133
+ <svelte:element this={titleTag} id={titleId} class="hp-sheet-title">{title}</svelte:element>
134
+ <button type="button" class="hp-sheet-close" aria-label={closeLabel} onclick={() => onclose()}>
135
+ <svg
136
+ viewBox="0 0 16 16"
137
+ fill="none"
138
+ stroke="currentColor"
139
+ stroke-width="1.5"
140
+ stroke-linecap="square"
141
+ aria-hidden="true"
142
+ >
143
+ <path d="M3 3l10 10M13 3L3 13" />
144
+ </svg>
145
+ </button>
146
+ </div>
147
+ <div class="hp-sheet-body" bind:this={body}>
148
+ {@render children()}
149
+ </div>
150
+ {#if footer}
151
+ <div class="hp-sheet-footer">
152
+ {@render footer()}
153
+ </div>
154
+ {/if}
155
+ </div>
156
+ {/if}
157
+
158
+ <style>
159
+ /* While a floating list is open Bits UI sets pointer-events: none on
160
+ <body>; the panel takes presses again. */
161
+ .hp-sheet {
162
+ /* The height cap: half the screen, or the room above the on-screen
163
+ keyboard while it is up (set inline). A list that must keep its
164
+ height while it filters asks for this. */
165
+ --sheet-max-height: 50dvh;
166
+
167
+ position: fixed;
168
+ inset: auto 0 0;
169
+ z-index: var(--layer-dialog);
170
+ display: flex;
171
+ flex-direction: column;
172
+ box-sizing: border-box;
173
+ /* Opens at half the screen at most; the body scrolls inside. */
174
+ max-height: var(--sheet-max-height);
175
+ padding: 0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
176
+ border-top: 1px solid var(--border-control);
177
+ background: var(--canvas);
178
+ color: var(--text);
179
+ overscroll-behavior: contain;
180
+ pointer-events: auto;
181
+ }
182
+ .hp-sheet[data-layer='popover'] {
183
+ z-index: var(--layer-popover);
184
+ }
185
+
186
+ /* The grab area: handle, title and close button. The swipe owns touches
187
+ here; the body below keeps its scroll until it reaches the top. */
188
+ .hp-sheet-head {
189
+ flex: none;
190
+ display: grid;
191
+ grid-template-columns: minmax(0, 1fr) auto;
192
+ align-items: start;
193
+ padding: 0 var(--space-1) 0 var(--gutter);
194
+ touch-action: none;
195
+ }
196
+ .hp-sheet-handle {
197
+ grid-column: 1 / -1;
198
+ justify-self: center;
199
+ width: var(--space-8);
200
+ height: var(--space-1);
201
+ margin-top: var(--space-2);
202
+ background: var(--border-control);
203
+ }
204
+ /* Tracked caps at the compact view-title size. A long title wraps (200
205
+ percent zoom); the padding centres its first line on the 44px close
206
+ button, so a single line sits where it always did. The clip stays: it
207
+ keeps the first glyph's antialiasing inside the text edge, as before. */
208
+ .hp-sheet-title {
209
+ overflow: hidden;
210
+ padding-block: calc((44px - 1lh) / 2);
211
+ font-size: var(--type-body);
212
+ font-weight: 500;
213
+ line-height: 24px;
214
+ letter-spacing: var(--label-tracking);
215
+ text-transform: var(--label-transform);
216
+ overflow-wrap: anywhere;
217
+ }
218
+ .hp-sheet-close {
219
+ display: grid;
220
+ place-items: center;
221
+ width: 44px;
222
+ height: 44px;
223
+ padding: 0;
224
+ border: 0;
225
+ background: transparent;
226
+ color: var(--text);
227
+ cursor: pointer;
228
+ -webkit-tap-highlight-color: transparent;
229
+ }
230
+ .hp-sheet-close svg {
231
+ width: var(--space-4);
232
+ height: var(--space-4);
233
+ }
234
+ .hp-sheet-close:active {
235
+ background: var(--surface-raised);
236
+ }
237
+ @media (hover: hover) and (pointer: fine) {
238
+ .hp-sheet-close {
239
+ transition: background-color var(--dur-hover) var(--settle);
240
+ }
241
+ .hp-sheet-close:hover {
242
+ background: var(--surface-hover);
243
+ }
244
+ .hp-sheet-close:active {
245
+ background: var(--surface-raised);
246
+ transition-duration: var(--dur-press);
247
+ }
248
+ }
249
+
250
+ /* A flex column, so a list inside can take the rest of the height
251
+ (flex: 1 1 auto; min-height: 0) and scroll on its own. */
252
+ .hp-sheet-body {
253
+ display: flex;
254
+ flex-direction: column;
255
+ flex: 1 1 auto;
256
+ min-height: 0;
257
+ overflow-y: auto;
258
+ overscroll-behavior: contain;
259
+ }
260
+ .hp-sheet-footer {
261
+ flex: none;
262
+ }
263
+ </style>
package/dist/fonts.css CHANGED
@@ -1,71 +1,71 @@
1
- /* Hardpoint faces. Import once, before tokens.css. An app that already serves
2
- Geist (omp-remote) can skip this file and keep its own @font-face rules. */
3
-
4
- /* Geist: UI, reading text and numbers. Latin subset, variable 100–900. */
5
- @font-face {
6
- font-family: 'Geist';
7
- font-style: normal;
8
- font-weight: 100 900;
9
- font-display: swap;
10
- src: url('./fonts/geist-latin-variable.woff2') format('woff2');
11
- }
12
-
13
- /* Geist Mono: identifiers, hashes, timestamps and code. */
14
- @font-face {
15
- font-family: 'Geist Mono';
16
- font-style: normal;
17
- font-weight: 100 900;
18
- font-display: swap;
19
- src: url('./fonts/geist-mono-latin-variable.woff2') format('woff2');
20
- }
21
-
22
- /* Geist Pixel Square: the wordmark, one display line per screen and the
23
- spinner. Single weight. Crisp only at multiples of 26.32px.
24
- On Windows, Chrome spreads every vertical edge over two pixels in all
25
- modes (DirectWrite; no CSS switch changes it); horizontal edges stay
26
- sharp. macOS, iOS and Android are unverified: check them on a device. */
27
- @font-face {
28
- font-family: 'Geist Pixel Square';
29
- font-style: normal;
30
- font-weight: 400;
31
- font-display: swap;
32
- src: url('./fonts/geist-pixel-square.woff2') format('woff2');
33
- }
34
-
35
- /* Fallbacks sized to Geist and Geist Mono, so text keeps its width and
36
- baseline when the real face arrives (font-display: swap). size-adjust is
37
- the real face's width over the local font's, measured in Chromium over
38
- every Geist text node of 13 docs pages at 1440 and 390 px wide
39
- (2026-11-11). One value per font: a separate weight-500 Arial face
40
- measured worse, because at 500 caps want 95.6% and lowercase 105.9%.
41
- The overrides give Geist's ascent (1005) and descent (295) at that size.
42
- Characters outside Geist's Latin subset also come from these faces. */
43
- @font-face {
44
- font-family: 'Geist Fallback Arial';
45
- src: local('Arial'), local('ArialMT'), local('Liberation Sans'), local('Arimo');
46
- size-adjust: 102.8%;
47
- ascent-override: 97.76%;
48
- descent-override: 28.7%;
49
- line-gap-override: 0%;
50
- }
51
-
52
- /* Android has no Arial; Roboto is its sans-serif. Unverified on a device. */
53
- @font-face {
54
- font-family: 'Geist Fallback Roboto';
55
- src: local('Roboto'), local('Roboto Regular'), local('Roboto-Regular');
56
- size-adjust: 103.1%;
57
- ascent-override: 97.48%;
58
- descent-override: 28.61%;
59
- line-gap-override: 0%;
60
- }
61
-
62
- /* Consolas is 0.55em wide; Geist Mono, Menlo, Courier New and Android's
63
- monospace are 0.6em, so only Consolas needs a face. */
64
- @font-face {
65
- font-family: 'Geist Mono Fallback';
66
- src: local('Consolas');
67
- size-adjust: 109.13%;
68
- ascent-override: 92.09%;
69
- descent-override: 27.03%;
70
- line-gap-override: 0%;
71
- }
1
+ /* Hardpoint faces. Import once, before tokens.css. An app that already serves
2
+ Geist (omp-remote) can skip this file and keep its own @font-face rules. */
3
+
4
+ /* Geist: UI, reading text and numbers. Latin subset, variable 100–900. */
5
+ @font-face {
6
+ font-family: 'Geist';
7
+ font-style: normal;
8
+ font-weight: 100 900;
9
+ font-display: swap;
10
+ src: url('./fonts/geist-latin-variable.woff2') format('woff2');
11
+ }
12
+
13
+ /* Geist Mono: identifiers, hashes, timestamps and code. */
14
+ @font-face {
15
+ font-family: 'Geist Mono';
16
+ font-style: normal;
17
+ font-weight: 100 900;
18
+ font-display: swap;
19
+ src: url('./fonts/geist-mono-latin-variable.woff2') format('woff2');
20
+ }
21
+
22
+ /* Geist Pixel Square: the wordmark, one display line per screen and the
23
+ spinner. Single weight. Crisp only at multiples of 26.32px.
24
+ On Windows, Chrome spreads every vertical edge over two pixels in all
25
+ modes (DirectWrite; no CSS switch changes it); horizontal edges stay
26
+ sharp. macOS, iOS and Android are unverified: check them on a device. */
27
+ @font-face {
28
+ font-family: 'Geist Pixel Square';
29
+ font-style: normal;
30
+ font-weight: 400;
31
+ font-display: swap;
32
+ src: url('./fonts/geist-pixel-square.woff2') format('woff2');
33
+ }
34
+
35
+ /* Fallbacks sized to Geist and Geist Mono, so text keeps its width and
36
+ baseline when the real face arrives (font-display: swap). size-adjust is
37
+ the real face's width over the local font's, measured in Chromium over
38
+ every Geist text node of 13 docs pages at 1440 and 390 px wide
39
+ (2026-11-11). One value per font: a separate weight-500 Arial face
40
+ measured worse, because at 500 caps want 95.6% and lowercase 105.9%.
41
+ The overrides give Geist's ascent (1005) and descent (295) at that size.
42
+ Characters outside Geist's Latin subset also come from these faces. */
43
+ @font-face {
44
+ font-family: 'Geist Fallback Arial';
45
+ src: local('Arial'), local('ArialMT'), local('Liberation Sans'), local('Arimo');
46
+ size-adjust: 102.8%;
47
+ ascent-override: 97.76%;
48
+ descent-override: 28.7%;
49
+ line-gap-override: 0%;
50
+ }
51
+
52
+ /* Android has no Arial; Roboto is its sans-serif. Unverified on a device. */
53
+ @font-face {
54
+ font-family: 'Geist Fallback Roboto';
55
+ src: local('Roboto'), local('Roboto Regular'), local('Roboto-Regular');
56
+ size-adjust: 103.1%;
57
+ ascent-override: 97.48%;
58
+ descent-override: 28.61%;
59
+ line-gap-override: 0%;
60
+ }
61
+
62
+ /* Consolas is 0.55em wide; Geist Mono, Menlo, Courier New and Android's
63
+ monospace are 0.6em, so only Consolas needs a face. */
64
+ @font-face {
65
+ font-family: 'Geist Mono Fallback';
66
+ src: local('Consolas');
67
+ size-adjust: 109.13%;
68
+ ascent-override: 92.09%;
69
+ descent-override: 27.03%;
70
+ line-gap-override: 0%;
71
+ }