@leavepulse/ui 0.45.0 → 0.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/assets/{LpCommandPalette-nGNnzXqQ.css → LpCommandPalette-HzAA4gri.css} +21 -21
  2. package/dist/chunks/{LpAddressInput-BwX8uo4_.js → LpAddressInput-CJOYGZlI.js} +1 -1
  3. package/dist/chunks/{LpAppShell-C53_RaGF.js → LpAppShell-B07Lf48e.js} +1 -1
  4. package/dist/chunks/{LpConfirmDialog-DCw5jR1t.js → LpConfirmDialog-BTVAR4tF.js} +1 -1
  5. package/dist/chunks/{LpConfirmHost-CbywMpMP.js → LpConfirmHost-Ci_uCHgQ.js} +1 -1
  6. package/dist/chunks/{LpDatePicker-DuusgT69.js → LpDatePicker-BrdFtSTy.js} +3 -1
  7. package/dist/chunks/{LpDateRangePicker-DHeuznfC.js → LpDateRangePicker-Drd7EbIz.js} +4 -2
  8. package/dist/chunks/{LpDrawer-Dt4aB4Fr.js → LpDrawer-CfRhHwmE.js} +1 -1
  9. package/dist/chunks/{LpModal-DMMDQHAu.js → LpModal-BzTVeNqe.js} +1 -1
  10. package/dist/chunks/{LpModalNav-BVPDyIup.js → LpModalNav-CjABSGCu.js} +12 -6
  11. package/dist/chunks/{LpSidebar-CQNB0-yU.js → LpSidebar-Bwws_X29.js} +1 -1
  12. package/dist/chunks/{LpToaster-DpJvoCOh.js → LpToaster-DTtWuKvq.js} +39 -6
  13. package/dist/components/LpAddressInput.vue.js +1 -1
  14. package/dist/components/LpAppShell.vue.js +1 -1
  15. package/dist/components/LpCommandPalette.vue.js +3 -3
  16. package/dist/components/LpConfirmDialog.vue.js +1 -1
  17. package/dist/components/LpConfirmHost.vue.js +1 -1
  18. package/dist/components/LpDatePicker.vue.js +1 -1
  19. package/dist/components/LpDateRangePicker.vue.js +1 -1
  20. package/dist/components/LpDrawer.vue.js +1 -1
  21. package/dist/components/LpModal.vue.js +1 -1
  22. package/dist/components/LpModalNav.vue.js +1 -1
  23. package/dist/components/LpShortcutsDialog.vue.js +1 -1
  24. package/dist/components/LpSidebar.vue.js +1 -1
  25. package/dist/components/LpToaster.vue.d.ts +2 -0
  26. package/dist/components/LpToaster.vue.js +1 -1
  27. package/dist/composables/useLocale.d.ts +2 -0
  28. package/dist/composables/useLocale.js +9 -0
  29. package/dist/index.js +11 -11
  30. package/package.json +1 -1
  31. package/src/components/LpCommandPalette.vue +1 -1
  32. package/src/components/LpDatePicker.vue +3 -1
  33. package/src/components/LpDateRangePicker.vue +3 -1
  34. package/src/components/LpDrawer.vue +1 -1
  35. package/src/components/LpModal.vue +1 -1
  36. package/src/components/LpModalNav.vue +13 -2
  37. package/src/components/LpToaster.vue +42 -4
  38. package/src/composables/useLocale.ts +14 -0
  39. package/src/tokens/tokens.css +7 -4
@@ -5,81 +5,81 @@
5
5
  * pane from its right edge as it grows. Animating the columns themselves made
6
6
  * every line of the list and the pane re-wrap on every frame.
7
7
  */
8
- .lp-cmd-dialog[data-v-2da506c1] {
8
+ .lp-cmd-dialog[data-v-bb9a3f4c] {
9
9
  --lp-cmd-list: min(92vw, 40rem);
10
10
  --lp-cmd-pane: max(0px, min(26rem, 96vw - var(--lp-cmd-list)));
11
11
  width: var(--lp-cmd-list);
12
12
  }
13
- .lp-cmd-split[data-v-2da506c1] {
13
+ .lp-cmd-split[data-v-bb9a3f4c] {
14
14
  transition:
15
15
  width 300ms var(--ease-emphasized, ease),
16
16
  min-height 300ms var(--ease-emphasized, ease);
17
17
  }
18
18
  @media (min-width: 768px) {
19
- .lp-cmd-split .lp-cmd-list[data-v-2da506c1] {
19
+ .lp-cmd-split .lp-cmd-list[data-v-bb9a3f4c] {
20
20
  flex: 0 0 auto;
21
21
  /* the dialog's 1px border on each side */
22
22
  width: calc(var(--lp-cmd-list) - 2px);
23
23
  }
24
- .lp-cmd-dialog[data-pane="open"][data-v-2da506c1] {
24
+ .lp-cmd-dialog[data-pane="open"][data-v-bb9a3f4c] {
25
25
  width: calc(var(--lp-cmd-list) + var(--lp-cmd-pane));
26
26
  min-height: min(76vh, 36rem);
27
27
  }
28
28
  }
29
- .lp-cmd-pane[data-v-2da506c1] {
29
+ .lp-cmd-pane[data-v-bb9a3f4c] {
30
30
  width: var(--lp-cmd-pane);
31
31
  }
32
32
  /*
33
33
  * The pane rides in from the right edge while the dialog opens up to it, and
34
34
  * leaves the same way, for as long as the dialog takes to close over it.
35
35
  */
36
- .lp-cmd-pane-enter-active[data-v-2da506c1],
37
- .lp-cmd-pane-leave-active[data-v-2da506c1] {
36
+ .lp-cmd-pane-enter-active[data-v-bb9a3f4c],
37
+ .lp-cmd-pane-leave-active[data-v-bb9a3f4c] {
38
38
  transition:
39
39
  transform 300ms var(--ease-emphasized, ease),
40
40
  opacity 300ms ease;
41
41
  }
42
- .lp-cmd-pane-enter-from[data-v-2da506c1],
43
- .lp-cmd-pane-leave-to[data-v-2da506c1] {
42
+ .lp-cmd-pane-enter-from[data-v-bb9a3f4c],
43
+ .lp-cmd-pane-leave-to[data-v-bb9a3f4c] {
44
44
  opacity: 0;
45
45
  transform: translateX(40%);
46
46
  }
47
- .lp-cmd-para[data-v-2da506c1] {
48
- animation: lp-cmd-para-in-2da506c1 260ms ease-out both;
47
+ .lp-cmd-para[data-v-bb9a3f4c] {
48
+ animation: lp-cmd-para-in-bb9a3f4c 260ms ease-out both;
49
49
  }
50
- @keyframes lp-cmd-para-in-2da506c1 {
50
+ @keyframes lp-cmd-para-in-bb9a3f4c {
51
51
  from {
52
52
  opacity: 0;
53
53
  transform: translateY(4px);
54
54
  }
55
55
  }
56
56
  @media (prefers-reduced-motion: reduce) {
57
- .lp-cmd-split[data-v-2da506c1],
58
- .lp-cmd-pane-enter-active[data-v-2da506c1],
59
- .lp-cmd-pane-leave-active[data-v-2da506c1] {
57
+ .lp-cmd-split[data-v-bb9a3f4c],
58
+ .lp-cmd-pane-enter-active[data-v-bb9a3f4c],
59
+ .lp-cmd-pane-leave-active[data-v-bb9a3f4c] {
60
60
  transition: none;
61
61
  }
62
- .lp-cmd-para[data-v-2da506c1] {
62
+ .lp-cmd-para[data-v-bb9a3f4c] {
63
63
  animation: none;
64
64
  }
65
65
  }
66
- .lp-cmd-excerpt[data-v-2da506c1] {
66
+ .lp-cmd-excerpt[data-v-bb9a3f4c] {
67
67
  display: -webkit-box;
68
68
  -webkit-box-orient: vertical;
69
69
  overflow: hidden;
70
70
  }
71
71
  /* Each result eases in; honours reduced motion. */
72
- .lp-cmd-item[data-v-2da506c1] {
73
- animation: lp-cmd-in-2da506c1 140ms ease-out both;
72
+ .lp-cmd-item[data-v-bb9a3f4c] {
73
+ animation: lp-cmd-in-bb9a3f4c 140ms ease-out both;
74
74
  }
75
- @keyframes lp-cmd-in-2da506c1 {
75
+ @keyframes lp-cmd-in-bb9a3f4c {
76
76
  from {
77
77
  opacity: 0;
78
78
  transform: translateY(3px);
79
79
  }
80
80
  }
81
81
  @media (prefers-reduced-motion: reduce) {
82
- .lp-cmd-item[data-v-2da506c1] {
82
+ .lp-cmd-item[data-v-bb9a3f4c] {
83
83
  animation: none;
84
84
  }
85
85
  }
@@ -7,7 +7,7 @@ import { _ as _sfc_main$3 } from "./LpButton-CKSwC6Mu.js";
7
7
  import { _ as _sfc_main$1 } from "./LpFormField-CtVYj6tO.js";
8
8
  import { _ as _sfc_main$4 } from "./LpIcon-DYtaid0K.js";
9
9
  import { _ as _sfc_main$5 } from "./LpInput-D-YGQbEv.js";
10
- import { _ as _sfc_main$7 } from "./LpModal-DMMDQHAu.js";
10
+ import { _ as _sfc_main$7 } from "./LpModal-BzTVeNqe.js";
11
11
  import { _ as _sfc_main$6 } from "./LpSelect-DNIL6MB9.js";
12
12
  import { useMergedAttrs } from "../composables/useMergedClass.js";
13
13
  const _hoisted_1 = { class: "flex items-stretch gap-2" };
@@ -4,7 +4,7 @@ import { _ as _sfc_main$2 } from "./LpButton-CKSwC6Mu.js";
4
4
  import { _ as _sfc_main$3 } from "./LpIcon-DYtaid0K.js";
5
5
  import { _ as _sfc_main$5 } from "./LpScrollArea-Bry1fdOM.js";
6
6
  import { _ as _sfc_main$4 } from "./LpShift-DXNsauQZ.js";
7
- import { _ as _sfc_main$1 } from "./LpSidebar-CQNB0-yU.js";
7
+ import { _ as _sfc_main$1 } from "./LpSidebar-Bwws_X29.js";
8
8
  import { useMergedAttrs } from "../composables/useMergedClass.js";
9
9
  const _hoisted_1 = ["href"];
10
10
  const _hoisted_2 = { class: "flex min-w-0 flex-1 flex-col" };
@@ -2,7 +2,7 @@ import { defineComponent, computed, ref, watch, openBlock, createBlock, createSl
2
2
  import { useLabels } from "../composables/useLpConfig.js";
3
3
  import { _ as _sfc_main$2 } from "./LpButton-CKSwC6Mu.js";
4
4
  import { _ as _sfc_main$3 } from "./LpIcon-DYtaid0K.js";
5
- import { _ as _sfc_main$1 } from "./LpModal-DMMDQHAu.js";
5
+ import { _ as _sfc_main$1 } from "./LpModal-BzTVeNqe.js";
6
6
  const _hoisted_1 = { class: "inline-flex items-center gap-2.5" };
7
7
  const _hoisted_2 = {
8
8
  key: 0,
@@ -1,6 +1,6 @@
1
1
  import { defineComponent, computed, ref, watch, openBlock, createBlock, createCommentVNode } from "vue";
2
2
  import { confirmQueue, settleConfirm } from "../components/confirm.js";
3
- import { _ as _sfc_main$1 } from "./LpConfirmDialog-DCw5jR1t.js";
3
+ import { _ as _sfc_main$1 } from "./LpConfirmDialog-BTVAR4tF.js";
4
4
  const _sfc_main = /* @__PURE__ */ defineComponent({
5
5
  __name: "LpConfirmHost",
6
6
  props: {
@@ -1,6 +1,7 @@
1
1
  import { defineComponent, ref, computed, useAttrs, openBlock, createElementBlock, normalizeStyle, unref, normalizeClass, createVNode, withCtx, createElementVNode, mergeProps, createCommentVNode, toDisplayString } from "vue";
2
2
  import { PopoverRoot, PopoverTrigger, PopoverPortal, PopoverContent } from "reka-ui";
3
3
  import { useFieldControl } from "../composables/useField.js";
4
+ import { useLocale } from "../composables/useLocale.js";
4
5
  import { useLabels } from "../composables/useLpConfig.js";
5
6
  import { mergeClass } from "../composables/useMergedClass.js";
6
7
  import { POPOVER_PANEL } from "../components/dropdown.js";
@@ -48,11 +49,12 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
48
49
  const at = (props.modelValue || "").slice(11, 16);
49
50
  return /^\d{2}:\d{2}$/.test(at) ? at : "";
50
51
  });
52
+ const locale = useLocale();
51
53
  const display = computed(() => {
52
54
  if (!props.modelValue) return "";
53
55
  const [y, m, d] = day.value.split("-").map(Number);
54
56
  if (!y || !m || !d) return props.modelValue;
55
- const shown = new Intl.DateTimeFormat(void 0, props.format).format(new Date(y, m - 1, d));
57
+ const shown = new Intl.DateTimeFormat(locale.value, props.format).format(new Date(y, m - 1, d));
56
58
  return props.time && minute.value ? `${shown}, ${minute.value}` : shown;
57
59
  });
58
60
  const shellSize = {
@@ -1,6 +1,7 @@
1
1
  import { defineComponent, ref, computed, onMounted, onBeforeUnmount, useAttrs, openBlock, createElementBlock, normalizeStyle, unref, normalizeClass, createVNode, withCtx, createElementVNode, mergeProps, createCommentVNode, Fragment, renderList, toDisplayString } from "vue";
2
2
  import { PopoverRoot, PopoverTrigger, PopoverPortal, PopoverContent } from "reka-ui";
3
3
  import { useFieldControl } from "../composables/useField.js";
4
+ import { useLocale } from "../composables/useLocale.js";
4
5
  import { useLabels } from "../composables/useLpConfig.js";
5
6
  import { mergeClass } from "../composables/useMergedClass.js";
6
7
  import { normalizeRange, formatRange, defaultRangePresets, sameRange, clampRange } from "../components/dateRange.js";
@@ -58,7 +59,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
58
59
  );
59
60
  const open = ref(false);
60
61
  const value = computed(() => normalizeRange(props.modelValue));
61
- const display = computed(() => value.value ? formatRange(value.value, props.locale, props.format) : "");
62
+ const locale = useLocale(() => props.locale);
63
+ const display = computed(() => value.value ? formatRange(value.value, locale.value, props.format) : "");
62
64
  const presetList = computed(() => {
63
65
  return props.presets ?? defaultRangePresets(text.value);
64
66
  });
@@ -174,7 +176,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
174
176
  "is-date-unavailable": __props.isDateUnavailable,
175
177
  "maximum-days": __props.maximumDays,
176
178
  months: wide.value ? 2 : 1,
177
- locale: __props.locale,
179
+ locale: unref(locale),
178
180
  class: "border-0 bg-transparent",
179
181
  "onUpdate:modelValue": pick
180
182
  }, null, 8, ["model-value", "min", "max", "is-disabled", "is-date-unavailable", "maximum-days", "months", "locale"])
@@ -60,7 +60,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
60
60
  watch(() => props.open, (open) => open ? claim() : release(), { immediate: true });
61
61
  onBeforeUnmount(release);
62
62
  const contentClass = computed(() => {
63
- const surface = frosted.value ? layer.value.nested ? "lp-glass-overlay--nested" : "lp-glass-overlay" : "bg-surface-raised";
63
+ const surface = frosted.value ? layer.value.nested ? "lp-glass-overlay lp-glass-overlay--nested" : "lp-glass-overlay" : "bg-surface-raised";
64
64
  const base = `fixed z-(--z-modal) flex flex-col border-line ${surface} shadow-panel outline-none`;
65
65
  const map = {
66
66
  left: "inset-y-0 left-0 border-r rounded-r-card",
@@ -162,7 +162,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
162
162
  createElementVNode("div", {
163
163
  ref_key: "panelRef",
164
164
  ref: panelRef,
165
- class: normalizeClass(["relative flex max-h-[min(90vh,calc(100dvh-2rem))] min-h-0 flex-col overflow-hidden rounded-card border border-line shadow-panel", [unref(frosted) ? unref(layer).nested ? "lp-glass-overlay--nested" : "lp-glass-overlay" : "bg-surface-raised", widthClass.value, unref(tweening) ? "transition-[height] duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none" : ""]]),
165
+ class: normalizeClass(["relative flex max-h-[min(90vh,calc(100dvh-2rem))] min-h-0 flex-col overflow-hidden rounded-card border border-line shadow-panel", [unref(frosted) ? unref(layer).nested ? "lp-glass-overlay lp-glass-overlay--nested" : "lp-glass-overlay" : "bg-surface-raised", widthClass.value, unref(tweening) ? "transition-[height] duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none" : ""]]),
166
166
  style: normalizeStyle({ ...__props.width ? { width: __props.width } : {}, ...hasAside.value ? { alignSelf: "stretch" } : {} })
167
167
  }, [
168
168
  !(__props.title || _ctx.$slots.title) && __props.ariaLabel ? (openBlock(), createBlock(unref(DialogTitle), {
@@ -1,9 +1,10 @@
1
1
  import { defineComponent, computed, ref, openBlock, createBlock, createSlots, withCtx, renderSlot, createElementVNode, normalizeClass, createVNode, createCommentVNode, createTextVNode, toDisplayString } from "vue";
2
+ import { useGlass } from "../composables/useGlass.js";
2
3
  import { useLabels } from "../composables/useLpConfig.js";
3
4
  import { _ as _sfc_main$2 } from "./LpButton-CKSwC6Mu.js";
4
5
  import { _ as _sfc_main$3 } from "./LpIcon-DYtaid0K.js";
5
- import { _ as _sfc_main$1 } from "./LpModal-DMMDQHAu.js";
6
- import { _ as _sfc_main$4 } from "./LpSidebar-CQNB0-yU.js";
6
+ import { _ as _sfc_main$1 } from "./LpModal-BzTVeNqe.js";
7
+ import { _ as _sfc_main$4 } from "./LpSidebar-Bwws_X29.js";
7
8
  const _hoisted_1 = { class: "flex items-center gap-2" };
8
9
  const _sfc_main = /* @__PURE__ */ defineComponent({
9
10
  __name: "LpModalNav",
@@ -61,6 +62,11 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
61
62
  );
62
63
  const triggerClass = computed(() => TRIGGER_VISIBILITY[props.mobileBreakpoint]);
63
64
  const railStyle = { maxHeight: "min(90vh, calc(100dvh - 2rem))" };
65
+ const frosted = useGlass(() => props.glass);
66
+ const railPanel = computed(() => [
67
+ "rounded-card border border-line p-2 shadow-panel",
68
+ frosted.value ? "lp-glass-overlay" : "bg-surface-raised"
69
+ ]);
64
70
  return (_ctx, _cache) => {
65
71
  return openBlock(), createBlock(_sfc_main$1, {
66
72
  glass: __props.glass,
@@ -122,8 +128,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
122
128
  responsive: "",
123
129
  "mobile-breakpoint": __props.mobileBreakpoint,
124
130
  style: railStyle,
125
- class: "rounded-card border border-line bg-surface-raised p-2 shadow-panel"
126
- }, null, 8, ["modelValue", "open", "items", "sections", "width", "searchable", "search-placeholder", "loading", "mobile-breakpoint"])
131
+ class: normalizeClass(railPanel.value)
132
+ }, null, 8, ["modelValue", "open", "items", "sections", "width", "searchable", "search-placeholder", "loading", "mobile-breakpoint", "class"])
127
133
  ], 2)
128
134
  ]),
129
135
  key: "0"
@@ -149,8 +155,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
149
155
  responsive: "",
150
156
  "mobile-breakpoint": __props.mobileBreakpoint,
151
157
  style: railStyle,
152
- class: "rounded-card border border-line bg-surface-raised p-2 shadow-panel"
153
- }, null, 8, ["modelValue", "open", "items", "sections", "width", "searchable", "search-placeholder", "loading", "mobile-breakpoint"])
158
+ class: normalizeClass(railPanel.value)
159
+ }, null, 8, ["modelValue", "open", "items", "sections", "width", "searchable", "search-placeholder", "loading", "mobile-breakpoint", "class"])
154
160
  ], 2)
155
161
  ]),
156
162
  key: "1"
@@ -1,6 +1,6 @@
1
1
  import { defineComponent, computed, ref, useId, openBlock, createElementBlock, Fragment, createElementVNode, mergeProps, unref, normalizeClass, renderSlot, createCommentVNode, createBlock, createVNode, createSlots, withCtx, normalizeProps, guardReactiveProps } from "vue";
2
2
  import { useLabels } from "../composables/useLpConfig.js";
3
- import { _ as _sfc_main$3 } from "./LpDrawer-Dt4aB4Fr.js";
3
+ import { _ as _sfc_main$3 } from "./LpDrawer-CfRhHwmE.js";
4
4
  import { _ as _sfc_main$1 } from "./LpInput-D-YGQbEv.js";
5
5
  import { _ as _sfc_main$2 } from "./LpSidebarNav-kx-LdqNU.js";
6
6
  const _hoisted_1 = ["aria-label"];
@@ -1,6 +1,7 @@
1
1
  import { defineComponent, useId, onMounted, nextTick, onBeforeUnmount, ref, watch, openBlock, createBlock, unref, withCtx, createElementBlock, Fragment, renderList, normalizeClass, createElementVNode, createVNode, createTextVNode, toDisplayString, createCommentVNode, withModifiers, normalizeStyle, Teleport } from "vue";
2
2
  import { ToastProvider, ToastRoot, ToastTitle, ToastDescription, ToastViewport } from "reka-ui";
3
3
  import { prefersReducedMotion } from "../composables/easing.js";
4
+ import { useClipboard } from "../composables/useClipboard.js";
4
5
  import { useLabels } from "../composables/useLpConfig.js";
5
6
  import { useToast } from "../composables/useToast.js";
6
7
  import { CLOSE_ICON } from "../components/dropdown.js";
@@ -8,7 +9,8 @@ import { _ as _sfc_main$1 } from "./LpIcon-DYtaid0K.js";
8
9
  const _hoisted_1 = { class: "flex items-start gap-3 p-3.5" };
9
10
  const _hoisted_2 = { class: "flex min-w-0 flex-1 flex-col gap-0.5" };
10
11
  const _hoisted_3 = ["disabled", "aria-busy", "onClick"];
11
- const _hoisted_4 = ["aria-label", "onClick"];
12
+ const _hoisted_4 = ["aria-label", "title", "onClick"];
13
+ const _hoisted_5 = ["aria-label", "onClick"];
12
14
  const _sfc_main = /* @__PURE__ */ defineComponent({
13
15
  __name: "LpToaster",
14
16
  props: {
@@ -16,7 +18,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
16
18
  },
17
19
  setup(__props) {
18
20
  const props = __props;
19
- const text = useLabels("toast", { dismiss: "Dismiss" }, () => props.labels);
21
+ const text = useLabels("toast", { dismiss: "Dismiss", copy: "Copy message", copied: "Copied" }, () => props.labels);
20
22
  const { queue, dismiss } = useToast();
21
23
  let observer;
22
24
  const tops = /* @__PURE__ */ new WeakMap();
@@ -147,8 +149,22 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
147
149
  busy.value = next;
148
150
  }
149
151
  }
152
+ const { copy } = useClipboard();
153
+ const copiedId = ref(null);
154
+ let copiedTimer;
155
+ const copyable = (t) => t.variant === "error" || t.variant === "warning";
156
+ async function copyToast(t) {
157
+ if (!await copy([t.title, t.message].filter(Boolean).join("\n"))) return;
158
+ copiedId.value = tag(t);
159
+ clearTimeout(copiedTimer);
160
+ copiedTimer = setTimeout(() => copiedId.value = null, 1500);
161
+ }
162
+ onBeforeUnmount(() => clearTimeout(copiedTimer));
163
+ function selecting() {
164
+ return Boolean(window.getSelection()?.toString());
165
+ }
150
166
  function runClick(t) {
151
- if (swiped === tag(t)) return;
167
+ if (swiped === tag(t) || selecting()) return;
152
168
  if (t.onClick) return settle(t, t.onClick);
153
169
  }
154
170
  function runAction(t, action) {
@@ -189,7 +205,11 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
189
205
  ]),
190
206
  _: 2
191
207
  }, 1024)) : createCommentVNode("", true),
192
- createVNode(unref(ToastDescription), { class: "break-words text-xs text-muted" }, {
208
+ createVNode(unref(ToastDescription), {
209
+ class: "cursor-text select-text break-words text-xs text-muted",
210
+ onPointerdown: _cache[0] || (_cache[0] = withModifiers(() => {
211
+ }, ["stop"]))
212
+ }, {
193
213
  default: withCtx(() => [
194
214
  createTextVNode(toDisplayString(t.message), 1)
195
215
  ]),
@@ -198,7 +218,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
198
218
  t.actions?.length ? (openBlock(), createElementBlock("div", {
199
219
  key: 1,
200
220
  class: "mt-2 flex flex-wrap gap-2",
201
- onClick: _cache[0] || (_cache[0] = withModifiers(() => {
221
+ onClick: _cache[1] || (_cache[1] = withModifiers(() => {
202
222
  }, ["stop"]))
203
223
  }, [
204
224
  (openBlock(true), createElementBlock(Fragment, null, renderList(t.actions, (action, i) => {
@@ -220,6 +240,19 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
220
240
  }), 128))
221
241
  ])) : createCommentVNode("", true)
222
242
  ]),
243
+ copyable(t) ? (openBlock(), createElementBlock("button", {
244
+ key: 0,
245
+ type: "button",
246
+ class: "-mt-1 rounded-md p-1 text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring",
247
+ "aria-label": copiedId.value === tag(t) ? unref(text).copied : unref(text).copy,
248
+ title: copiedId.value === tag(t) ? unref(text).copied : unref(text).copy,
249
+ onClick: withModifiers(($event) => copyToast(t), ["stop"])
250
+ }, [
251
+ createVNode(_sfc_main$1, {
252
+ name: copiedId.value === tag(t) ? "lucide:check" : "lucide:copy",
253
+ size: 15
254
+ }, null, 8, ["name"])
255
+ ], 8, _hoisted_4)) : createCommentVNode("", true),
223
256
  createElementVNode("button", {
224
257
  type: "button",
225
258
  class: "group -mr-1 -mt-1 rounded-md p-1 text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink",
@@ -231,7 +264,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
231
264
  size: 16,
232
265
  class: normalizeClass(unref(CLOSE_ICON))
233
266
  }, null, 8, ["class"])
234
- ], 8, _hoisted_4)
267
+ ], 8, _hoisted_5)
235
268
  ]),
236
269
  t.dismissAt ? (openBlock(), createElementBlock("div", {
237
270
  key: 0,
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpAddressInput-BwX8uo4_.js";
1
+ import { _ as _sfc_main } from "../chunks/LpAddressInput-CJOYGZlI.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpAppShell-C53_RaGF.js";
1
+ import { _ as _sfc_main } from "../chunks/LpAppShell-B07Lf48e.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -10,7 +10,7 @@ import { useModalLayer } from "./modalDepth.js";
10
10
  import { useLabels } from "../composables/useLpConfig.js";
11
11
  import { parseCommandQuery, rankCommandItems } from "./commandSearch.js";
12
12
  import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper-1tPrXgE0.js";
13
- import '../assets/LpCommandPalette-nGNnzXqQ.css';const _hoisted_1 = { class: "flex shrink-0 items-center gap-2.5 border-b border-line px-4" };
13
+ import '../assets/LpCommandPalette-HzAA4gri.css';const _hoisted_1 = { class: "flex shrink-0 items-center gap-2.5 border-b border-line px-4" };
14
14
  const _hoisted_2 = ["title", "onClick"];
15
15
  const _hoisted_3 = ["aria-activedescendant", "placeholder", "onKeydown"];
16
16
  const _hoisted_4 = { class: "shrink-0 rounded border border-line px-1.5 py-0.5 text-[10px] text-muted" };
@@ -554,7 +554,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
554
554
  createVNode(unref(DialogContent), {
555
555
  ref_key: "contentRef",
556
556
  ref: contentRef,
557
- class: normalizeClass(["lp-cmd-dialog fixed left-1/2 top-[12vh] flex max-h-[76vh] -translate-x-1/2 flex-col overflow-hidden rounded-card border border-line shadow-panel outline-none data-[state=open]:animate-[pop-in_160ms_var(--ease-emphasized)] data-[state=closed]:animate-[pop-out_130ms_ease]", [unref(frosted) ? unref(layer).nested ? "lp-glass-overlay--nested" : "lp-glass-overlay" : "bg-surface-raised", __props.previewPane && "lp-cmd-split"]]),
557
+ class: normalizeClass(["lp-cmd-dialog fixed left-1/2 top-[12vh] flex max-h-[76vh] -translate-x-1/2 flex-col overflow-hidden rounded-card border border-line shadow-panel outline-none data-[state=open]:animate-[pop-in_160ms_var(--ease-emphasized)] data-[state=closed]:animate-[pop-out_130ms_ease]", [unref(frosted) ? unref(layer).nested ? "lp-glass-overlay lp-glass-overlay--nested" : "lp-glass-overlay" : "bg-surface-raised", __props.previewPane && "lp-cmd-split"]]),
558
558
  style: normalizeStyle({ zIndex: unref(layer).panel }),
559
559
  "data-pane": paneOpen.value ? "open" : void 0,
560
560
  onOpenAutoFocus: _cache[2] || (_cache[2] = withModifiers(() => {
@@ -844,7 +844,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
844
844
  };
845
845
  }
846
846
  });
847
- const LpCommandPalette = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-2da506c1"]]);
847
+ const LpCommandPalette = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-bb9a3f4c"]]);
848
848
  export {
849
849
  LpCommandPalette as default
850
850
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpConfirmDialog-DCw5jR1t.js";
1
+ import { _ as _sfc_main } from "../chunks/LpConfirmDialog-BTVAR4tF.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpConfirmHost-CbywMpMP.js";
1
+ import { _ as _sfc_main } from "../chunks/LpConfirmHost-Ci_uCHgQ.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpDatePicker-DuusgT69.js";
1
+ import { _ as _sfc_main } from "../chunks/LpDatePicker-BrdFtSTy.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpDateRangePicker-DHeuznfC.js";
1
+ import { _ as _sfc_main } from "../chunks/LpDateRangePicker-Drd7EbIz.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpDrawer-Dt4aB4Fr.js";
1
+ import { _ as _sfc_main } from "../chunks/LpDrawer-CfRhHwmE.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpModal-DMMDQHAu.js";
1
+ import { _ as _sfc_main } from "../chunks/LpModal-BzTVeNqe.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpModalNav-BVPDyIup.js";
1
+ import { _ as _sfc_main } from "../chunks/LpModalNav-CjABSGCu.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,5 +1,5 @@
1
1
  import { defineComponent, computed, openBlock, createBlock, unref, withCtx, createElementVNode, createElementBlock, Fragment, renderList, toDisplayString, createTextVNode, createCommentVNode } from "vue";
2
- import { _ as _sfc_main$1 } from "../chunks/LpModal-DMMDQHAu.js";
2
+ import { _ as _sfc_main$1 } from "../chunks/LpModal-BzTVeNqe.js";
3
3
  import { useAppleKeyboard, mergeShortcutGroups, activeShortcuts, useHotkeys, appleModifierLabel } from "../composables/useHotkeys.js";
4
4
  import { useLabels } from "../composables/useLpConfig.js";
5
5
  import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper-1tPrXgE0.js";
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpSidebar-CQNB0-yU.js";
1
+ import { _ as _sfc_main } from "../chunks/LpSidebar-Bwws_X29.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,6 +1,8 @@
1
1
  type __VLS_Props = {
2
2
  labels?: Partial<{
3
3
  dismiss: string;
4
+ copy: string;
5
+ copied: string;
4
6
  }>;
5
7
  };
6
8
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpToaster-DpJvoCOh.js";
1
+ import { _ as _sfc_main } from "../chunks/LpToaster-DTtWuKvq.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -0,0 +1,2 @@
1
+ import { type ComputedRef, type MaybeRefOrGetter } from "vue";
2
+ export declare function useLocale(own?: MaybeRefOrGetter<string | undefined>): ComputedRef<string | undefined>;
@@ -0,0 +1,9 @@
1
+ import { computed, toValue } from "vue";
2
+ import { injectConfigProviderContext } from "reka-ui";
3
+ function useLocale(own) {
4
+ const context = injectConfigProviderContext({});
5
+ return computed(() => toValue(own) || context.locale?.value || void 0);
6
+ }
7
+ export {
8
+ useLocale
9
+ };
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { _ } from "./chunks/LayoutCanvas-BkaGXTjx.js";
2
- import { _ as _2 } from "./chunks/LpAddressInput-BwX8uo4_.js";
2
+ import { _ as _2 } from "./chunks/LpAddressInput-CJOYGZlI.js";
3
3
  import { _ as _3 } from "./chunks/LpMapPicker-Cc84LaM3.js";
4
4
  import { OSM_TILES, createNominatimProvider, emptyAddress, formatAddress, project, tileUrl, unproject } from "./components/address.js";
5
5
  import { _ as _4 } from "./chunks/LpActionBar-CnfFVIwC.js";
6
6
  import { _ as _5 } from "./chunks/LpAlert-B6ilrM7T.js";
7
- import { _ as _6 } from "./chunks/LpAppShell-C53_RaGF.js";
7
+ import { _ as _6 } from "./chunks/LpAppShell-B07Lf48e.js";
8
8
  import { _ as _7 } from "./chunks/LpAutocomplete-C8IgSMsi.js";
9
9
  import { _ as _8 } from "./chunks/LpAvatar-C1XStdOo.js";
10
10
  import { _ as _9 } from "./chunks/LpBadge-Fe0D1bYx.js";
@@ -22,17 +22,17 @@ import { _ as _16 } from "./chunks/LpColorPicker-zz4VGkG4.js";
22
22
  import { DEFAULT_SWATCHES, clampHsv, formatColor, hsvToHsl, hsvToRgb, isLight, normalizeHue, opaqueCss, parseColor, parseHex, rgbToHex, rgbToHsv } from "./components/color.js";
23
23
  import { default as default3 } from "./components/LpCommandPalette.vue.js";
24
24
  import { parseCommandQuery, rankCommandItems } from "./components/commandSearch.js";
25
- import { _ as _17 } from "./chunks/LpConfirmDialog-DCw5jR1t.js";
26
- import { _ as _18 } from "./chunks/LpConfirmHost-CbywMpMP.js";
25
+ import { _ as _17 } from "./chunks/LpConfirmDialog-BTVAR4tF.js";
26
+ import { _ as _18 } from "./chunks/LpConfirmHost-Ci_uCHgQ.js";
27
27
  import { confirm, useConfirm } from "./components/confirm.js";
28
28
  import { _ as _19 } from "./chunks/LpContextMenu-BqkAYa9X.js";
29
- import { _ as _20 } from "./chunks/LpDatePicker-DuusgT69.js";
30
- import { _ as _21 } from "./chunks/LpDateRangePicker-DHeuznfC.js";
29
+ import { _ as _20 } from "./chunks/LpDatePicker-BrdFtSTy.js";
30
+ import { _ as _21 } from "./chunks/LpDateRangePicker-Drd7EbIz.js";
31
31
  import { _ as _22 } from "./chunks/LpRangeCalendar-DZuaRV0W.js";
32
32
  import { clampRange, defaultRangePresets, formatRange, normalizeRange, sameRange } from "./components/dateRange.js";
33
33
  import { _ as _23 } from "./chunks/LpDisclosure-Cc23-JOT.js";
34
34
  import { _ as _24 } from "./chunks/LpDivider-CUX46KR0.js";
35
- import { _ as _25 } from "./chunks/LpDrawer-Dt4aB4Fr.js";
35
+ import { _ as _25 } from "./chunks/LpDrawer-CfRhHwmE.js";
36
36
  import { _ as _26 } from "./chunks/LpDropdownMenu-DdJGQzPn.js";
37
37
  import { default as default4 } from "./components/LpDropzone.vue.js";
38
38
  import { _ as _27 } from "./chunks/LpEmptyState-BJbZXC8D.js";
@@ -51,8 +51,8 @@ import { _ as _35 } from "./chunks/LpLink-Krkjhe_y.js";
51
51
  import { _ as _36 } from "./chunks/LpLogViewer-KypbgOyo.js";
52
52
  import { countLevels, matchPositions, matchesTerm, ringWindow, stepMatch } from "./components/logSearch.js";
53
53
  import { _ as _37 } from "./chunks/LpMediaGallery-YKr-cQPN.js";
54
- import { _ as _38 } from "./chunks/LpModal-DMMDQHAu.js";
55
- import { _ as _39 } from "./chunks/LpModalNav-BVPDyIup.js";
54
+ import { _ as _38 } from "./chunks/LpModal-BzTVeNqe.js";
55
+ import { _ as _39 } from "./chunks/LpModalNav-CjABSGCu.js";
56
56
  import { _ as _40 } from "./chunks/LpNotificationBell-CCwyJP5C.js";
57
57
  import { _ as _41 } from "./chunks/LpNumberField-B77O-z28.js";
58
58
  import { _ as _42 } from "./chunks/LpTimeField-Cqe7fXnh.js";
@@ -75,7 +75,7 @@ import { _ as _57 } from "./chunks/LpSelect-DNIL6MB9.js";
75
75
  import { _ as _58 } from "./chunks/LpServiceNode-D8uJ8sGk.js";
76
76
  import { _ as _59 } from "./chunks/LpShift-DXNsauQZ.js";
77
77
  import { default as default6 } from "./components/LpShortcutsDialog.vue.js";
78
- import { _ as _60 } from "./chunks/LpSidebar-CQNB0-yU.js";
78
+ import { _ as _60 } from "./chunks/LpSidebar-Bwws_X29.js";
79
79
  import { _ as _61 } from "./chunks/LpSkeleton-BB6XIV-w.js";
80
80
  import { _ as _62 } from "./chunks/LpSlider-Cuzmrd7p.js";
81
81
  import { _ as _63 } from "./chunks/LpSparkline-CB80N8SJ.js";
@@ -91,7 +91,7 @@ import { addTags, splitTags } from "./components/tags.js";
91
91
  import { _ as _70 } from "./chunks/LpTextarea-uuScWo6-.js";
92
92
  import { _ as _71 } from "./chunks/LpThemeSwitcher-B4aVPbNC.js";
93
93
  import { _ as _72 } from "./chunks/LpTilt-B7KHX1zH.js";
94
- import { _ as _73 } from "./chunks/LpToaster-DpJvoCOh.js";
94
+ import { _ as _73 } from "./chunks/LpToaster-DTtWuKvq.js";
95
95
  import { _ as _74 } from "./chunks/LpTooltip-CXVKcOXT.js";
96
96
  import { _ as _75 } from "./chunks/LpTooltipProvider-CftjV7Xc.js";
97
97
  import { _ as _76 } from "./chunks/LpTopologyCanvas-6ozNrQKh.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.45.0",
3
+ "version": "0.45.1",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -939,7 +939,7 @@ useHotkeys(() =>
939
939
  <DialogContent
940
940
  ref="contentRef"
941
941
  class="lp-cmd-dialog fixed left-1/2 top-[12vh] flex max-h-[76vh] -translate-x-1/2 flex-col overflow-hidden rounded-card border border-line shadow-panel outline-none data-[state=open]:animate-[pop-in_160ms_var(--ease-emphasized)] data-[state=closed]:animate-[pop-out_130ms_ease]"
942
- :class="[frosted ? (layer.nested ? 'lp-glass-overlay--nested' : 'lp-glass-overlay') : 'bg-surface-raised', previewPane && 'lp-cmd-split']"
942
+ :class="[frosted ? (layer.nested ? 'lp-glass-overlay lp-glass-overlay--nested' : 'lp-glass-overlay') : 'bg-surface-raised', previewPane && 'lp-cmd-split']"
943
943
  :style="{ zIndex: layer.panel }"
944
944
  :data-pane="paneOpen ? 'open' : undefined"
945
945
  @open-auto-focus.prevent
@@ -20,6 +20,7 @@ export default { inheritAttrs: false }
20
20
  import { PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from "reka-ui"
21
21
  import { computed, ref, useAttrs } from "vue"
22
22
  import { useFieldControl } from "../composables/useField"
23
+ import { useLocale } from "../composables/useLocale"
23
24
  import { useLabels } from "../composables/useLpConfig"
24
25
  import { mergeClass } from "../composables/useMergedClass"
25
26
  import { POPOVER_PANEL } from "./dropdown"
@@ -73,12 +74,13 @@ const minute = computed(() => {
73
74
  return /^\d{2}:\d{2}$/.test(at) ? at : ""
74
75
  })
75
76
 
77
+ const locale = useLocale()
76
78
  const display = computed(() => {
77
79
  if (!props.modelValue) return ""
78
80
  // Parse as local midnight so the formatted day can't drift across a timezone.
79
81
  const [y, m, d] = day.value.split("-").map(Number)
80
82
  if (!y || !m || !d) return props.modelValue
81
- const shown = new Intl.DateTimeFormat(undefined, props.format).format(new Date(y, m - 1, d))
83
+ const shown = new Intl.DateTimeFormat(locale.value, props.format).format(new Date(y, m - 1, d))
82
84
  return props.time && minute.value ? `${shown}, ${minute.value}` : shown
83
85
  })
84
86
 
@@ -12,6 +12,7 @@ export default { inheritAttrs: false }
12
12
  import { PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from "reka-ui"
13
13
  import { computed, onBeforeUnmount, onMounted, ref, useAttrs } from "vue"
14
14
  import { useFieldControl } from "../composables/useField"
15
+ import { useLocale } from "../composables/useLocale"
15
16
  import { useLabels } from "../composables/useLpConfig"
16
17
  import { mergeClass } from "../composables/useMergedClass"
17
18
  import {
@@ -85,7 +86,8 @@ const text = useLabels<DateRangePickerLabels>(
85
86
 
86
87
  const open = ref(false)
87
88
  const value = computed(() => normalizeRange(props.modelValue))
88
- const display = computed(() => (value.value ? formatRange(value.value, props.locale, props.format) : ""))
89
+ const locale = useLocale(() => props.locale)
90
+ const display = computed(() => (value.value ? formatRange(value.value, locale.value, props.format) : ""))
89
91
 
90
92
  const presetList = computed<DateRangePreset[]>(() => {
91
93
  return props.presets ?? defaultRangePresets(text.value)
@@ -162,7 +162,7 @@ watch(() => props.open, (open) => (open ? claim() : release()), { immediate: tru
162
162
  onBeforeUnmount(release)
163
163
 
164
164
  const contentClass = computed(() => {
165
- const surface = frosted.value ? (layer.value.nested ? "lp-glass-overlay--nested" : "lp-glass-overlay") : "bg-surface-raised"
165
+ const surface = frosted.value ? (layer.value.nested ? "lp-glass-overlay lp-glass-overlay--nested" : "lp-glass-overlay") : "bg-surface-raised"
166
166
  const base = `fixed z-(--z-modal) flex flex-col border-line ${surface} shadow-panel outline-none`
167
167
  const map: Record<Direction, string> = {
168
168
  left: "inset-y-0 left-0 border-r rounded-r-card",
@@ -325,7 +325,7 @@ watch(
325
325
  <div
326
326
  ref="panelRef"
327
327
  class="relative flex max-h-[min(90vh,calc(100dvh-2rem))] min-h-0 flex-col overflow-hidden rounded-card border border-line shadow-panel"
328
- :class="[frosted ? (layer.nested ? 'lp-glass-overlay--nested' : 'lp-glass-overlay') : 'bg-surface-raised', widthClass, tweening ? 'transition-[height] duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none' : '']"
328
+ :class="[frosted ? (layer.nested ? 'lp-glass-overlay lp-glass-overlay--nested' : 'lp-glass-overlay') : 'bg-surface-raised', widthClass, tweening ? 'transition-[height] duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none' : '']"
329
329
  :style="{ ...(width ? { width } : {}), ...(hasAside ? { alignSelf: 'stretch' } : {}) }"
330
330
  >
331
331
  <DialogTitle v-if="!(title || $slots.title) && ariaLabel" class="sr-only">
@@ -14,6 +14,7 @@
14
14
  * one the sections are simply unreachable on a phone.
15
15
  */
16
16
  import { computed, ref } from "vue"
17
+ import { useGlass } from "../composables/useGlass"
17
18
  import { useLabels } from "../composables/useLpConfig"
18
19
  import LpButton from "./LpButton.vue"
19
20
  import LpIcon from "./LpIcon.vue"
@@ -127,6 +128,16 @@ const triggerClass = computed(() => TRIGGER_VISIBILITY[props.mobileBreakpoint])
127
128
  * consuming app's scan.
128
129
  */
129
130
  const railStyle = { maxHeight: "min(90vh, calc(100dvh - 2rem))" }
131
+
132
+ /*
133
+ * The rail is part of the dialog, so it takes the same surface: a glass
134
+ * dialog beside a solid rail read as two different windows.
135
+ */
136
+ const frosted = useGlass(() => props.glass)
137
+ const railPanel = computed(() => [
138
+ "rounded-card border border-line p-2 shadow-panel",
139
+ frosted.value ? "lp-glass-overlay" : "bg-surface-raised",
140
+ ])
130
141
  </script>
131
142
 
132
143
  <template>
@@ -158,7 +169,7 @@ const railStyle = { maxHeight: "min(90vh, calc(100dvh - 2rem))" }
158
169
  responsive
159
170
  :mobile-breakpoint="mobileBreakpoint"
160
171
  :style="railStyle"
161
- class="rounded-card border border-line bg-surface-raised p-2 shadow-panel"
172
+ :class="railPanel"
162
173
  />
163
174
  </div>
164
175
  </template>
@@ -178,7 +189,7 @@ const railStyle = { maxHeight: "min(90vh, calc(100dvh - 2rem))" }
178
189
  responsive
179
190
  :mobile-breakpoint="mobileBreakpoint"
180
191
  :style="railStyle"
181
- class="rounded-card border border-line bg-surface-raised p-2 shadow-panel"
192
+ :class="railPanel"
182
193
  />
183
194
  </div>
184
195
  </template>
@@ -13,13 +13,14 @@ import {
13
13
  } from "reka-ui"
14
14
  import { nextTick, onBeforeUnmount, onMounted, ref, useId, watch } from "vue"
15
15
  import { prefersReducedMotion } from "../composables/easing"
16
+ import { useClipboard } from "../composables/useClipboard"
16
17
  import { useLabels } from "../composables/useLpConfig"
17
18
  import { useToast, type ToastItem } from "../composables/useToast"
18
19
  import { CLOSE_ICON } from "./dropdown"
19
20
  import LpIcon from "./LpIcon.vue"
20
21
 
21
- const props = defineProps<{ labels?: Partial<{ dismiss: string }> }>()
22
- const text = useLabels("toast", { dismiss: "Dismiss" }, () => props.labels)
22
+ const props = defineProps<{ labels?: Partial<{ dismiss: string; copy: string; copied: string }> }>()
23
+ const text = useLabels("toast", { dismiss: "Dismiss", copy: "Copy message", copied: "Copied" }, () => props.labels)
23
24
 
24
25
  const { queue, dismiss } = useToast()
25
26
 
@@ -211,8 +212,32 @@ async function settle(t: ToastItem, run: () => void | Promise<void>) {
211
212
  }
212
213
  }
213
214
 
215
+ /*
216
+ * An error is something people need to pass on, so its text can be selected
217
+ * and copied. reka sets user-select: none on the toast and starts a swipe on
218
+ * any press, which made the message impossible to select: the description
219
+ * opts back into selection and keeps its presses from starting a swipe. A
220
+ * click that ends a selection is not a click on the toast.
221
+ */
222
+ const { copy } = useClipboard()
223
+ const copiedId = ref<string | null>(null)
224
+ let copiedTimer: ReturnType<typeof setTimeout> | undefined
225
+ const copyable = (t: ToastItem) => t.variant === "error" || t.variant === "warning"
226
+
227
+ async function copyToast(t: ToastItem) {
228
+ if (!(await copy([t.title, t.message].filter(Boolean).join("\n")))) return
229
+ copiedId.value = tag(t)
230
+ clearTimeout(copiedTimer)
231
+ copiedTimer = setTimeout(() => (copiedId.value = null), 1500)
232
+ }
233
+ onBeforeUnmount(() => clearTimeout(copiedTimer))
234
+
235
+ function selecting(): boolean {
236
+ return Boolean(window.getSelection()?.toString())
237
+ }
238
+
214
239
  function runClick(t: ToastItem) {
215
- if (swiped === tag(t)) return
240
+ if (swiped === tag(t) || selecting()) return
216
241
  if (t.onClick) return settle(t, t.onClick)
217
242
  }
218
243
 
@@ -250,7 +275,10 @@ function runAction(t: ToastItem, action: NonNullable<ToastItem["actions"]>[numbe
250
275
  <ToastTitle v-if="t.title" class="text-sm font-semibold text-ink">
251
276
  {{ t.title }}
252
277
  </ToastTitle>
253
- <ToastDescription class="break-words text-xs text-muted">
278
+ <ToastDescription
279
+ class="cursor-text select-text break-words text-xs text-muted"
280
+ @pointerdown.stop
281
+ >
254
282
  {{ t.message }}
255
283
  </ToastDescription>
256
284
 
@@ -274,6 +302,16 @@ function runAction(t: ToastItem, action: NonNullable<ToastItem["actions"]>[numbe
274
302
  </div>
275
303
  </div>
276
304
 
305
+ <button
306
+ v-if="copyable(t)"
307
+ type="button"
308
+ class="-mt-1 rounded-md p-1 text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
309
+ :aria-label="copiedId === tag(t) ? text.copied : text.copy"
310
+ :title="copiedId === tag(t) ? text.copied : text.copy"
311
+ @click.stop="copyToast(t)"
312
+ >
313
+ <LpIcon :name="copiedId === tag(t) ? 'lucide:check' : 'lucide:copy'" :size="15" />
314
+ </button>
277
315
  <button
278
316
  type="button"
279
317
  class="group -mr-1 -mt-1 rounded-md p-1 text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink"
@@ -0,0 +1,14 @@
1
+ import { computed, toValue, type ComputedRef, type MaybeRefOrGetter } from "vue"
2
+ import { injectConfigProviderContext } from "reka-ui"
3
+
4
+ /*
5
+ * The locale a date is written in: the component's own `locale`, else the one
6
+ * the app gave reka's ConfigProvider, else undefined, which lets Intl use the
7
+ * browser's. The calendars already followed ConfigProvider while the field
8
+ * that shows the picked date did not, so a Russian app showed a Russian
9
+ * calendar under "Oct 8, 2026".
10
+ */
11
+ export function useLocale(own?: MaybeRefOrGetter<string | undefined>): ComputedRef<string | undefined> {
12
+ const context = injectConfigProviderContext({})
13
+ return computed(() => toValue(own) || context.locale?.value || undefined)
14
+ }
@@ -550,11 +550,14 @@
550
550
  backdrop-filter: blur(var(--glass-overlay-blur, 28px)) saturate(170%);
551
551
  }
552
552
 
553
+ /*
554
+ * A dialog opened over another is glass too: without its own blur the text of
555
+ * the dialog below read straight through the fill. Its scrim stays unblurred
556
+ * (.lp-scrim--nested), so the only blur is the panel's, and the fill is a
557
+ * little denser than the first layer's, since what it blurs is glass already.
558
+ */
553
559
  .lp-glass-overlay--nested {
554
- background-color: color-mix(in srgb, var(--color-surface-raised) 88%, transparent);
555
- background-image: none;
556
- -webkit-backdrop-filter: none;
557
- backdrop-filter: none;
560
+ --glass-overlay-fill: 62%;
558
561
  }
559
562
 
560
563
  :root[data-lp-no-backdrop] .lp-glass-overlay {