@lyeve-labs/ui-kit 0.17.1 → 0.19.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 (65) hide show
  1. package/README.md +27 -17
  2. package/dist/components/AccordionItem.svelte +2 -2
  3. package/dist/components/AccountMenu.svelte +1 -1
  4. package/dist/components/AppShell.svelte +22 -8
  5. package/dist/components/Autocomplete.svelte +13 -4
  6. package/dist/components/AvatarGroup.svelte +2 -2
  7. package/dist/components/Breadcrumb.svelte +1 -1
  8. package/dist/components/Button.svelte +34 -9
  9. package/dist/components/ButtonGroup.svelte +3 -3
  10. package/dist/components/Card.svelte +2 -1
  11. package/dist/components/Checkbox.svelte +1 -1
  12. package/dist/components/CheckboxGroup.svelte +1 -1
  13. package/dist/components/Collapsible.svelte +4 -4
  14. package/dist/components/CopyButton.svelte +1 -1
  15. package/dist/components/DatePicker.svelte +3 -3
  16. package/dist/components/DateTimePicker.svelte +1 -1
  17. package/dist/components/Drawer.svelte +7 -27
  18. package/dist/components/Dropdown.svelte +197 -11
  19. package/dist/components/Dropdown.svelte.d.ts +12 -0
  20. package/dist/components/Field.svelte +1 -1
  21. package/dist/components/Input.svelte +1 -1
  22. package/dist/components/Label.svelte +1 -1
  23. package/dist/components/Logo.svelte +77 -0
  24. package/dist/components/Logo.svelte.d.ts +35 -0
  25. package/dist/components/Modal.svelte +2 -15
  26. package/dist/components/MultiSelect.svelte +16 -7
  27. package/dist/components/NumberInput.svelte +3 -3
  28. package/dist/components/PageHeader.svelte +23 -1
  29. package/dist/components/Pagination.svelte +164 -65
  30. package/dist/components/Pagination.svelte.d.ts +39 -2
  31. package/dist/components/Panel.svelte +1 -1
  32. package/dist/components/PasswordInput.svelte +4 -4
  33. package/dist/components/Radio.svelte +1 -1
  34. package/dist/components/RadioGroup.svelte +1 -1
  35. package/dist/components/SearchInput.svelte +13 -4
  36. package/dist/components/SegmentedControl.svelte +3 -1
  37. package/dist/components/Select.svelte +6 -6
  38. package/dist/components/SidebarNav.svelte +20 -5
  39. package/dist/components/Table.svelte +256 -10
  40. package/dist/components/Table.svelte.d.ts +44 -0
  41. package/dist/components/Tabs.svelte +1 -1
  42. package/dist/components/Tag.svelte +1 -1
  43. package/dist/components/Textarea.svelte +1 -1
  44. package/dist/components/ThemeToggle.svelte +76 -13
  45. package/dist/components/ThemeToggle.svelte.d.ts +9 -0
  46. package/dist/components/TimePicker.svelte +2 -2
  47. package/dist/components/Toaster.svelte +3 -15
  48. package/dist/components/Toggle.svelte +10 -4
  49. package/dist/components/Tooltip.svelte +1 -1
  50. package/dist/components/TreeView.svelte +4 -4
  51. package/dist/components/dialog/Dialog.svelte +10 -5
  52. package/dist/components/dialog/dialog-manager.svelte.d.ts +20 -1
  53. package/dist/components/dialog/dialog-manager.svelte.js +13 -2
  54. package/dist/index.d.ts +5 -2
  55. package/dist/index.js +6 -2
  56. package/dist/internal/href.d.ts +13 -0
  57. package/dist/internal/href.js +27 -0
  58. package/dist/internal/layout.js +11 -2
  59. package/dist/internal/panel.d.ts +1 -1
  60. package/dist/internal/panel.js +2 -2
  61. package/dist/styles/theme.css +288 -0
  62. package/dist/utils/theme.d.ts +68 -3
  63. package/dist/utils/theme.js +118 -3
  64. package/package.json +20 -22
  65. package/src/lib/styles/theme.css +288 -0
@@ -5,17 +5,46 @@
5
5
  * The `<ThemeToggle />` component uses these under the hood; you can also
6
6
  * call them directly when you need to set the theme from a layout, route
7
7
  * loader, or an inline `<script>` that runs before paint.
8
+ *
9
+ * Two vocabularies, and the distinction is the whole point:
10
+ *
11
+ * Theme what is painted. `dark` or `light`, and it is what
12
+ * `data-theme` holds and what every palette rule keys off.
13
+ * ThemePreference what the reader asked for. `dark`, `light`, or `system`,
14
+ * which is a standing instruction to follow the operating
15
+ * system rather than a colour.
16
+ *
17
+ * The kit resolved a preference in exactly one place, the pre-paint script,
18
+ * and nothing else could read it back. A control could therefore offer two
19
+ * states and no third, so a reader who wanted the operating system's choice
20
+ * had to keep re-picking it by hand, and one console forked ThemeToggle rather
21
+ * than ship that. `system` is now a value the storage key holds and every
22
+ * helper here understands.
23
+ */
24
+ /**
25
+ * The one key every surface in the estate reads and writes.
26
+ *
27
+ * Not exported. A surface that needs the name has `getThemePreference` and
28
+ * `setThemePreference`; a surface that spells the key itself is how two of
29
+ * them ended up on different keys, with the control writing one and the
30
+ * pre-paint script reading the other.
8
31
  */
9
32
  const STORAGE_KEY = 'lyeve-theme';
33
+ /** The order a control cycles through, and the order a picker lists. */
34
+ export const THEME_PREFERENCES = ['light', 'dark', 'system'];
35
+ function isPreference(value) {
36
+ return value === 'light' || value === 'dark' || value === 'system';
37
+ }
10
38
  /**
11
39
  * Inline-script payload that should run *before* the first paint to avoid
12
40
  * the flash-of-wrong-theme. Drop the returned string into a `<script>` tag
13
41
  * in the `<head>` of your app shell.
14
42
  *
15
43
  * Picks the theme in this order:
16
- * 1. `localStorage["lyeve-theme"]` if set
17
- * 2. The user's OS preference (`prefers-color-scheme`)
18
- * 3. Dark (the default brand palette)
44
+ * 1. `localStorage["lyeve-theme"]` when it holds `'light'` or `'dark'`
45
+ * 2. The user's OS preference (`prefers-color-scheme`), which is also what
46
+ * the stored value `'system'` asks for
47
+ * 3. Dark (the default brand palette), including when storage throws
19
48
  */
20
49
  export function themeBootScript() {
21
50
  return `(function () {
@@ -29,12 +58,69 @@ export function themeBootScript() {
29
58
  }
30
59
  })();`;
31
60
  }
61
+ /**
62
+ * What the operating system is asking for.
63
+ *
64
+ * Dark unless the OS says light, which is the same fallback the pre-paint
65
+ * script uses and the same one a browser without `matchMedia` gets.
66
+ */
67
+ export function systemTheme() {
68
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function')
69
+ return 'dark';
70
+ return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
71
+ }
72
+ /** The theme a preference resolves to right now. */
73
+ export function resolveTheme(preference) {
74
+ return preference === 'system' ? systemTheme() : preference;
75
+ }
76
+ /**
77
+ * What the reader asked for, from storage.
78
+ *
79
+ * `system` when nothing is stored, which is the default this kit resolves by
80
+ * and not a guess: an unanswered question is answered by the operating system,
81
+ * because the reader already answered it there.
82
+ */
83
+ export function getThemePreference() {
84
+ if (typeof localStorage === 'undefined')
85
+ return 'system';
86
+ try {
87
+ const stored = localStorage.getItem(STORAGE_KEY);
88
+ return isPreference(stored) ? stored : 'system';
89
+ }
90
+ catch {
91
+ // Storage may be disabled (private mode, quota). Follow the OS.
92
+ return 'system';
93
+ }
94
+ }
32
95
  /** Read the current theme. SSR-safe; returns `'dark'` on the server. */
33
96
  export function getTheme() {
34
97
  if (typeof document === 'undefined')
35
98
  return 'dark';
36
99
  return document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
37
100
  }
101
+ /**
102
+ * Record a preference, apply what it resolves to, and return the applied theme.
103
+ *
104
+ * `system` is stored under its own name rather than by clearing the key. The
105
+ * two are indistinguishable on the next read otherwise, and they are not the
106
+ * same thing: one reader chose to follow the OS and the other has not chosen
107
+ * anything yet. Both resolve identically today; only one of them is a decision
108
+ * a later default may not override.
109
+ */
110
+ export function setThemePreference(preference) {
111
+ const theme = resolveTheme(preference);
112
+ if (typeof document !== 'undefined') {
113
+ document.documentElement.setAttribute('data-theme', theme);
114
+ }
115
+ try {
116
+ localStorage.setItem(STORAGE_KEY, preference);
117
+ }
118
+ catch {
119
+ // Storage may be disabled (private mode, quota); the attribute is applied
120
+ // either way, so the session the reader is in still honours the choice.
121
+ }
122
+ return theme;
123
+ }
38
124
  /** Apply a theme and persist the choice to `localStorage`. */
39
125
  export function setTheme(theme) {
40
126
  if (typeof document === 'undefined')
@@ -47,9 +133,38 @@ export function setTheme(theme) {
47
133
  // Storage may be disabled (private mode, quota); fall through silently.
48
134
  }
49
135
  }
136
+ /**
137
+ * The next preference in `order`, wrapping at the end.
138
+ *
139
+ * Takes the list so a surface that offers only two states cycles through only
140
+ * those two. A preference that is not in the list starts the cycle at its
141
+ * first entry rather than falling out of it.
142
+ */
143
+ export function nextThemePreference(current, order = THEME_PREFERENCES) {
144
+ if (order.length === 0)
145
+ return current;
146
+ const at = order.indexOf(current);
147
+ return at === -1 ? order[0] : order[(at + 1) % order.length];
148
+ }
50
149
  /** Flip between dark and light. Returns the new theme. */
51
150
  export function toggleTheme() {
52
151
  const next = getTheme() === 'dark' ? 'light' : 'dark';
53
152
  setTheme(next);
54
153
  return next;
55
154
  }
155
+ /**
156
+ * Call `onChange` whenever the operating system's choice changes. Returns the
157
+ * unsubscribe function, and a no-op one where there is no `matchMedia`.
158
+ *
159
+ * A page resolving `system` has to repaint when the OS flips at dusk. Without
160
+ * this the preference is honoured once, at load, and reads as ignored for the
161
+ * rest of the session.
162
+ */
163
+ export function watchSystemTheme(onChange) {
164
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function')
165
+ return () => { };
166
+ const query = window.matchMedia('(prefers-color-scheme: light)');
167
+ const handler = () => onChange(query.matches ? 'light' : 'dark');
168
+ query.addEventListener('change', handler);
169
+ return () => query.removeEventListener('change', handler);
170
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyeve-labs/ui-kit",
3
- "version": "0.17.1",
3
+ "version": "0.19.0",
4
4
  "description": "A clean, accessible, palette-aware Svelte 5 component library. The design system behind LyEve.",
5
5
  "license": "MIT",
6
6
  "author": "LyEve Labs <hello@lyeve.com>",
@@ -23,25 +23,9 @@
23
23
  ],
24
24
  "type": "module",
25
25
  "sideEffects": false,
26
- "packageManager": "pnpm@11.25.0",
27
26
  "engines": {
28
27
  "node": ">=20"
29
28
  },
30
- "scripts": {
31
- "build": "pnpm version:check && svelte-package && publint && node scripts/check-dist.mjs",
32
- "version:sync": "node scripts/sync-version.mjs",
33
- "version:check": "node scripts/sync-version.mjs --check",
34
- "package": "pnpm version:check && svelte-package && publint",
35
- "prepublishOnly": "pnpm package",
36
- "check": "svelte-check --tsconfig ./tsconfig.json",
37
- "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
38
- "test": "vitest run",
39
- "test:watch": "vitest",
40
- "test:coverage": "vitest run --coverage",
41
- "test:ct": "playwright test --config playwright-ct.config.ts",
42
- "format": "prettier --write .",
43
- "format:check": "prettier --check ."
44
- },
45
29
  "files": [
46
30
  "dist",
47
31
  "src/lib/styles",
@@ -60,15 +44,15 @@
60
44
  "./package.json": "./package.json"
61
45
  },
62
46
  "peerDependencies": {
63
- "svelte": "^5.0.0"
47
+ "@lucide/svelte": ">=0.511.0 <2",
48
+ "svelte": "^5.0.0",
49
+ "tailwindcss": "^4"
64
50
  },
65
51
  "publishConfig": {
66
52
  "access": "public"
67
53
  },
68
- "dependencies": {
69
- "@lucide/svelte": "^0.511.0"
70
- },
71
54
  "devDependencies": {
55
+ "@lucide/svelte": "^0.511.0",
72
56
  "@sveltejs/package": "^2.5.7",
73
57
  "@sveltejs/vite-plugin-svelte": "^4.0.4",
74
58
  "@testing-library/svelte": "^5.3.1",
@@ -82,5 +66,19 @@
82
66
  "svelte-check": "^4.4.8",
83
67
  "typescript": "^5.9.3",
84
68
  "vitest": "^2.1.9"
69
+ },
70
+ "scripts": {
71
+ "build": "pnpm version:check && svelte-package && publint && node scripts/check-dist.mjs",
72
+ "version:sync": "node scripts/sync-version.mjs",
73
+ "version:check": "node scripts/sync-version.mjs --check",
74
+ "package": "pnpm version:check && svelte-package && publint",
75
+ "check": "svelte-check --tsconfig ./tsconfig.json",
76
+ "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
77
+ "test": "vitest run",
78
+ "test:watch": "vitest",
79
+ "test:coverage": "vitest run --coverage",
80
+ "test:ct": "playwright test --config playwright-ct.config.ts",
81
+ "format": "prettier --write .",
82
+ "format:check": "prettier --check ."
85
83
  }
86
- }
84
+ }
@@ -79,6 +79,61 @@
79
79
  --radius-xl: 16px;
80
80
  --radius-2xl: 24px;
81
81
 
82
+ /* -- Elevation - the stacking order of everything that floats --- *
83
+ * Every floating surface in the kit carried a bare z-50, so which
84
+ * one won was decided by document order: a Dropdown opened after a
85
+ * Modal painted over it, and the same two in the other order did
86
+ * not. The layer a surface belongs to is a property of the surface,
87
+ * so it is named here once and each component asks for its layer by
88
+ * name.
89
+ *
90
+ * The ramp starts at 100 and leaves everything below it to the
91
+ * consuming app. The admin's own chrome sits at z-10 and z-20
92
+ * today, and a kit layer landing on either number would tie with
93
+ * it and resolve, again, by document order.
94
+ *
95
+ * The steps are 100 apart because one of these layers holds a
96
+ * stack rather than a surface: the dialog manager offsets each
97
+ * dialog above the one it opened over, and a ramp in steps of ten
98
+ * would put the eleventh dialog through the ceiling of its own
99
+ * layer and under the tooltip above it.
100
+ *
101
+ * tooltip sits above modal because a tooltip on a control inside a
102
+ * dialog is drawn while that dialog is the topmost surface. toast
103
+ * sits above tooltip because it reports the outcome of the action
104
+ * the tooltip described. skip-link is above everything: it is the
105
+ * first focusable thing in the document and nothing may cover it.
106
+ *
107
+ * No sticky rung is declared. Nothing in the kit is sticky or
108
+ * fixed below the dropdown layer, and a token no rule reads is the
109
+ * defect this scale was written to end. */
110
+ --z-index-dropdown: 100; /* a panel anchored to its trigger */
111
+ --z-index-overlay: 200; /* the scrim behind a drawer or modal */
112
+ --z-index-drawer: 300; /* a panel docked to a viewport edge */
113
+ --z-index-modal: 400; /* a centred dialog, and the stack */
114
+ --z-index-tooltip: 500; /* a label attached to a control */
115
+ --z-index-toast: 600; /* transient status */
116
+ --z-index-skip-link: 700; /* the first tab stop in the document */
117
+
118
+ /* -- Motion - how long a transition or an entrance runs -------- *
119
+ * Four durations shipped as literals inside the animate-[...]
120
+ * utility of the component that used them, so the only way to
121
+ * read the kit's motion was to grep four components. They are
122
+ * stated here at the values they already ran at.
123
+ *
124
+ * Tailwind builds no utility from a --duration-* token: there is
125
+ * no such theme namespace, and `duration-collapse` compiles to
126
+ * nothing at all. Each one is read through var() from an
127
+ * arbitrary utility instead, which is why they are listed in
128
+ * VAR_ONLY in theme-tokens.test.ts.
129
+ *
130
+ * The reduced-motion block at the foot of this file overrides
131
+ * every one of them and stays the authority on whether they run. */
132
+ --duration-modal-in: 120ms; /* Modal, the centred entrance */
133
+ --duration-toast-in: 140ms; /* Toaster, one toast arriving */
134
+ --duration-drawer-in: 150ms; /* Drawer, either edge */
135
+ --duration-collapse: 200ms; /* Accordion and Collapsible */
136
+
82
137
  /* ── Typography · families ───────────────────────────────────── */
83
138
  --font-sans:
84
139
  'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
@@ -96,6 +151,8 @@
96
151
  * Body 16px 1.55 0 400 Default body copy *
97
152
  * Caption 13px 1.50 0 400 Labels, metadata *
98
153
  * Mono 18px 1.50 0 500 Code, eyebrows *
154
+ * The ramp the guideline states, for marketing surfaces. *
155
+ * The scale the console renders is declared below it. *
99
156
  * */
100
157
  --text-display: 4rem; /* 64px */
101
158
  --text-h1: 2.75rem; /* 44px */
@@ -118,6 +175,57 @@
118
175
  --tracking-h2: -0.02em;
119
176
  --tracking-h3: -0.015em;
120
177
 
178
+ /* -- Typography - console type scale --------------------------- *
179
+ * Token Size Line Where it renders *
180
+ * text-xs 12px 16px labels, table heads, metadata, hints*
181
+ * text-sm 14px 20px body copy and every control *
182
+ * text-base 16px 24px a card title, the large button *
183
+ * text-lg 18px 28px a dialog title, a small stat value *
184
+ * text-xl 20px 28px the large wordmark *
185
+ * text-2xl 24px 32px a stat value, an empty-state glyph *
186
+ *
187
+ * Two scales, on purpose, and this is the one the components
188
+ * are drawn in.
189
+ *
190
+ * The ramp above is the brand guideline's, and it is a marketing
191
+ * ramp: it runs 64 / 44 / 32 / 22 / 16 / 13, holds nothing
192
+ * between 16px and 22px, and has no step at 14px at all. A
193
+ * console's body copy is 14px and its labels are 12px, both
194
+ * below the ramp's floor, so adopting the ramp wholesale would
195
+ * have meant restyling every screen to fit a scale drawn for a
196
+ * landing page. The ramp keeps its steps for the surfaces it was
197
+ * drawn for, and the two overlap at exactly the two steps a
198
+ * console does reach: PageHeader takes H2 for the page title and
199
+ * SectionHeading takes H3 for a section head.
200
+ *
201
+ * These are Tailwind's own steps at Tailwind's own values,
202
+ * stated here rather than inherited silently. Nothing renders
203
+ * one pixel differently for it. What changes is that the scale
204
+ * the components are built from is now declared beside the one
205
+ * they are not, so a designer reading this file sees both, and
206
+ * moving console body copy to 15px is one line here instead of
207
+ * 49 class attributes. Restated rather than renamed: an app
208
+ * writing `text-sm` in its own markup and a component writing it
209
+ * in the kit's must mean the same size, and a private spelling
210
+ * of the same 14px would be a second name for one thing.
211
+ *
212
+ * A step is declared with its leading, because a --text-* token
213
+ * sets font-size and nothing else. Only the six steps the kit
214
+ * actually renders are declared; the larger ones a marketing
215
+ * page reaches for stay as Tailwind ships them. */
216
+ --text-xs: 0.75rem; /* 12px */
217
+ --text-xs--line-height: calc(1 / 0.75); /* 16px */
218
+ --text-sm: 0.875rem; /* 14px */
219
+ --text-sm--line-height: calc(1.25 / 0.875); /* 20px */
220
+ --text-base: 1rem; /* 16px */
221
+ --text-base--line-height: calc(1.5 / 1); /* 24px */
222
+ --text-lg: 1.125rem; /* 18px */
223
+ --text-lg--line-height: calc(1.75 / 1.125); /* 28px */
224
+ --text-xl: 1.25rem; /* 20px */
225
+ --text-xl--line-height: calc(1.75 / 1.25); /* 28px */
226
+ --text-2xl: 1.5rem; /* 24px */
227
+ --text-2xl--line-height: calc(2 / 1.5); /* 32px */
228
+
121
229
  /* ── Spacing grid · 4px base (semantic shortcuts) ────────────── *
122
230
  * Tailwind's default 0.25rem (4px) grid is unchanged. *
123
231
  * These tokens add named slots for common component spacing. */
@@ -181,6 +289,65 @@ html[data-theme='light'] {
181
289
  --color-danger: #ab1a1a;
182
290
  }
183
291
 
292
+ /*
293
+ * The entrance keyframes, declared here and not in the components that play
294
+ * them.
295
+ *
296
+ * Modal, Drawer and Toaster each held their own `@keyframes` in a scoped
297
+ * `<style>` block and started it from a Tailwind `animate-[...]` class. Svelte
298
+ * renames a scoped keyframe to `svelte-<hash>-<name>` and rewrites only the
299
+ * references inside that same style block, so the class asked the browser for
300
+ * `modal-in` while the only rule declared was `svelte-ta60gp-modal-in`. An
301
+ * animation-name that resolves to nothing is not an error: the element simply
302
+ * appears, and all four entrances had been dead in every consumer.
303
+ *
304
+ * A keyframe named from a class has to be global, and the file every consumer
305
+ * already imports is the one place it can be global from.
306
+ */
307
+ @keyframes modal-in {
308
+ from {
309
+ opacity: 0;
310
+ transform: translateY(6px) scale(0.98);
311
+ }
312
+ to {
313
+ opacity: 1;
314
+ transform: translateY(0) scale(1);
315
+ }
316
+ }
317
+
318
+ @keyframes drawer-in-right {
319
+ from {
320
+ opacity: 0;
321
+ transform: translateX(16px);
322
+ }
323
+ to {
324
+ opacity: 1;
325
+ transform: translateX(0);
326
+ }
327
+ }
328
+
329
+ @keyframes drawer-in-left {
330
+ from {
331
+ opacity: 0;
332
+ transform: translateX(-16px);
333
+ }
334
+ to {
335
+ opacity: 1;
336
+ transform: translateX(0);
337
+ }
338
+ }
339
+
340
+ @keyframes toast-in {
341
+ from {
342
+ opacity: 0;
343
+ transform: translateX(12px);
344
+ }
345
+ to {
346
+ opacity: 1;
347
+ transform: translateX(0);
348
+ }
349
+ }
350
+
184
351
  @layer base {
185
352
  html {
186
353
  background-color: var(--color-ink);
@@ -238,3 +405,124 @@ html[data-theme='light'] {
238
405
  }
239
406
  }
240
407
  }
408
+
409
+ /*
410
+ * Print.
411
+ *
412
+ * The estate had no `@media print` rule anywhere, and three surfaces print:
413
+ * the portal prints an invoice, the ops console prints an audit log, the admin
414
+ * prints a subject-access export. All three printed the sidebar, the theme
415
+ * toggle and the nav, in a dark palette, and all three printed one screenful
416
+ * and stopped, because the shell is `h-screen` with the content column set to
417
+ * `overflow-auto` and paper has neither a viewport nor a scrollbar.
418
+ *
419
+ * These rules are deliberately outside `@layer base`. An unlayered declaration
420
+ * beats every layered one whatever its specificity, so a `bg-surface` utility
421
+ * on a card cannot put the dark surface back.
422
+ *
423
+ * Three markers, each stating what paper needs rather than what an element is:
424
+ *
425
+ * data-print="hide" chrome that exists to navigate. Paper cannot be
426
+ * navigated, so it is noise on every page.
427
+ * data-print="unclip" a box that clips on screen because the screen is a
428
+ * fixed height. On paper it must give up its height and
429
+ * its overflow or the content past the first page is
430
+ * never printed.
431
+ * data-print="keep" a block that reads as one thing and must not be split
432
+ * across a page break.
433
+ */
434
+ @media print {
435
+ /*
436
+ * The palette, not the components.
437
+ *
438
+ * Every kit component paints from these tokens, so redefining them here
439
+ * turns the whole library monochrome without a single component knowing
440
+ * about paper. Browsers drop backgrounds when printing by default, which
441
+ * left light text on the white the printer supplies; naming white here means
442
+ * the two agree either way.
443
+ *
444
+ * The status hues collapse to black rather than to a print-safe tint. A
445
+ * colour that carries meaning on screen carries none through a mono printer,
446
+ * and a legible black label beats an unreadable coloured one.
447
+ */
448
+ html,
449
+ html[data-theme='light'] {
450
+ --color-ink: #ffffff;
451
+ --color-surface: #ffffff;
452
+ --color-surface-2: #ffffff;
453
+ --color-line: #999999;
454
+ --color-line-strong: #666666;
455
+ --color-fg: #000000;
456
+ --color-muted: #000000;
457
+ --color-faint: #000000;
458
+ --color-brand: #000000;
459
+ --color-brand-light: #000000;
460
+ --color-brand-deep: #000000;
461
+ --color-violet: #000000;
462
+ --color-success: #000000;
463
+ --color-warn: #000000;
464
+ --color-danger: #000000;
465
+ color-scheme: light;
466
+ }
467
+
468
+ html,
469
+ body {
470
+ background: #ffffff;
471
+ color: #000000;
472
+ }
473
+
474
+ [data-print='hide'] {
475
+ display: none !important;
476
+ }
477
+
478
+ /*
479
+ * height as well as overflow. The shell is `h-screen`, so releasing the
480
+ * overflow alone still caps the printed document at one viewport.
481
+ */
482
+ [data-print='unclip'] {
483
+ overflow: visible !important;
484
+ height: auto !important;
485
+ max-height: none !important;
486
+ }
487
+
488
+ [data-print='keep'],
489
+ tr,
490
+ li {
491
+ break-inside: avoid;
492
+ }
493
+
494
+ /* A table running past one page repeats its own column headings. */
495
+ thead {
496
+ display: table-header-group;
497
+ }
498
+
499
+ tfoot {
500
+ display: table-footer-group;
501
+ }
502
+
503
+ /*
504
+ * A heading orphaned at the foot of a page belongs to the section on the
505
+ * next one.
506
+ */
507
+ h1,
508
+ h2,
509
+ h3 {
510
+ break-after: avoid;
511
+ }
512
+
513
+ /*
514
+ * The target of a link, for the links a reader cannot follow from paper and
515
+ * could not guess.
516
+ *
517
+ * Absolute links only. An invoice full of "(/portal/invoices/8841)" after
518
+ * every internal link is noise, and the reader holding the paper is already
519
+ * inside that application. A caller whose absolute link is decorative, or
520
+ * whose text is the URL already, opts out per link.
521
+ */
522
+ a[href^='http']:not([data-print='bare'])::after {
523
+ content: ' <' attr(href) '>';
524
+ font-size: 0.85em;
525
+ font-weight: normal;
526
+ word-break: break-all;
527
+ }
528
+ }