@leavepulse/ui 0.18.3 → 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 (30) hide show
  1. package/dist/assets/{LpCommandPalette-D315B47-.css → LpCommandPalette-Cp6Y-j7g.css} +4 -4
  2. package/dist/chunks/{LpAddressInput-Dfyn88Dd.js → LpAddressInput-CWlfDyKz.js} +1 -1
  3. package/dist/chunks/{LpAppShell-BvAVgVLW.js → LpAppShell-D42la4ZD.js} +1 -1
  4. package/dist/chunks/{LpConfirmDialog-BxBU4Pj9.js → LpConfirmDialog-JNW4rpx3.js} +1 -1
  5. package/dist/chunks/{LpDrawer-DaCsNuW3.js → LpDrawer-CYm-nUIZ.js} +1 -1
  6. package/dist/chunks/{LpLightbox-CWdIsthW.js → LpLightbox-CzhFe9wZ.js} +2 -2
  7. package/dist/chunks/{LpModal-CYKAaSHO.js → LpModal-BJTiTSDJ.js} +1 -1
  8. package/dist/chunks/{LpSidebar-BBG9NhAn.js → LpSidebar-DlLZJk1u.js} +1 -1
  9. package/dist/chunks/{LpThemeSwitcher-Ct-lnXMa.js → LpThemeSwitcher-B_kj0T2b.js} +21 -4
  10. package/dist/components/LpAddressInput.vue.js +1 -1
  11. package/dist/components/LpAppShell.vue.js +1 -1
  12. package/dist/components/LpCommandPalette.vue.js +3 -3
  13. package/dist/components/LpConfirmDialog.vue.js +1 -1
  14. package/dist/components/LpDrawer.vue.js +1 -1
  15. package/dist/components/LpLightbox.vue.js +1 -1
  16. package/dist/components/LpModal.vue.js +1 -1
  17. package/dist/components/LpSidebar.vue.js +1 -1
  18. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +16 -13
  21. package/dist/theme/backdrop.d.ts +9 -0
  22. package/package.json +1 -1
  23. package/src/components/LpCommandPalette.vue +1 -1
  24. package/src/components/LpDrawer.vue +1 -1
  25. package/src/components/LpLightbox.vue +4 -2
  26. package/src/components/LpModal.vue +1 -1
  27. package/src/index.ts +1 -0
  28. package/src/theme/backdrop.ts +51 -0
  29. package/src/theme/useTheme.ts +4 -0
  30. package/src/tokens/tokens.css +48 -0
@@ -1,16 +1,16 @@
1
1
 
2
2
  /* Each result eases in; honours reduced motion. */
3
- .lp-cmd-item[data-v-d845063e] {
4
- animation: lp-cmd-in-d845063e 140ms ease-out both;
3
+ .lp-cmd-item[data-v-7fba2dd8] {
4
+ animation: lp-cmd-in-7fba2dd8 140ms ease-out both;
5
5
  }
6
- @keyframes lp-cmd-in-d845063e {
6
+ @keyframes lp-cmd-in-7fba2dd8 {
7
7
  from {
8
8
  opacity: 0;
9
9
  transform: translateY(3px);
10
10
  }
11
11
  }
12
12
  @media (prefers-reduced-motion: reduce) {
13
- .lp-cmd-item[data-v-d845063e] {
13
+ .lp-cmd-item[data-v-7fba2dd8] {
14
14
  animation: none;
15
15
  }
16
16
  }
@@ -6,7 +6,7 @@ import { _ as _sfc_main$3 } from "./LpButton-5VXXCjwF.js";
6
6
  import { _ as _sfc_main$1 } from "./LpFormField-Bn-ZwC6z.js";
7
7
  import { _ as _sfc_main$4 } from "./LpIcon-CCnX5_2j.js";
8
8
  import { _ as _sfc_main$5 } from "./LpInput-BauZRyzm.js";
9
- import { _ as _sfc_main$7 } from "./LpModal-CYKAaSHO.js";
9
+ import { _ as _sfc_main$7 } from "./LpModal-BJTiTSDJ.js";
10
10
  import { _ as _sfc_main$6 } from "./LpSelect-D5mU4cZO.js";
11
11
  const _hoisted_1 = { class: "flex flex-col gap-3" };
12
12
  const _hoisted_2 = { class: "flex items-stretch gap-2" };
@@ -2,7 +2,7 @@ import { defineComponent, computed, watch, openBlock, createElementBlock, create
2
2
  import { _ as _sfc_main$2 } from "./LpButton-5VXXCjwF.js";
3
3
  import { _ as _sfc_main$3 } from "./LpIcon-CCnX5_2j.js";
4
4
  import { _ as _sfc_main$4 } from "./LpScrollArea-DQ0Yudfu.js";
5
- import { _ as _sfc_main$1 } from "./LpSidebar-BBG9NhAn.js";
5
+ import { _ as _sfc_main$1 } from "./LpSidebar-DlLZJk1u.js";
6
6
  const _hoisted_1 = { class: "flex h-dvh overflow-hidden bg-surface text-ink" };
7
7
  const _hoisted_2 = { class: "flex min-w-0 flex-1 flex-col" };
8
8
  const _hoisted_3 = { class: "flex h-16 shrink-0 items-center gap-3 border-b border-line bg-surface px-4 md:px-6" };
@@ -1,6 +1,6 @@
1
1
  import { defineComponent, openBlock, createBlock, withCtx, renderSlot, createVNode, createTextVNode, toDisplayString } from "vue";
2
2
  import { _ as _sfc_main$2 } from "./LpButton-5VXXCjwF.js";
3
- import { _ as _sfc_main$1 } from "./LpModal-CYKAaSHO.js";
3
+ import { _ as _sfc_main$1 } from "./LpModal-BJTiTSDJ.js";
4
4
  const _sfc_main = /* @__PURE__ */ defineComponent({
5
5
  __name: "LpConfirmDialog",
6
6
  props: {
@@ -215,7 +215,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
215
215
  default: withCtx(() => [
216
216
  createVNode(unref(DrawerPortal), null, {
217
217
  default: withCtx(() => [
218
- createVNode(unref(DrawerOverlay), { class: "fixed inset-0 z-(--z-overlay) bg-black/50 backdrop-blur-sm data-[state=open]:animate-[fade-in_150ms_ease] data-[state=closed]:animate-[fade-out_130ms_ease]" }),
218
+ createVNode(unref(DrawerOverlay), { class: "lp-scrim fixed inset-0 z-(--z-overlay) backdrop-blur-sm data-[state=open]:animate-[fade-in_150ms_ease] data-[state=closed]:animate-[fade-out_130ms_ease]" }),
219
219
  createVNode(unref(DrawerContent), {
220
220
  class: normalizeClass(contentClass.value),
221
221
  style: normalizeStyle(sizeStyle.value),
@@ -210,9 +210,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
210
210
  default: withCtx(() => [
211
211
  createVNode(unref(DialogPortal), null, {
212
212
  default: withCtx(() => [
213
- createVNode(unref(DialogOverlay), { class: "fixed inset-0 z-(--z-overlay) bg-black/85 backdrop-blur-sm data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]" }),
213
+ createVNode(unref(DialogOverlay), { class: "lp-scrim fixed inset-0 z-(--z-lightbox-overlay) backdrop-blur-sm data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]" }),
214
214
  createVNode(unref(DialogContent), {
215
- class: "fixed inset-0 z-(--z-modal) flex flex-col outline-none data-[state=open]:animate-[lightbox-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[lightbox-out_140ms_cubic-bezier(0.4,0,1,1)] motion-reduce:animate-none",
215
+ class: "fixed inset-0 z-(--z-lightbox) flex flex-col outline-none data-[state=open]:animate-[lightbox-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[lightbox-out_140ms_cubic-bezier(0.4,0,1,1)] motion-reduce:animate-none",
216
216
  onKeydown
217
217
  }, {
218
218
  default: withCtx(() => [
@@ -57,7 +57,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
57
57
  default: withCtx(() => [
58
58
  createVNode(unref(DialogPortal), null, {
59
59
  default: withCtx(() => [
60
- createVNode(unref(DialogOverlay), { class: "fixed inset-0 z-(--z-overlay) bg-black/50 backdrop-blur-sm data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]" }),
60
+ createVNode(unref(DialogOverlay), { class: "lp-scrim fixed inset-0 z-(--z-overlay) backdrop-blur-sm data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]" }),
61
61
  createElementVNode("div", _hoisted_1, [
62
62
  createVNode(unref(DialogContent), mergeProps({
63
63
  class: ["pointer-events-auto flex max-h-[min(90vh,calc(100dvh-2rem))] min-h-0 flex-col rounded-card border border-line bg-surface-raised shadow-panel outline-none data-[state=open]:animate-[rise-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[rise-out_120ms_cubic-bezier(0.4,0,1,1)]", widthClass.value],
@@ -1,5 +1,5 @@
1
1
  import { defineComponent, computed, useId, openBlock, createElementBlock, Fragment, createElementVNode, mergeProps, normalizeClass, renderSlot, createCommentVNode, createVNode, createSlots, withCtx, normalizeProps, guardReactiveProps, createBlock } from "vue";
2
- import { _ as _sfc_main$2 } from "./LpDrawer-DaCsNuW3.js";
2
+ import { _ as _sfc_main$2 } from "./LpDrawer-CYm-nUIZ.js";
3
3
  import { _ as _sfc_main$1 } from "./LpSidebarNav-DSDa5mAY.js";
4
4
  const _hoisted_1 = {
5
5
  key: 1,
@@ -249,6 +249,19 @@ const nord = {
249
249
  font: { ...interFont }
250
250
  };
251
251
  const presets = { dark, light, lime, leavepulse, rose, amber, violet, nord };
252
+ const NO_BACKDROP_ATTR = "data-lp-no-backdrop";
253
+ function isBrokenBackdropEngine(nav = navigator) {
254
+ const ua = nav.userAgent;
255
+ if (!/AppleWebKit/.test(ua)) return false;
256
+ if (/Chrome|Chromium|Edg\//.test(ua)) return false;
257
+ return !/Mac|iPhone|iPad|iPod/.test(nav.platform || "");
258
+ }
259
+ function detectBackdropFilter(root = document.documentElement) {
260
+ if (typeof navigator === "undefined") return true;
261
+ const usable = !isBrokenBackdropEngine();
262
+ root.toggleAttribute(NO_BACKDROP_ATTR, !usable);
263
+ return usable;
264
+ }
252
265
  const COLOR_VARS = {
253
266
  surface: "--color-surface",
254
267
  surfaceRaised: "--color-surface-raised",
@@ -341,6 +354,7 @@ function applyTheme(theme, root = document.documentElement) {
341
354
  }
342
355
  root.dataset.themeMode = theme.mode;
343
356
  root.style.colorScheme = theme.mode;
357
+ detectBackdropFilter(root);
344
358
  }
345
359
  function prefersReducedMotion() {
346
360
  return typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
@@ -600,6 +614,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
600
614
  });
601
615
  export {
602
616
  DEFAULT_SURFACE as D,
617
+ NO_BACKDROP_ATTR as N,
603
618
  THEME_COOKIE as T,
604
619
  _sfc_main as _,
605
620
  parseTheme as a,
@@ -608,11 +623,13 @@ export {
608
623
  applyThemeWithTransition as d,
609
624
  bootstrapTheme as e,
610
625
  dark as f,
611
- light as g,
612
- lime as h,
613
- themeToCssRule as i,
614
- themeToCssVars as j,
626
+ detectBackdropFilter as g,
627
+ light as h,
628
+ isBrokenBackdropEngine as i,
629
+ lime as j,
630
+ themeToCssRule as k,
615
631
  leavepulse as l,
632
+ themeToCssVars as m,
616
633
  nord as n,
617
634
  presets as p,
618
635
  rose as r,
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpAddressInput-Dfyn88Dd.js";
1
+ import { _ as _sfc_main } from "../chunks/LpAddressInput-CWlfDyKz.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpAppShell-BvAVgVLW.js";
1
+ import { _ as _sfc_main } from "../chunks/LpAppShell-D42la4ZD.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -2,7 +2,7 @@ import { defineComponent, ref, computed, watch, nextTick, onMounted, onBeforeUnm
2
2
  import { DialogRoot, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription } from "reka-ui";
3
3
  import { _ as _sfc_main$1 } from "../chunks/LpIcon-CCnX5_2j.js";
4
4
  import { _ as _sfc_main$2 } from "../chunks/LpScrollArea-DQ0Yudfu.js";
5
- import '../assets/LpCommandPalette-D315B47-.css';const _hoisted_1 = { class: "flex shrink-0 items-center gap-2.5 border-b border-line px-4" };
5
+ import '../assets/LpCommandPalette-Cp6Y-j7g.css';const _hoisted_1 = { class: "flex shrink-0 items-center gap-2.5 border-b border-line px-4" };
6
6
  const _hoisted_2 = ["placeholder", "onKeydown"];
7
7
  const _hoisted_3 = {
8
8
  key: 0,
@@ -167,7 +167,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
167
167
  default: withCtx(() => [
168
168
  createVNode(unref(DialogPortal), null, {
169
169
  default: withCtx(() => [
170
- createVNode(unref(DialogOverlay), { class: "fixed inset-0 z-(--z-overlay) bg-black/50 backdrop-blur-sm data-[state=open]:animate-[fade-in_150ms_ease] data-[state=closed]:animate-[fade-out_130ms_ease]" }),
170
+ createVNode(unref(DialogOverlay), { class: "lp-scrim fixed inset-0 z-(--z-overlay) backdrop-blur-sm data-[state=open]:animate-[fade-in_150ms_ease] data-[state=closed]:animate-[fade-out_130ms_ease]" }),
171
171
  createVNode(unref(DialogContent), {
172
172
  class: "fixed left-1/2 top-[12vh] z-(--z-modal) flex max-h-[76vh] w-[min(92vw,40rem)] -translate-x-1/2 flex-col overflow-hidden rounded-card border border-line bg-surface-raised shadow-panel outline-none data-[state=open]:animate-[pop-in_160ms_var(--ease-emphasized)] data-[state=closed]:animate-[pop-out_130ms_ease]",
173
173
  onOpenAutoFocus: _cache[1] || (_cache[1] = withModifiers(() => {
@@ -304,7 +304,7 @@ const _export_sfc = (sfc, props) => {
304
304
  }
305
305
  return target;
306
306
  };
307
- const LpCommandPalette = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-d845063e"]]);
307
+ const LpCommandPalette = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-7fba2dd8"]]);
308
308
  export {
309
309
  LpCommandPalette as default
310
310
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpConfirmDialog-BxBU4Pj9.js";
1
+ import { _ as _sfc_main } from "../chunks/LpConfirmDialog-JNW4rpx3.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpDrawer-DaCsNuW3.js";
1
+ import { _ as _sfc_main } from "../chunks/LpDrawer-CYm-nUIZ.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpLightbox-CWdIsthW.js";
1
+ import { _ as _sfc_main } from "../chunks/LpLightbox-CzhFe9wZ.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpModal-CYKAaSHO.js";
1
+ import { _ as _sfc_main } from "../chunks/LpModal-BJTiTSDJ.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpSidebar-BBG9NhAn.js";
1
+ import { _ as _sfc_main } from "../chunks/LpSidebar-DlLZJk1u.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpThemeSwitcher-Ct-lnXMa.js";
1
+ import { _ as _sfc_main } from "../chunks/LpThemeSwitcher-B_kj0T2b.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
package/dist/index.d.ts CHANGED
@@ -98,6 +98,7 @@ export { useLayout } from './layout/useLayout';
98
98
  export type { UseLayout, UseLayoutOptions } from './layout/useLayout';
99
99
  export { applyTheme, applyThemeWithTransition, bootstrapTheme, parseTheme, serializeTheme, THEME_COOKIE, themeFromCookie, themeToCssRule, themeToCssVars, useTheme, } from './theme/useTheme';
100
100
  export type { RevealOrigin } from './theme/useTheme';
101
+ export { detectBackdropFilter, isBrokenBackdropEngine, NO_BACKDROP_ATTR } from './theme/backdrop';
101
102
  export { DEFAULT_SURFACE } from './theme/tokens';
102
103
  export { defineTheme } from './theme/define';
103
104
  export type { ThemeOverrides } from './theme/define';
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { _ } from "./chunks/LayoutCanvas-Dd20boUG.js";
2
- import { _ as _2 } from "./chunks/LpAddressInput-Dfyn88Dd.js";
2
+ import { _ as _2 } from "./chunks/LpAddressInput-CWlfDyKz.js";
3
3
  import { _ as _3 } from "./chunks/LpMapPicker-DFKrd1Ne.js";
4
4
  import { OSM_TILES, createNominatimProvider, emptyAddress, formatAddress, project, tileUrl, unproject } from "./components/address.js";
5
5
  import { _ as _4 } from "./chunks/LpAlert-BAYHDSS1.js";
6
- import { _ as _5 } from "./chunks/LpAppShell-BvAVgVLW.js";
6
+ import { _ as _5 } from "./chunks/LpAppShell-D42la4ZD.js";
7
7
  import { _ as _6 } from "./chunks/LpAutocomplete-DIgXmtCA.js";
8
8
  import { _ as _7 } from "./chunks/LpAvatar-B1Ez0ZAx.js";
9
9
  import { _ as _8 } from "./chunks/LpBadge-CXzBPnwO.js";
@@ -14,12 +14,12 @@ import { _ as _12 } from "./chunks/LpCard-BmrTvWOe.js";
14
14
  import { _ as _13 } from "./chunks/LpCheckbox-BO0zuuPh.js";
15
15
  import { _ as _14 } from "./chunks/LpCodeBlock-CSiOSPs8.js";
16
16
  import { default as default2 } from "./components/LpCommandPalette.vue.js";
17
- import { _ as _15 } from "./chunks/LpConfirmDialog-BxBU4Pj9.js";
17
+ import { _ as _15 } from "./chunks/LpConfirmDialog-JNW4rpx3.js";
18
18
  import { _ as _16 } from "./chunks/LpContextMenu-D0mzXqA7.js";
19
19
  import { _ as _17 } from "./chunks/LpDatePicker-dY2q4dr4.js";
20
20
  import { _ as _18 } from "./chunks/LpDisclosure-DP6yfgfM.js";
21
21
  import { _ as _19 } from "./chunks/LpDivider-CUX46KR0.js";
22
- import { _ as _20 } from "./chunks/LpDrawer-DaCsNuW3.js";
22
+ import { _ as _20 } from "./chunks/LpDrawer-CYm-nUIZ.js";
23
23
  import { _ as _21 } from "./chunks/LpDropdownMenu-DJpoqRCb.js";
24
24
  import { _ as _22 } from "./chunks/LpEmptyState-CrfyRJUG.js";
25
25
  import { _ as _23 } from "./chunks/LpFileTree-DXb3yKzH.js";
@@ -28,11 +28,11 @@ import { _ as _24 } from "./chunks/LpFormField-Bn-ZwC6z.js";
28
28
  import { _ as _25 } from "./chunks/LpIcon-CCnX5_2j.js";
29
29
  import { _ as _26 } from "./chunks/LpInfraNode-C_PHcthC.js";
30
30
  import { _ as _27 } from "./chunks/LpInput-BauZRyzm.js";
31
- import { _ as _28 } from "./chunks/LpLightbox-CWdIsthW.js";
31
+ import { _ as _28 } from "./chunks/LpLightbox-CzhFe9wZ.js";
32
32
  import { fileNameOf } from "./components/lightbox.js";
33
33
  import { _ as _29 } from "./chunks/LpLink-DHwOEp4e.js";
34
34
  import { _ as _30 } from "./chunks/LpLogViewer-tmKPNTuU.js";
35
- import { _ as _31 } from "./chunks/LpModal-CYKAaSHO.js";
35
+ import { _ as _31 } from "./chunks/LpModal-BJTiTSDJ.js";
36
36
  import { _ as _32 } from "./chunks/LpNotificationBell-DHze8wYN.js";
37
37
  import { _ as _33 } from "./chunks/LpNumberField-C5ar-OwE.js";
38
38
  import { _ as _34 } from "./chunks/LpOtpInput-DqaT0Z75.js";
@@ -48,7 +48,7 @@ import { _ as _42 } from "./chunks/LpScrollArea-DQ0Yudfu.js";
48
48
  import { _ as _43 } from "./chunks/LpSegmented-BmpanyAo.js";
49
49
  import { _ as _44 } from "./chunks/LpSelect-D5mU4cZO.js";
50
50
  import { _ as _45 } from "./chunks/LpServiceNode-Dc-Ut5RS.js";
51
- import { _ as _46 } from "./chunks/LpSidebar-BBG9NhAn.js";
51
+ import { _ as _46 } from "./chunks/LpSidebar-DlLZJk1u.js";
52
52
  import { _ as _47 } from "./chunks/LpSkeleton-eSFIo4kD.js";
53
53
  import { _ as _48 } from "./chunks/LpSlider-B1SF3pRV.js";
54
54
  import { _ as _49 } from "./chunks/LpStat-CdbEFOvn.js";
@@ -58,8 +58,8 @@ import { _ as _52 } from "./chunks/LpTable-DqZ-L4Au.js";
58
58
  import { _ as _53 } from "./chunks/LpTableOfContents-CjvxPMpU.js";
59
59
  import { _ as _54 } from "./chunks/LpTabs-D7aTVPOb.js";
60
60
  import { _ as _55 } from "./chunks/LpTextarea-CRAkhemZ.js";
61
- import { p as presets, a as parseTheme, s as serializeTheme } from "./chunks/LpThemeSwitcher-Ct-lnXMa.js";
62
- import { D, _ as _56, T, b, c, d, e, f, l, g, h, n, r, t, i, j, u, v } from "./chunks/LpThemeSwitcher-Ct-lnXMa.js";
61
+ import { p as presets, a as parseTheme, s as serializeTheme } from "./chunks/LpThemeSwitcher-B_kj0T2b.js";
62
+ import { D, _ as _56, N, T, b, c, d, e, f, g, i, l, h, j, n, r, t, k, m, u, v } from "./chunks/LpThemeSwitcher-B_kj0T2b.js";
63
63
  import { _ as _57 } from "./chunks/LpTilt-DuyKbpkA.js";
64
64
  import { _ as _58 } from "./chunks/LpToaster-gDB1EaU3.js";
65
65
  import { _ as _59 } from "./chunks/LpTooltip-CIwoRCag.js";
@@ -167,6 +167,7 @@ export {
167
167
  _59 as LpTooltip,
168
168
  _60 as LpTopologyCanvas,
169
169
  _61 as LpUptimeBar,
170
+ N as NO_BACKDROP_ATTR,
170
171
  OSM_TILES,
171
172
  T as THEME_COOKIE,
172
173
  UI_CONFIG_VERSION,
@@ -188,6 +189,7 @@ export {
188
189
  defineBlocks,
189
190
  defineTheme,
190
191
  deserializeLayout,
192
+ g as detectBackdropFilter,
191
193
  emptyAddress,
192
194
  fileIcon,
193
195
  fileNameOf,
@@ -195,9 +197,10 @@ export {
195
197
  formatAddress,
196
198
  formatModified,
197
199
  formatSize,
200
+ i as isBrokenBackdropEngine,
198
201
  l as leavepulse,
199
- g as light,
200
- h as lime,
202
+ h as light,
203
+ j as lime,
201
204
  loadCountries,
202
205
  makeLayout,
203
206
  matchCountryByValue,
@@ -220,8 +223,8 @@ export {
220
223
  subtreeIds,
221
224
  subtreeSize,
222
225
  t as themeFromCookie,
223
- i as themeToCssRule,
224
- j as themeToCssVars,
226
+ k as themeToCssRule,
227
+ m as themeToCssVars,
225
228
  tileUrl,
226
229
  unproject,
227
230
  useInputFilter,
@@ -0,0 +1,9 @@
1
+ /** Set on <html> when backdrop-filter resolves but does not composite. */
2
+ export declare const NO_BACKDROP_ATTR = "data-lp-no-backdrop";
3
+ export declare function isBrokenBackdropEngine(nav?: Navigator): boolean;
4
+ /**
5
+ * Flag the document when backdrop-filter cannot be trusted, so the fallback
6
+ * rules in tokens.css can trade blur for opacity. Safe to call more than once
7
+ * and a no-op outside the browser.
8
+ */
9
+ export declare function detectBackdropFilter(root?: HTMLElement): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.18.3",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -212,7 +212,7 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onHotkey))
212
212
  <DialogRoot :open="open" @update:open="(v) => emit('update:open', v)">
213
213
  <DialogPortal>
214
214
  <DialogOverlay
215
- class="fixed inset-0 z-(--z-overlay) bg-black/50 backdrop-blur-sm data-[state=open]:animate-[fade-in_150ms_ease] data-[state=closed]:animate-[fade-out_130ms_ease]"
215
+ class="lp-scrim fixed inset-0 z-(--z-overlay) backdrop-blur-sm data-[state=open]:animate-[fade-in_150ms_ease] data-[state=closed]:animate-[fade-out_130ms_ease]"
216
216
  />
217
217
  <DialogContent
218
218
  class="fixed left-1/2 top-[12vh] z-(--z-modal) flex max-h-[76vh] w-[min(92vw,40rem)] -translate-x-1/2 flex-col overflow-hidden rounded-card border border-line bg-surface-raised shadow-panel outline-none data-[state=open]:animate-[pop-in_160ms_var(--ease-emphasized)] data-[state=closed]:animate-[pop-out_130ms_ease]"
@@ -345,7 +345,7 @@ const edgeStripClass = computed(() =>
345
345
  >
346
346
  <DrawerPortal>
347
347
  <DrawerOverlay
348
- class="fixed inset-0 z-(--z-overlay) bg-black/50 backdrop-blur-sm data-[state=open]:animate-[fade-in_150ms_ease] data-[state=closed]:animate-[fade-out_130ms_ease]"
348
+ class="lp-scrim fixed inset-0 z-(--z-overlay) backdrop-blur-sm data-[state=open]:animate-[fade-in_150ms_ease] data-[state=closed]:animate-[fade-out_130ms_ease]"
349
349
  />
350
350
  <DrawerContent
351
351
  :class="contentClass"
@@ -270,11 +270,13 @@ const ARROW =
270
270
  <template>
271
271
  <DialogRoot :open="open" @update:open="(v) => emit('update:open', v)">
272
272
  <DialogPortal>
273
+ <!-- Sits on the lightbox rung, not the shared modal one: opening this
274
+ from inside a dialog must cover that dialog, not tie with it. -->
273
275
  <DialogOverlay
274
- class="fixed inset-0 z-(--z-overlay) bg-black/85 backdrop-blur-sm data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]"
276
+ class="lp-scrim fixed inset-0 z-(--z-lightbox-overlay) backdrop-blur-sm data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]"
275
277
  />
276
278
  <DialogContent
277
- class="fixed inset-0 z-(--z-modal) flex flex-col outline-none data-[state=open]:animate-[lightbox-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[lightbox-out_140ms_cubic-bezier(0.4,0,1,1)] motion-reduce:animate-none"
279
+ class="fixed inset-0 z-(--z-lightbox) flex flex-col outline-none data-[state=open]:animate-[lightbox-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[lightbox-out_140ms_cubic-bezier(0.4,0,1,1)] motion-reduce:animate-none"
278
280
  @keydown="onKeydown"
279
281
  >
280
282
  <!-- reka wants both for a labelled dialog; the caption is the title. -->
@@ -76,7 +76,7 @@ const bodyPad = computed(() =>
76
76
  <DialogRoot :open="open" @update:open="(v) => $emit('update:open', v)">
77
77
  <DialogPortal>
78
78
  <DialogOverlay
79
- class="fixed inset-0 z-(--z-overlay) bg-black/50 backdrop-blur-sm data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]"
79
+ class="lp-scrim fixed inset-0 z-(--z-overlay) backdrop-blur-sm data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]"
80
80
  />
81
81
  <!-- Centred by a full-screen flex wrapper rather than by translating the
82
82
  panel off its own centre. With `top:50% / -translate-y-1/2` the panel
package/src/index.ts CHANGED
@@ -165,6 +165,7 @@ export {
165
165
  useTheme,
166
166
  } from "./theme/useTheme"
167
167
  export type { RevealOrigin } from "./theme/useTheme"
168
+ export { detectBackdropFilter, isBrokenBackdropEngine, NO_BACKDROP_ATTR } from "./theme/backdrop"
168
169
  export { DEFAULT_SURFACE } from "./theme/tokens"
169
170
  export { defineTheme } from "./theme/define"
170
171
  export type { ThemeOverrides } from "./theme/define"
@@ -0,0 +1,51 @@
1
+ /*
2
+ * backdrop-filter capability detection.
3
+ *
4
+ * Every glass surface in the kit (dialog scrims, dropdown panels, the glass
5
+ * skin) leans on backdrop-filter. WebKitGTK — the engine Tauri embeds on Linux,
6
+ * and the one behind WebKit2GTK desktop shells — parses the property, reports
7
+ * it as supported through BOTH CSS.supports() and @supports, resolves it to a
8
+ * real computed value, and then composites it as a no-op. The layer tints; it
9
+ * never blurs.
10
+ *
11
+ * That makes the usual feature query worthless: there is no CSS condition that
12
+ * separates a working implementation from this one, because the engine answers
13
+ * "yes" to all of them. Tailwind's own Safari workaround does not fire either
14
+ * (WebKitGTK matches `margin-trim`, which the hack uses to rule Safari out).
15
+ *
16
+ * So the check is a runtime one, and deliberately conservative: it only flags
17
+ * the engine fingerprint that is known to be broken, and treats anything it
18
+ * cannot positively identify as working. A false negative costs a blur nobody
19
+ * sees; a false positive would strip the blur from browsers that render it
20
+ * correctly, which is the worse failure.
21
+ */
22
+
23
+ /** Set on <html> when backdrop-filter resolves but does not composite. */
24
+ export const NO_BACKDROP_ATTR = "data-lp-no-backdrop"
25
+
26
+ /*
27
+ * WebKit that is neither Chromium nor an Apple platform. Chromium keeps
28
+ * "AppleWebKit" in its UA string, so it has to be excluded explicitly; real
29
+ * Safari only ever runs on a Mac/iOS platform string. What survives both
30
+ * filters is WebKitGTK/WPE — the broken case.
31
+ */
32
+ export function isBrokenBackdropEngine(nav: Navigator = navigator): boolean {
33
+ const ua = nav.userAgent
34
+ if (!/AppleWebKit/.test(ua)) return false
35
+ if (/Chrome|Chromium|Edg\//.test(ua)) return false
36
+ return !/Mac|iPhone|iPad|iPod/.test(nav.platform || "")
37
+ }
38
+
39
+ /**
40
+ * Flag the document when backdrop-filter cannot be trusted, so the fallback
41
+ * rules in tokens.css can trade blur for opacity. Safe to call more than once
42
+ * and a no-op outside the browser.
43
+ */
44
+ export function detectBackdropFilter(
45
+ root: HTMLElement = document.documentElement,
46
+ ): boolean {
47
+ if (typeof navigator === "undefined") return true
48
+ const usable = !isBrokenBackdropEngine()
49
+ root.toggleAttribute(NO_BACKDROP_ATTR, !usable)
50
+ return usable
51
+ }
@@ -5,6 +5,7 @@
5
5
  * serialize/parse with validation.
6
6
  */
7
7
  import { computed, readonly, shallowRef, type ComputedRef, type DeepReadonly, type Ref } from "vue"
8
+ import { detectBackdropFilter } from "./backdrop"
8
9
  import {
9
10
  COLOR_VARS,
10
11
  DEFAULT_SURFACE,
@@ -75,6 +76,9 @@ export function applyTheme(theme: TokenSet, root: HTMLElement = document.documen
75
76
  }
76
77
  root.dataset.themeMode = theme.mode
77
78
  root.style.colorScheme = theme.mode
79
+ // Engine capability, not theme data — but this is the one path every consumer
80
+ // already calls, so the glass fallback lands without extra wiring.
81
+ detectBackdropFilter(root)
78
82
  }
79
83
 
80
84
  /** Point a circular theme reveal expands from (viewport px). */
@@ -120,6 +120,15 @@
120
120
  --z-popover: 120;
121
121
  --z-tooltip: 130;
122
122
  --z-toast: 200;
123
+
124
+ /* A lightbox is usually opened FROM a dialog — click a thumbnail in a modal
125
+ and the full image comes up over it. Sharing --z-overlay/--z-modal left
126
+ that order to DOM insertion alone, so the lightbox's own scrim landed on
127
+ top of the modal panel and swallowed it while double-darkening the page
128
+ behind. Its own rung above the modal (but below tooltip/toast, which must
129
+ outrank everything) makes the stack explicit. */
130
+ --z-lightbox-overlay: 140;
131
+ --z-lightbox: 150;
123
132
  }
124
133
 
125
134
  /* ── overlay enter/exit animations ───────────────
@@ -379,6 +388,45 @@
379
388
  -webkit-backdrop-filter: blur(var(--surface-panel-blur)) saturate(var(--surface-panel-saturate));
380
389
  }
381
390
 
391
+ /*
392
+ * No-backdrop fallback. WebKitGTK — the engine behind Tauri on Linux — parses
393
+ * backdrop-filter and reports it through both CSS.supports() and @supports,
394
+ * then composites it as a no-op: the layer tints but never blurs. Since the
395
+ * lie makes @supports useless here, detectBackdropFilter() sets this flag on
396
+ * <html> instead, and every glass surface trades the blur it cannot get for
397
+ * the opacity it can. Without it a translucent panel over busy content reads
398
+ * as unfinished rather than frosted.
399
+ */
400
+ :root[data-lp-no-backdrop] {
401
+ --surface-panel-blur: 0px;
402
+ --surface-panel-saturate: 100%;
403
+ }
404
+
405
+ :root[data-lp-no-backdrop] .lp-skin-panel {
406
+ backdrop-filter: none;
407
+ -webkit-backdrop-filter: none;
408
+ }
409
+
410
+ /*
411
+ * Scrims lean on the same trick. `.lp-scrim` marks the full-screen overlay
412
+ * behind a dialog; where the blur lands the tint stays light, and where it
413
+ * cannot the tint carries the separation on its own.
414
+ *
415
+ * The bump is deliberately mild. Scrims stack — a lightbox opened from a modal
416
+ * paints two — and opacity compounds, so a value tuned to look right alone goes
417
+ * nearly black in a pair. 0.62 reads as a clear backdrop on its own and still
418
+ * leaves the stacked case legible.
419
+ */
420
+ .lp-scrim {
421
+ background-color: rgb(0 0 0 / var(--scrim-opacity, 0.5));
422
+ }
423
+
424
+ :root[data-lp-no-backdrop] .lp-scrim {
425
+ backdrop-filter: none;
426
+ -webkit-backdrop-filter: none;
427
+ --scrim-opacity: 0.62;
428
+ }
429
+
382
430
  /* The overlay gradient rides above the base fill but under the content. */
383
431
  .lp-skin-panel::before {
384
432
  content: "";