@leavepulse/ui 0.12.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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/dist/component-names.d.ts +1 -0
  4. package/dist/components/LayoutCanvas.vue.d.ts +28 -0
  5. package/dist/components/LayoutNode.vue.d.ts +40 -0
  6. package/dist/components/LpAlert.vue.d.ts +44 -0
  7. package/dist/components/LpAppShell.vue.d.ts +83 -0
  8. package/dist/components/LpAutocomplete.vue.d.ts +50 -0
  9. package/dist/components/LpAvatar.vue.d.ts +20 -0
  10. package/dist/components/LpBadge.vue.d.ts +47 -0
  11. package/dist/components/LpBreadcrumbs.vue.d.ts +14 -0
  12. package/dist/components/LpButton.vue.d.ts +113 -0
  13. package/dist/components/LpCalendar.vue.d.ts +20 -0
  14. package/dist/components/LpCard.vue.d.ts +29 -0
  15. package/dist/components/LpCheckbox.vue.d.ts +28 -0
  16. package/dist/components/LpCodeBlock.vue.d.ts +39 -0
  17. package/dist/components/LpCommandPalette.vue.d.ts +42 -0
  18. package/dist/components/LpConfirmDialog.vue.d.ts +32 -0
  19. package/dist/components/LpContextMenu.vue.d.ts +30 -0
  20. package/dist/components/LpDatePicker.vue.d.ts +25 -0
  21. package/dist/components/LpDisclosure.vue.d.ts +37 -0
  22. package/dist/components/LpDivider.vue.d.ts +17 -0
  23. package/dist/components/LpDrawer.vue.d.ts +88 -0
  24. package/dist/components/LpDropdownMenu.vue.d.ts +23 -0
  25. package/dist/components/LpEmptyState.vue.d.ts +18 -0
  26. package/dist/components/LpFormField.vue.d.ts +21 -0
  27. package/dist/components/LpIcon.vue.d.ts +8 -0
  28. package/dist/components/LpInfraNode.vue.d.ts +21 -0
  29. package/dist/components/LpInput.vue.d.ts +68 -0
  30. package/dist/components/LpLaneNode.vue.d.ts +24 -0
  31. package/dist/components/LpLink.vue.d.ts +27 -0
  32. package/dist/components/LpLogViewer.vue.d.ts +92 -0
  33. package/dist/components/LpModal.vue.d.ts +43 -0
  34. package/dist/components/LpNotificationBell.vue.d.ts +51 -0
  35. package/dist/components/LpNumberField.vue.d.ts +14 -0
  36. package/dist/components/LpOtpInput.vue.d.ts +27 -0
  37. package/dist/components/LpPagination.vue.d.ts +25 -0
  38. package/dist/components/LpPasswordInput.vue.d.ts +28 -0
  39. package/dist/components/LpPhoneInput.vue.d.ts +37 -0
  40. package/dist/components/LpPopover.vue.d.ts +34 -0
  41. package/dist/components/LpProgress.vue.d.ts +12 -0
  42. package/dist/components/LpRadio.vue.d.ts +18 -0
  43. package/dist/components/LpRadioGroup.vue.d.ts +26 -0
  44. package/dist/components/LpScrollArea.vue.d.ts +24 -0
  45. package/dist/components/LpSegmented.vue.d.ts +24 -0
  46. package/dist/components/LpSelect.vue.d.ts +45 -0
  47. package/dist/components/LpServiceNode.vue.d.ts +14 -0
  48. package/dist/components/LpSidebar.vue.d.ts +85 -0
  49. package/dist/components/LpSidebarNav.vue.d.ts +34 -0
  50. package/dist/components/LpSkeleton.vue.d.ts +6 -0
  51. package/dist/components/LpSlider.vue.d.ts +23 -0
  52. package/dist/components/LpStat.vue.d.ts +22 -0
  53. package/dist/components/LpStepper.vue.d.ts +10 -0
  54. package/dist/components/LpSwitch.vue.d.ts +11 -0
  55. package/dist/components/LpTable.vue.d.ts +63 -0
  56. package/dist/components/LpTableOfContents.vue.d.ts +19 -0
  57. package/dist/components/LpTabs.vue.d.ts +45 -0
  58. package/dist/components/LpTextarea.vue.d.ts +17 -0
  59. package/dist/components/LpThemeSwitcher.vue.d.ts +40 -0
  60. package/dist/components/LpToaster.vue.d.ts +3 -0
  61. package/dist/components/LpTooltip.vue.d.ts +19 -0
  62. package/dist/components/LpTopologyCanvas.vue.d.ts +154 -0
  63. package/dist/components/LpUptimeBar.vue.d.ts +30 -0
  64. package/dist/components/codeHighlight.d.ts +8 -0
  65. package/dist/components/countries-data.d.ts +2 -0
  66. package/dist/components/countries.d.ts +20 -0
  67. package/dist/components/dropdown.d.ts +29 -0
  68. package/dist/components/sidebar.d.ts +18 -0
  69. package/dist/composables/useInputFilter.d.ts +14 -0
  70. package/dist/composables/usePillTransition.d.ts +9 -0
  71. package/dist/composables/useToast.d.ts +39 -0
  72. package/dist/countries-data-9rgqJWHo.js +203 -0
  73. package/dist/icons.d.ts +1 -0
  74. package/dist/index.css +1 -0
  75. package/dist/index.d.ts +100 -0
  76. package/dist/index.js +17600 -0
  77. package/dist/layout/registry.d.ts +26 -0
  78. package/dist/layout/tree.d.ts +75 -0
  79. package/dist/layout/useLayout.d.ts +39 -0
  80. package/dist/theme/config.d.ts +16 -0
  81. package/dist/theme/define.d.ts +12 -0
  82. package/dist/theme/presets.d.ts +20 -0
  83. package/dist/theme/tokens.d.ts +72 -0
  84. package/dist/theme/useTheme.d.ts +36 -0
  85. package/package.json +93 -0
  86. package/src/canvas.css +67 -0
  87. package/src/component-names.ts +65 -0
  88. package/src/components/LayoutCanvas.vue +76 -0
  89. package/src/components/LayoutNode.vue +248 -0
  90. package/src/components/LpAlert.vue +59 -0
  91. package/src/components/LpAppShell.vue +210 -0
  92. package/src/components/LpAutocomplete.vue +244 -0
  93. package/src/components/LpAvatar.vue +67 -0
  94. package/src/components/LpBadge.vue +32 -0
  95. package/src/components/LpBreadcrumbs.vue +35 -0
  96. package/src/components/LpButton.vue +111 -0
  97. package/src/components/LpCalendar.vue +134 -0
  98. package/src/components/LpCard.vue +71 -0
  99. package/src/components/LpCheckbox.vue +44 -0
  100. package/src/components/LpCodeBlock.vue +298 -0
  101. package/src/components/LpCommandPalette.vue +323 -0
  102. package/src/components/LpConfirmDialog.vue +49 -0
  103. package/src/components/LpContextMenu.vue +111 -0
  104. package/src/components/LpDatePicker.vue +117 -0
  105. package/src/components/LpDisclosure.vue +92 -0
  106. package/src/components/LpDivider.vue +20 -0
  107. package/src/components/LpDrawer.vue +402 -0
  108. package/src/components/LpDropdownMenu.vue +54 -0
  109. package/src/components/LpEmptyState.vue +21 -0
  110. package/src/components/LpFormField.vue +31 -0
  111. package/src/components/LpIcon.vue +49 -0
  112. package/src/components/LpInfraNode.vue +108 -0
  113. package/src/components/LpInput.vue +147 -0
  114. package/src/components/LpLaneNode.vue +48 -0
  115. package/src/components/LpLink.vue +48 -0
  116. package/src/components/LpLogViewer.vue +569 -0
  117. package/src/components/LpModal.vue +123 -0
  118. package/src/components/LpNotificationBell.vue +238 -0
  119. package/src/components/LpNumberField.vue +56 -0
  120. package/src/components/LpOtpInput.vue +81 -0
  121. package/src/components/LpPagination.vue +165 -0
  122. package/src/components/LpPasswordInput.vue +50 -0
  123. package/src/components/LpPhoneInput.vue +241 -0
  124. package/src/components/LpPopover.vue +42 -0
  125. package/src/components/LpProgress.vue +30 -0
  126. package/src/components/LpRadio.vue +28 -0
  127. package/src/components/LpRadioGroup.vue +43 -0
  128. package/src/components/LpScrollArea.vue +82 -0
  129. package/src/components/LpSegmented.vue +83 -0
  130. package/src/components/LpSelect.vue +0 -0
  131. package/src/components/LpServiceNode.vue +57 -0
  132. package/src/components/LpSidebar.vue +200 -0
  133. package/src/components/LpSidebarNav.vue +118 -0
  134. package/src/components/LpSkeleton.vue +16 -0
  135. package/src/components/LpSlider.vue +53 -0
  136. package/src/components/LpStat.vue +90 -0
  137. package/src/components/LpStepper.vue +46 -0
  138. package/src/components/LpSwitch.vue +19 -0
  139. package/src/components/LpTable.vue +252 -0
  140. package/src/components/LpTableOfContents.vue +183 -0
  141. package/src/components/LpTabs.vue +120 -0
  142. package/src/components/LpTextarea.vue +39 -0
  143. package/src/components/LpThemeSwitcher.vue +168 -0
  144. package/src/components/LpToaster.vue +216 -0
  145. package/src/components/LpTooltip.vue +30 -0
  146. package/src/components/LpTopologyCanvas.vue +370 -0
  147. package/src/components/LpUptimeBar.vue +126 -0
  148. package/src/components/codeHighlight.ts +137 -0
  149. package/src/components/countries-data.ts +207 -0
  150. package/src/components/countries.ts +67 -0
  151. package/src/components/dropdown.ts +64 -0
  152. package/src/components/sidebar.ts +26 -0
  153. package/src/composables/useInputFilter.ts +89 -0
  154. package/src/composables/usePillTransition.ts +19 -0
  155. package/src/composables/useToast.ts +92 -0
  156. package/src/icons.ts +11 -0
  157. package/src/index.css +21 -0
  158. package/src/index.ts +146 -0
  159. package/src/layout/registry.ts +44 -0
  160. package/src/layout/tree.ts +288 -0
  161. package/src/layout/useLayout.ts +134 -0
  162. package/src/nuxt.ts +22 -0
  163. package/src/theme/config.ts +48 -0
  164. package/src/theme/define.ts +50 -0
  165. package/src/theme/presets.ts +279 -0
  166. package/src/theme/tokens.ts +151 -0
  167. package/src/theme/useTheme.ts +288 -0
  168. package/src/tokens/scrollbar.css +138 -0
  169. package/src/tokens/tokens.css +333 -0
  170. package/src/tokens/utilities.css +23 -0
@@ -0,0 +1,288 @@
1
+ /*
2
+ * Theme engine. A theme is a TokenSet (plain JSON); applyTheme() writes it to
3
+ * CSS variables at runtime so every kit component re-skins live. Includes an
4
+ * anti-flash bootstrap (paint the cached theme before mount) and JSON
5
+ * serialize/parse with validation.
6
+ */
7
+ import { computed, readonly, shallowRef, type ComputedRef, type DeepReadonly, type Ref } from "vue"
8
+ import {
9
+ COLOR_VARS,
10
+ DEFAULT_SURFACE,
11
+ DENSITY_VARS,
12
+ FONT_VARS,
13
+ SHAPE_VARS,
14
+ SURFACE_VARS,
15
+ type ColorTokens,
16
+ type DensityTokens,
17
+ type FontTokens,
18
+ type ShapeTokens,
19
+ type SurfaceTokens,
20
+ type TokenSet,
21
+ } from "./tokens"
22
+
23
+ const CACHE_KEY = "leavepulse-ui-theme"
24
+
25
+ // ── self-managed theme state (singleton) ─────────────────────────────────
26
+ // The kit owns the active theme — like an i18n locale — so every consumer
27
+ // shares one source of truth instead of each wiring its own ref + bootstrap.
28
+ // Module-level (created once per app), reactive, and lazily hydrated from the
29
+ // cache on the client. SSR-safe: hydration only touches localStorage in a
30
+ // browser; on the server `current` stays null until the client mounts.
31
+ const _current = shallowRef<TokenSet | null>(null)
32
+ let _hydrated = false
33
+
34
+ // The fallback used when nothing is cached yet. Consumers can override it via
35
+ // initTheme(fallback); defaults to whatever the first apply() sets.
36
+ let _fallback: TokenSet | null = null
37
+
38
+ function hydrateOnce(): void {
39
+ if (_hydrated || typeof window === "undefined") return
40
+ _hydrated = true
41
+ try {
42
+ const cached = localStorage.getItem(CACHE_KEY)
43
+ if (cached) {
44
+ _current.value = parseTheme(cached)
45
+ applyTheme(_current.value)
46
+ return
47
+ }
48
+ } catch {
49
+ /* ignore — fall through to fallback */
50
+ }
51
+ if (_fallback) {
52
+ _current.value = _fallback
53
+ applyTheme(_fallback)
54
+ }
55
+ }
56
+
57
+ export function applyTheme(theme: TokenSet, root: HTMLElement = document.documentElement): void {
58
+ for (const [key, cssVar] of Object.entries(COLOR_VARS)) {
59
+ root.style.setProperty(cssVar, theme.colors[key as keyof ColorTokens])
60
+ }
61
+ for (const [key, cssVar] of Object.entries(SHAPE_VARS)) {
62
+ root.style.setProperty(cssVar, `${theme.shape[key as keyof ShapeTokens]}px`)
63
+ }
64
+ for (const [key, cssVar] of Object.entries(DENSITY_VARS)) {
65
+ root.style.setProperty(cssVar, `${theme.density[key as keyof DensityTokens]}px`)
66
+ }
67
+ for (const [key, cssVar] of Object.entries(FONT_VARS)) {
68
+ root.style.setProperty(cssVar, theme.font[key as keyof FontTokens])
69
+ }
70
+ // Skin axis is optional; fall back to a flat default so themes saved before
71
+ // it existed still paint correctly.
72
+ const surface = theme.surface ?? DEFAULT_SURFACE
73
+ for (const [key, cssVar] of Object.entries(SURFACE_VARS)) {
74
+ root.style.setProperty(cssVar, surface[key as keyof SurfaceTokens])
75
+ }
76
+ root.dataset.themeMode = theme.mode
77
+ root.style.colorScheme = theme.mode
78
+ }
79
+
80
+ /** Point a circular theme reveal expands from (viewport px). */
81
+ export interface RevealOrigin {
82
+ x: number
83
+ y: number
84
+ }
85
+
86
+ type ViewTransitionDocument = Document & {
87
+ startViewTransition?: (cb: () => void) => { ready: Promise<void>; finished: Promise<void> }
88
+ }
89
+
90
+ function prefersReducedMotion(): boolean {
91
+ return (
92
+ typeof window !== "undefined" &&
93
+ window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true
94
+ )
95
+ }
96
+
97
+ /*
98
+ * Apply a theme with the "circular reveal" transition — a clip-path circle
99
+ * grows from `origin` (usually the toggle button's centre), repainting the page
100
+ * underneath. Uses the View Transitions API; falls back to a plain applyTheme
101
+ * when it's unsupported or the user prefers reduced motion (so behaviour is
102
+ * identical, just without the animation). The actual token write still happens
103
+ * through applyTheme, so this stays a thin wrapper — no duplicated var logic.
104
+ */
105
+ export function applyThemeWithTransition(
106
+ theme: TokenSet,
107
+ origin?: RevealOrigin,
108
+ root: HTMLElement = document.documentElement,
109
+ durationMs = 480,
110
+ ): void {
111
+ const doc = document as ViewTransitionDocument
112
+ if (typeof doc.startViewTransition !== "function" || prefersReducedMotion()) {
113
+ applyTheme(theme, root)
114
+ return
115
+ }
116
+
117
+ // Centre of the viewport is the default origin if no click point is given.
118
+ const x = origin?.x ?? window.innerWidth / 2
119
+ const y = origin?.y ?? window.innerHeight / 2
120
+ // Radius that reaches the farthest viewport corner from the origin.
121
+ const endRadius = Math.hypot(
122
+ Math.max(x, window.innerWidth - x),
123
+ Math.max(y, window.innerHeight - y),
124
+ )
125
+
126
+ const transition = doc.startViewTransition(() => applyTheme(theme, root))
127
+ transition.ready.then(() => {
128
+ // Animate the OLD snapshot shrinking to a point, revealing the (already
129
+ // repainted) live page underneath through the growing hole. Clipping the
130
+ // old layer — not growing the new one — avoids a one-frame flash of the new
131
+ // theme before the circle starts, which read as the theme "changing twice".
132
+ root.animate(
133
+ {
134
+ clipPath: [`circle(${endRadius}px at ${x}px ${y}px)`, `circle(0px at ${x}px ${y}px)`],
135
+ },
136
+ {
137
+ duration: durationMs,
138
+ easing: "cubic-bezier(0.2, 0, 0, 1)",
139
+ // Hold the fully-shrunk circle after the keyframes end. Without it the
140
+ // old snapshot snaps back to its default (un-clipped) state for the one
141
+ // frame between animation-end and the browser tearing down the
142
+ // view-transition pseudo-elements — that flash is the old theme
143
+ // "blinking" at the very end.
144
+ fill: "forwards",
145
+ pseudoElement: "::view-transition-old(root)",
146
+ },
147
+ )
148
+ })
149
+ }
150
+
151
+ export function serializeTheme(theme: TokenSet): string {
152
+ return JSON.stringify(theme)
153
+ }
154
+
155
+ /*
156
+ * Render a theme as CSS custom-property declarations (the body of a rule, e.g.
157
+ * `--color-brand: #00bcff; …`). Same var mapping as applyTheme, so a server can
158
+ * inline the theme into a <style> for the first paint while the client uses
159
+ * applyTheme — no duplicated values, the TokenSet stays the single source.
160
+ */
161
+ export function themeToCssVars(theme: TokenSet): string {
162
+ const decls: string[] = []
163
+ for (const [key, cssVar] of Object.entries(COLOR_VARS)) {
164
+ decls.push(`${cssVar}: ${theme.colors[key as keyof ColorTokens]}`)
165
+ }
166
+ for (const [key, cssVar] of Object.entries(SHAPE_VARS)) {
167
+ decls.push(`${cssVar}: ${theme.shape[key as keyof ShapeTokens]}px`)
168
+ }
169
+ for (const [key, cssVar] of Object.entries(DENSITY_VARS)) {
170
+ decls.push(`${cssVar}: ${theme.density[key as keyof DensityTokens]}px`)
171
+ }
172
+ for (const [key, cssVar] of Object.entries(FONT_VARS)) {
173
+ decls.push(`${cssVar}: ${theme.font[key as keyof FontTokens]}`)
174
+ }
175
+ const surface = theme.surface ?? DEFAULT_SURFACE
176
+ for (const [key, cssVar] of Object.entries(SURFACE_VARS)) {
177
+ decls.push(`${cssVar}: ${surface[key as keyof SurfaceTokens]}`)
178
+ }
179
+ return decls.join("; ")
180
+ }
181
+
182
+ /** Render a theme as a complete CSS rule (default selector `:root`). */
183
+ export function themeToCssRule(theme: TokenSet, selector = ":root"): string {
184
+ return `${selector} { color-scheme: ${theme.mode}; ${themeToCssVars(theme)}; }`
185
+ }
186
+
187
+ const DEFAULT_FONT = {
188
+ sans: '"Inter", "Segoe UI", system-ui, sans-serif',
189
+ mono: '"JetBrains Mono", "SF Mono", ui-monospace, monospace',
190
+ }
191
+
192
+ /** Parse + shallow-validate a theme JSON; throws on a malformed shape. */
193
+ export function parseTheme(json: string): TokenSet {
194
+ const raw = JSON.parse(json) as Partial<TokenSet>
195
+ if (!raw || typeof raw !== "object" || !raw.colors || !raw.shape || !raw.density) {
196
+ throw new Error("Invalid theme: missing colors/shape/density")
197
+ }
198
+ // font is a newer axis — fill it in for configs saved before it existed.
199
+ return { ...raw, font: raw.font ?? DEFAULT_FONT } as TokenSet
200
+ }
201
+
202
+ // ── anti-flash bootstrap ─────────────────────────────────────
203
+ // Call once before mount with your default theme; paints the cached theme (if
204
+ // any) synchronously so the first frame already matches the saved look.
205
+ export function bootstrapTheme(fallback: TokenSet): TokenSet {
206
+ let theme = fallback
207
+ try {
208
+ const cached = localStorage.getItem(CACHE_KEY)
209
+ if (cached) theme = parseTheme(cached)
210
+ } catch {
211
+ /* ignore, use fallback */
212
+ }
213
+ applyTheme(theme)
214
+ return theme
215
+ }
216
+
217
+ export function cacheTheme(theme: TokenSet): void {
218
+ try {
219
+ localStorage.setItem(CACHE_KEY, serializeTheme(theme))
220
+ } catch {
221
+ /* storage unavailable — non-fatal */
222
+ }
223
+ }
224
+
225
+ export interface UseTheme {
226
+ /** The active theme, kept in sync by the kit. Null until hydrated (SSR/first
227
+ * paint) — reactive, so a switcher binds straight to it. */
228
+ current: DeepReadonly<Ref<TokenSet | null>>
229
+ /** The active theme's name (""="not yet hydrated"). */
230
+ currentName: ComputedRef<string>
231
+ /** Set + persist a theme (with the circular reveal if `origin`/transition).
232
+ * This is the one call a switcher needs — it updates `current` too. */
233
+ setTheme: (theme: TokenSet, origin?: RevealOrigin) => void
234
+ /** Seed the fallback (used when nothing is cached) and hydrate now. Call once
235
+ * at app start (client) — optional; the first useTheme() hydrates anyway. */
236
+ init: (fallback: TokenSet) => TokenSet
237
+ apply: (theme: TokenSet) => void
238
+ applyWithTransition: (theme: TokenSet, origin?: RevealOrigin) => void
239
+ serialize: typeof serializeTheme
240
+ parse: typeof parseTheme
241
+ bootstrap: typeof bootstrapTheme
242
+ }
243
+
244
+ export function useTheme(): UseTheme {
245
+ // Lazily hydrate the singleton the first time anyone asks (client only).
246
+ hydrateOnce()
247
+
248
+ // The single "set a theme" path the kit owns: apply, cache, and update the
249
+ // shared reactive state so every consumer + switcher reflects it.
250
+ const setTheme = (theme: TokenSet, origin?: RevealOrigin) => {
251
+ if (origin) applyThemeWithTransition(theme, origin)
252
+ else applyThemeWithTransition(theme)
253
+ cacheTheme(theme)
254
+ _current.value = theme
255
+ }
256
+
257
+ return {
258
+ current: readonly(_current),
259
+ currentName: computed(() => _current.value?.name ?? ""),
260
+ setTheme,
261
+ init: (fallback: TokenSet) => {
262
+ _fallback = fallback
263
+ if (!_hydrated) hydrateOnce()
264
+ if (!_current.value) {
265
+ _current.value = bootstrapTheme(fallback)
266
+ }
267
+ return _current.value
268
+ },
269
+ apply: (theme: TokenSet) => {
270
+ applyTheme(theme)
271
+ cacheTheme(theme)
272
+ _current.value = theme
273
+ },
274
+ /**
275
+ * Apply + cache a theme with the circular reveal animation. Pass the click
276
+ * point (e.g. from a toggle's `event`) as `origin` so the circle grows from
277
+ * the button; omit it to reveal from the viewport centre.
278
+ */
279
+ applyWithTransition: (theme: TokenSet, origin?: RevealOrigin) => {
280
+ applyThemeWithTransition(theme, origin)
281
+ cacheTheme(theme)
282
+ _current.value = theme
283
+ },
284
+ serialize: serializeTheme,
285
+ parse: parseTheme,
286
+ bootstrap: bootstrapTheme,
287
+ }
288
+ }
@@ -0,0 +1,138 @@
1
+ /*
2
+ * Native scrollbar skin — opt-in. Styles the browser's OWN scrollbars (the
3
+ * window scroll and any `overflow:auto/scroll` element) to match LpScrollArea:
4
+ * a thin, rounded, brand-tinted thumb on a transparent track. Use this when you
5
+ * want the kit look on scroll that LpScrollArea can't own — the page itself,
6
+ * sticky panels, code blocks — without wrapping everything in a component.
7
+ *
8
+ * It does NOT replace the native scroll mechanism (no container, no JS), so
9
+ * sticky positioning, scroll restoration, anchor jumps and IntersectionObserver
10
+ * keep working. It's purely cosmetic.
11
+ *
12
+ * Reads --color-brand (kit token), so import it AFTER the tokens:
13
+ * @import "@leavepulse/ui";
14
+ * @import "@leavepulse/ui/scrollbar";
15
+ *
16
+ * Falls back to a neutral tint if --color-brand is absent.
17
+ */
18
+
19
+ :root {
20
+ --lp-scrollbar-size: 10px;
21
+ --lp-scrollbar-thumb: color-mix(
22
+ in oklab,
23
+ var(--color-brand, #7a8a84) 40%,
24
+ transparent
25
+ );
26
+ --lp-scrollbar-thumb-hover: var(--color-brand, #c7d2cc);
27
+ }
28
+
29
+ /* Firefox — Chrome ignores ::-webkit-* once scrollbar-width is set, so scope it. */
30
+ @supports (-moz-appearance: none) {
31
+ html {
32
+ scrollbar-width: thin;
33
+ scrollbar-color: var(--lp-scrollbar-thumb) transparent;
34
+ }
35
+ }
36
+
37
+ *::-webkit-scrollbar {
38
+ width: var(--lp-scrollbar-size);
39
+ height: var(--lp-scrollbar-size);
40
+ background: transparent;
41
+ }
42
+
43
+ *::-webkit-scrollbar-track,
44
+ *::-webkit-scrollbar-track-piece {
45
+ background: transparent;
46
+ border: none;
47
+ box-shadow: none;
48
+ }
49
+
50
+ *::-webkit-scrollbar-thumb {
51
+ background-color: var(--lp-scrollbar-thumb);
52
+ border-radius: 9999px;
53
+ /* Transparent border + padding-box clip = a slim thumb floating in a wider
54
+ hit area, the way LpScrollArea's overlay bar reads. */
55
+ border: 3px solid transparent;
56
+ background-clip: padding-box;
57
+ min-height: 40px;
58
+ transition: background-color var(--duration-fast, 120ms) ease;
59
+ }
60
+
61
+ *::-webkit-scrollbar-thumb:hover {
62
+ background-color: var(--lp-scrollbar-thumb-hover);
63
+ background-clip: padding-box;
64
+ }
65
+
66
+ *::-webkit-scrollbar-corner {
67
+ background: transparent;
68
+ }
69
+
70
+ *::-webkit-scrollbar-button {
71
+ display: none;
72
+ width: 0;
73
+ height: 0;
74
+ }
75
+
76
+ /* Chrome reads the root scrollbar from body, not html/*, so repeat for body. */
77
+ body::-webkit-scrollbar {
78
+ width: var(--lp-scrollbar-size);
79
+ height: var(--lp-scrollbar-size);
80
+ background: transparent;
81
+ }
82
+
83
+ body::-webkit-scrollbar-track,
84
+ body::-webkit-scrollbar-track-piece {
85
+ background: transparent;
86
+ border: none;
87
+ box-shadow: none;
88
+ }
89
+
90
+ body::-webkit-scrollbar-thumb {
91
+ background-color: var(--lp-scrollbar-thumb);
92
+ border-radius: 9999px;
93
+ border: 3px solid transparent;
94
+ background-clip: padding-box;
95
+ min-height: 40px;
96
+ }
97
+
98
+ body::-webkit-scrollbar-thumb:hover {
99
+ background-color: var(--lp-scrollbar-thumb-hover);
100
+ background-clip: padding-box;
101
+ }
102
+
103
+ body::-webkit-scrollbar-corner {
104
+ background: transparent;
105
+ }
106
+
107
+ body::-webkit-scrollbar-button {
108
+ display: none;
109
+ width: 0;
110
+ height: 0;
111
+ }
112
+
113
+ /*
114
+ * Per-element opt-out (hide the bar entirely — carousels, the CodeBlock editor,
115
+ * etc.) lives in utilities.css as `.lp-scrollbar-none`, shipped with the main
116
+ * import so it's available even without this opt-in skin.
117
+ */
118
+
119
+ /*
120
+ * GLOBAL opt-in: drop the native scrollbar from the layout flow site-wide so it
121
+ * never reserves viewport width (the page still scrolls — bar is just hidden).
122
+ * Add the class to <html> at the app root:
123
+ * <html class="lp-scroll-hide">
124
+ * Pair with LpScrollArea (or the skin above) for any region that should still
125
+ * show a bar. This only hides the document's own bar; nested overflow elements
126
+ * keep theirs unless they also carry .lp-scrollbar-none.
127
+ */
128
+ html.lp-scroll-hide {
129
+ scrollbar-width: none;
130
+ -ms-overflow-style: none;
131
+ }
132
+
133
+ html.lp-scroll-hide::-webkit-scrollbar,
134
+ html.lp-scroll-hide body::-webkit-scrollbar {
135
+ display: none;
136
+ width: 0;
137
+ height: 0;
138
+ }