@leavepulse/ui 0.60.0 → 0.62.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 (74) hide show
  1. package/README.md +8 -0
  2. package/dist/assets/{LpTable-BZZf_rrt.css → LpTable-DpXyPzs8.css} +19 -19
  3. package/dist/chunks/{LpAddressInput-DD4zTN4P.js → LpAddressInput-CzdZguBo.js} +1 -1
  4. package/dist/chunks/{LpAlert--KLL4NLk.js → LpAlert-BAOujnXy.js} +8 -6
  5. package/dist/chunks/{LpAppShell-Cv-Y9qNA.js → LpAppShell-BXMNDEpF.js} +1 -1
  6. package/dist/chunks/{LpAutocomplete-PnL2ntKl.js → LpAutocomplete-D9mY5Tqk.js} +101 -36
  7. package/dist/chunks/{LpAvatar-CwGs1JCQ.js → LpAvatar-K6MOMfgn.js} +1 -1
  8. package/dist/chunks/{LpCard-B2D0GIUU.js → LpCard-D9HVgX8Y.js} +1 -1
  9. package/dist/chunks/{LpColorPicker-CvBF8TeL.js → LpColorPicker-BO1ZTY51.js} +1 -1
  10. package/dist/chunks/{LpContextMenu-BnN1k7ga.js → LpContextMenu-Cv_F2iCG.js} +6 -3
  11. package/dist/chunks/{LpFileTree-DEqh2ySW.js → LpFileTree-DTwUj0L-.js} +1 -1
  12. package/dist/chunks/{LpFileTreeNode-DCZA7sQ-.js → LpFileTreeNode-D-XLPZ6T.js} +1 -1
  13. package/dist/chunks/{LpHints-BTpvisgP.js → LpHints-B_85eSmm.js} +1 -1
  14. package/dist/chunks/{LpLogViewer-BuPe23sZ.js → LpLogViewer-D9UgNz2m.js} +1 -1
  15. package/dist/chunks/{LpModalNav-Cw7m-eks.js → LpModalNav-D4rmflQn.js} +1 -1
  16. package/dist/chunks/{LpNotificationBell-xH_JphpS.js → LpNotificationBell-CyylOlZR.js} +1 -1
  17. package/dist/chunks/{LpPie-BD5Lsh-2.js → LpPie-B2deu8CI.js} +1 -1
  18. package/dist/chunks/{LpSidebar-B3E_iViL.js → LpSidebar-D8tub3bt.js} +1 -1
  19. package/dist/chunks/{LpSidebarNav-32sAXNM-.js → LpSidebarNav-BBSRg34G.js} +1 -1
  20. package/dist/chunks/LpSwap-D82jiecV.js +38 -0
  21. package/dist/chunks/{LpThemeSwitcher-CeBG0e_D.js → LpThemeSwitcher-ZFFftBQn.js} +1 -1
  22. package/dist/chunks/{LpToaster-BcKs7vX7.js → LpToaster-D3OisETH.js} +1 -1
  23. package/dist/chunks/{LpTour-C29-1wha.js → LpTour-ByaL36fN.js} +75 -53
  24. package/dist/component-names.d.ts +1 -1
  25. package/dist/components/LpAddressInput.vue.js +1 -1
  26. package/dist/components/LpAlert.vue.d.ts +7 -0
  27. package/dist/components/LpAlert.vue.js +1 -1
  28. package/dist/components/LpAppShell.vue.js +1 -1
  29. package/dist/components/LpAutocomplete.vue.d.ts +24 -0
  30. package/dist/components/LpAutocomplete.vue.js +1 -1
  31. package/dist/components/LpAvatar.vue.js +1 -1
  32. package/dist/components/LpCard.vue.js +1 -1
  33. package/dist/components/LpChart.vue.js +1 -1
  34. package/dist/components/LpColorPicker.vue.js +1 -1
  35. package/dist/components/LpCommandPalette.vue.js +1 -1
  36. package/dist/components/LpContextMenu.vue.js +1 -1
  37. package/dist/components/LpFileTree.vue.js +1 -1
  38. package/dist/components/LpFileTreeNode.vue.js +1 -1
  39. package/dist/components/LpHints.vue.js +1 -1
  40. package/dist/components/LpLogViewer.vue.js +1 -1
  41. package/dist/components/LpModalNav.vue.js +1 -1
  42. package/dist/components/LpNotificationBell.vue.js +1 -1
  43. package/dist/components/LpPaletteHeader.vue.js +1 -1
  44. package/dist/components/LpPie.vue.js +1 -1
  45. package/dist/components/LpSidebar.vue.js +1 -1
  46. package/dist/components/LpSidebarNav.vue.js +1 -1
  47. package/dist/components/LpSwap.vue.d.ts +19 -0
  48. package/dist/components/LpSwap.vue.js +4 -0
  49. package/dist/components/LpTable.vue.js +5 -4
  50. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  51. package/dist/components/LpToaster.vue.js +1 -1
  52. package/dist/components/LpTour.vue.js +1 -1
  53. package/dist/components/tour.d.ts +9 -0
  54. package/dist/components/tour.js +22 -0
  55. package/dist/composables/useHintSystem.d.ts +2 -0
  56. package/dist/composables/useHintSystem.js +16 -4
  57. package/dist/composables/useToast.d.ts +13 -4
  58. package/dist/composables/useToast.js +13 -2
  59. package/dist/index.d.ts +3 -2
  60. package/dist/index.js +63 -61
  61. package/package.json +2 -1
  62. package/src/component-names.ts +1 -0
  63. package/src/components/LpAlert.vue +16 -5
  64. package/src/components/LpAutocomplete.vue +110 -24
  65. package/src/components/LpContextMenu.vue +6 -3
  66. package/src/components/LpSwap.vue +45 -0
  67. package/src/components/LpTable.vue +1 -0
  68. package/src/components/LpToaster.vue +1 -1
  69. package/src/components/LpTour.vue +26 -8
  70. package/src/components/tour.ts +31 -0
  71. package/src/composables/useHintSystem.ts +26 -4
  72. package/src/composables/useToast.ts +32 -5
  73. package/src/index.ts +3 -1
  74. package/src/tokens/selection.css +20 -0
@@ -0,0 +1,45 @@
1
+ <script setup lang="ts">
2
+ /*
3
+ * One piece of content standing in for another: the section picked in a side
4
+ * nav, the step of a wizard, the view chosen in a segmented control. Without
5
+ * it the old content vanishes and the new one appears in the same frame, and
6
+ * everything below jumps to the new height.
7
+ *
8
+ * <LpSidebar v-model="section" :items="items" />
9
+ * <LpSwap :swap-key="section">
10
+ * <SectionBody :name="section" />
11
+ * </LpSwap>
12
+ *
13
+ * The outgoing content fades out quickly, the incoming one rises in the way
14
+ * LpTabs panels do (`tab-in`), and the box eases between the two heights
15
+ * through LpShift, so what sits under it glides instead of jolting.
16
+ *
17
+ * `swapKey` is what decides that the content is "another one". Change it per
18
+ * section, not per record: a key that changes on every reload replays the
19
+ * entrance for data that merely refreshed.
20
+ */
21
+ import LpShift from "./LpShift.vue"
22
+
23
+ defineProps<{
24
+ /** Identity of the current content; a new value plays the swap. */
25
+ swapKey: string | number
26
+ /** Turn the motion off (the content still swaps). */
27
+ disabled?: boolean
28
+ }>()
29
+ </script>
30
+
31
+ <template>
32
+ <LpShift axis="height" :disabled="disabled">
33
+ <Transition
34
+ mode="out-in"
35
+ :css="!disabled"
36
+ enter-active-class="animate-[tab-in_220ms_var(--ease-emphasized)] motion-reduce:animate-none"
37
+ leave-active-class="transition-opacity duration-[var(--duration-fast)] ease-out motion-reduce:transition-none"
38
+ leave-to-class="opacity-0"
39
+ >
40
+ <div :key="swapKey">
41
+ <slot />
42
+ </div>
43
+ </Transition>
44
+ </LpShift>
45
+ </template>
@@ -1293,6 +1293,7 @@ const barInsetTop = computed(() =>
1293
1293
  :class="selectedSet.has(keyFor(row, index)) ? 'bg-brand-soft/40' : ''"
1294
1294
  v-bind="roving.itemProps(String(keyFor(row, index)))"
1295
1295
  :data-paint-key="selectable ? String(keyFor(row, index)) : undefined"
1296
+ :data-lp-context-row="rowMenu ? '' : undefined"
1296
1297
  @click="onRowClick(row, keyFor(row, index))"
1297
1298
  @pointerdown="onRowPointerDown($event, keyFor(row, index))"
1298
1299
  @keydown="onRowKeydown"
@@ -225,7 +225,7 @@ let copiedTimer: ReturnType<typeof setTimeout> | undefined
225
225
  const copyable = (t: ToastItem) => t.variant === "error" || t.variant === "warning"
226
226
 
227
227
  async function copyToast(t: ToastItem) {
228
- if (!(await copy([t.title, t.message].filter(Boolean).join("\n")))) return
228
+ if (!(await copy([t.title, t.message, t.details].filter(Boolean).join("\n")))) return
229
229
  copiedId.value = tag(t)
230
230
  clearTimeout(copiedTimer)
231
231
  copiedTimer = setTimeout(() => (copiedId.value = null), 1500)
@@ -37,9 +37,11 @@ export interface TourStep {
37
37
  * scrim, a card beside it says what it is, and the user steps through with the
38
38
  * buttons or the arrow keys.
39
39
  *
40
- * The scrim is four panels around a hole rather than a mask, so the target
41
- * stays clickable (a `try` step waits for exactly that click) and the hole can
42
- * glide between targets on any engine, WebKitGTK included. The card is a reka
40
+ * The dim is one blurred layer clipped around the hole (`holeClip`), so the
41
+ * blur has no seams; four transparent panels around the hole take the clicks,
42
+ * so the target stays clickable (a `try` step waits for exactly that click).
43
+ * Both glide between targets on any engine, WebKitGTK included. Closing fades
44
+ * the whole layer out instead of dropping it. The card is a reka
43
45
  * popover anchored to the hole, so flipping and sliding to stay on screen is
44
46
  * floating-ui's job; on a phone it becomes a sheet at the bottom instead.
45
47
  *
@@ -59,7 +61,7 @@ import { scrollToElement } from "../composables/scroll"
59
61
  import { PANEL_SURFACE } from "./dropdown"
60
62
  import TourCard from "./TourCard.vue"
61
63
  import type { Box } from "../composables/spatialNav"
62
- import { holeOf, panelsAround } from "./tour"
64
+ import { holeClip, holeOf, panelsAround } from "./tour"
63
65
 
64
66
  const props = withDefaults(
65
67
  defineProps<{
@@ -113,6 +115,8 @@ const panels = computed(() =>
113
115
  ? panelsAround(hole.value, viewport.value)
114
116
  : [{ x: 0, y: 0, width: viewport.value.width, height: viewport.value.height }],
115
117
  )
118
+ const radius = ref(8)
119
+ const clip = computed(() => holeClip(hole.value, viewport.value, radius.value))
116
120
  const reference = {
117
121
  getBoundingClientRect: () => {
118
122
  const h = hole.value
@@ -244,7 +248,6 @@ function end() {
244
248
  cancelAnimationFrame(frame)
245
249
  frame = 0
246
250
  target.value = null
247
- box.value = null
248
251
  runAll(cleanups)
249
252
  }
250
253
 
@@ -269,6 +272,8 @@ watch(
269
272
  (value) => {
270
273
  if (value) {
271
274
  direction = 1
275
+ box.value = null
276
+ radius.value = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--radius-control")) || 8
272
277
  document.addEventListener("click", onTargetClick, true)
273
278
  document.addEventListener("contextmenu", onTargetClick, true)
274
279
  frame ||= requestAnimationFrame(track)
@@ -307,11 +312,21 @@ const CARD = `${PANEL_SURFACE} rounded-card w-80 max-w-[calc(100vw-24px)] p-4 te
307
312
 
308
313
  <template>
309
314
  <Teleport to="body">
315
+ <Transition
316
+ enter-active-class="transition-opacity duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] motion-reduce:transition-none"
317
+ leave-active-class="transition-opacity duration-[var(--duration-medium)] ease-out motion-reduce:transition-none"
318
+ enter-from-class="opacity-0"
319
+ leave-to-class="opacity-0"
320
+ >
310
321
  <div v-if="open" class="lp-tour pointer-events-none fixed inset-0 z-(--z-tour)" @pointerdown.stop>
322
+ <div
323
+ class="lp-scrim absolute inset-0 transition-[clip-path] duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] motion-reduce:transition-none"
324
+ :style="{ clipPath: clip }"
325
+ />
311
326
  <div
312
327
  v-for="(panel, i) in panels"
313
328
  :key="i"
314
- class="lp-scrim pointer-events-auto absolute left-0 top-0 transition-[transform,width,height] duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] motion-reduce:transition-none"
329
+ class="pointer-events-auto absolute left-0 top-0"
315
330
  :style="{ transform: `translate(${panel.x}px, ${panel.y}px)`, width: `${panel.width}px`, height: `${panel.height}px` }"
316
331
  />
317
332
  <div
@@ -321,8 +336,9 @@ const CARD = `${PANEL_SURFACE} rounded-card w-80 max-w-[calc(100vw-24px)] p-4 te
321
336
  :style="{ transform: `translate(${hole.x}px, ${hole.y}px)`, width: `${hole.width}px`, height: `${hole.height}px` }"
322
337
  />
323
338
  </div>
339
+ </Transition>
324
340
 
325
- <PopoverRoot v-if="open && wide" :open="ready">
341
+ <PopoverRoot v-if="wide" :open="open && ready">
326
342
  <PopoverAnchor :reference="reference" />
327
343
  <PopoverPortal>
328
344
  <PopoverContent
@@ -331,7 +347,7 @@ const CARD = `${PANEL_SURFACE} rounded-card w-80 max-w-[calc(100vw-24px)] p-4 te
331
347
  :collision-padding="12"
332
348
  update-position-strategy="always"
333
349
  :trap-focus="false"
334
- :class="[CARD, 'z-[calc(var(--z-tour)+1)] data-[state=open]:animate-[popover-in_var(--duration-medium)_var(--ease-emphasized)] motion-reduce:animate-none']"
350
+ :class="[CARD, 'z-[calc(var(--z-tour)+1)] data-[state=open]:animate-[popover-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[popover-out_var(--duration-fast)_ease-out] motion-reduce:animate-none']"
335
351
  role="dialog"
336
352
  :aria-label="step?.title"
337
353
  @pointerdown.stop
@@ -346,6 +362,7 @@ const CARD = `${PANEL_SURFACE} rounded-card w-80 max-w-[calc(100vw-24px)] p-4 te
346
362
  </PopoverPortal>
347
363
  </PopoverRoot>
348
364
 
365
+ <Transition leave-active-class="animate-[rise-out_var(--duration-fast)_ease-out] motion-reduce:animate-none">
349
366
  <div
350
367
  v-if="open && !wide && ready"
351
368
  :class="[CARD, 'pointer-events-auto fixed inset-x-3 bottom-3 z-[calc(var(--z-tour)+1)] w-auto animate-[rise-in_var(--duration-medium)_var(--ease-emphasized)] motion-reduce:animate-none']"
@@ -357,5 +374,6 @@ const CARD = `${PANEL_SURFACE} rounded-card w-80 max-w-[calc(100vw-24px)] p-4 te
357
374
  <TourCard :step="step" :index="index" :last="last" :progress="progress" :labels="labels" @next="next" @back="back" @perform="perform" @dismiss="dismiss" />
358
375
  </slot>
359
376
  </div>
377
+ </Transition>
360
378
  </Teleport>
361
379
  </template>
@@ -29,6 +29,37 @@ export function panelsAround(hole: Box, viewport: Size): Box[] {
29
29
  ]
30
30
  }
31
31
 
32
+ const CORNER_STEPS = 6
33
+
34
+ /**
35
+ * The dimmed, blurred layer as one clip: the whole viewport with the hole cut
36
+ * out (evenodd), corners rounded like the ring drawn around it. One layer
37
+ * instead of the four panels, because each panel blurred the page on its own
38
+ * and the blur's edges showed as seams where they met. The point count never
39
+ * changes, so the clip eases between steps on every engine; with no target
40
+ * the hole shrinks to a dot in the middle.
41
+ */
42
+ export function holeClip(hole: Box | null, viewport: Size, radius: number): string {
43
+ const h = hole ?? { x: viewport.width / 2, y: viewport.height / 2, width: 0, height: 0 }
44
+ const r = Math.max(0, Math.min(radius, h.width / 2, h.height / 2))
45
+ const px = (n: number) => `${Math.round(n * 100) / 100}px`
46
+ const corners: [number, number, number][] = [
47
+ [h.x + h.width - r, h.y + r, -90],
48
+ [h.x + h.width - r, h.y + h.height - r, 0],
49
+ [h.x + r, h.y + h.height - r, 90],
50
+ [h.x + r, h.y + r, 180],
51
+ ]
52
+ const points: string[] = []
53
+ for (const [cx, cy, start] of corners) {
54
+ for (let i = 0; i <= CORNER_STEPS; i++) {
55
+ const angle = ((start + (90 * i) / CORNER_STEPS) * Math.PI) / 180
56
+ points.push(`${px(cx + r * Math.cos(angle))} ${px(cy + r * Math.sin(angle))}`)
57
+ }
58
+ }
59
+ const outer = [`0px 0px`, `${px(viewport.width)} 0px`, `${px(viewport.width)} ${px(viewport.height)}`, `0px ${px(viewport.height)}`, `0px 0px`]
60
+ return `polygon(evenodd, ${[...outer, ...points, points[0]].join(", ")})`
61
+ }
62
+
32
63
  /**
33
64
  * Open the context menu of an element as a right-click would, at its centre.
34
65
  * Lets a tour step show a row's menu without the user hunting for it; the menu
@@ -109,6 +109,31 @@ export const HINT_SIGNALS = {
109
109
  paletteActions: "lp.palette-actions",
110
110
  } as const
111
111
 
112
+ let lastContextTarget: WeakRef<Element> | null = null
113
+
114
+ /** The element a left click just landed on that also has a right-click menu (a table row rather than the table). */
115
+ export function noteContextTarget(el: Element): void {
116
+ lastContextTarget = new WeakRef(el)
117
+ }
118
+
119
+ const onScreen = (el: Element) => {
120
+ const box = el.getBoundingClientRect()
121
+ return el.isConnected && box.width > 0 && box.bottom > 0 && box.top < window.innerHeight
122
+ }
123
+
124
+ /*
125
+ * What the tip lights up: the thing the user keeps clicking, while it is still
126
+ * on screen; otherwise a row with a menu, then any menu target. Rows first,
127
+ * because a page's first target in document order is usually chrome (a
128
+ * sidebar account card), not the content the habit is about.
129
+ */
130
+ function rightClickTarget(): Element | null {
131
+ const last = lastContextTarget?.deref()
132
+ if (last && onScreen(last)) return last
133
+ const visible = (selector: string) => [...document.querySelectorAll(selector)].find(onScreen) ?? null
134
+ return visible("[data-lp-context-row]") ?? visible("main [data-lp-context-menu]") ?? visible("[data-lp-context-menu]")
135
+ }
136
+
112
137
  /** Right click instead of the "…" button. */
113
138
  export function contextMenuTip(text: Partial<Pick<HintTip, "title" | "text"> & { stepText: string }> = {}): HintTip {
114
139
  return {
@@ -119,10 +144,7 @@ export function contextMenuTip(text: Partial<Pick<HintTip, "title" | "text"> & {
119
144
  count(HINT_SIGNALS.contextMenu) === 0
120
145
  && (count(HINT_SIGNALS.menuButton) >= 5 || count(HINT_SIGNALS.contextTarget) >= 25),
121
146
  step: () => {
122
- const target = [...document.querySelectorAll("[data-lp-context-menu]")].find((el) => {
123
- const box = el.getBoundingClientRect()
124
- return box.width > 0 && box.bottom > 0 && box.top < window.innerHeight
125
- })
147
+ const target = rightClickTarget()
126
148
  return target ? { target, title: text.title ?? "Right-click works here", text: text.stepText ?? "Right-click this to see what it can do.", try: true } : null
127
149
  },
128
150
  }
@@ -23,6 +23,8 @@ export interface ToastItem {
23
23
  actions?: ToastAction[]
24
24
  /** Whole-toast click handler (UAProject-style clickable toast). */
25
25
  onClick?: () => void | Promise<void>
26
+ /** Copied with the message, not shown: what a bug report needs (request, status, ids). */
27
+ details?: string
26
28
  }
27
29
 
28
30
  export interface ToastOptions {
@@ -36,6 +38,29 @@ export interface ToastOptions {
36
38
  duration?: number
37
39
  actions?: ToastAction[]
38
40
  onClick?: () => void | Promise<void>
41
+ /** Copied with the message, not shown. An Error passed to `error()` brings its own `details`. */
42
+ details?: string
43
+ /** The error behind a hand-written message ("Not saved: " + e.message): its `details` ride along. */
44
+ cause?: unknown
45
+ }
46
+
47
+ /*
48
+ * What a toast can be made from. An Error shows its message; a `details`
49
+ * string on it (an API error's request, status and ids) rides along to the
50
+ * copy button, so "copy" yields something worth pasting into a bug report
51
+ * instead of the same one line the user already sees.
52
+ */
53
+ export type ToastMessage = string | (Error & { details?: string })
54
+
55
+ function detailsOf(source: unknown): string | undefined {
56
+ const details = (source as { details?: unknown } | null | undefined)?.details
57
+ return typeof details === "string" && details ? details : undefined
58
+ }
59
+
60
+ function split(message: ToastMessage, options: ToastOptions): [string, ToastOptions] {
61
+ const text = typeof message === "string" ? message : message.message
62
+ const details = options.details ?? (typeof message === "string" ? undefined : detailsOf(message)) ?? detailsOf(options.cause)
63
+ return [text, { ...options, details }]
39
64
  }
40
65
 
41
66
  const DEFAULT_DURATION = 4500
@@ -54,9 +79,10 @@ const queue = state.queue
54
79
 
55
80
  function push(
56
81
  variant: ToastVariant,
57
- message: string,
58
- options: ToastOptions = {},
82
+ input: ToastMessage,
83
+ given: ToastOptions = {},
59
84
  ): number {
85
+ const [message, options] = split(input, given)
60
86
  // A server render has no one to show it to, and the queue would outlive the
61
87
  // request and surface in the next visitor's page.
62
88
  if (typeof window === "undefined") return -1
@@ -70,6 +96,7 @@ function push(
70
96
  dismissAt: duration > 0 ? Date.now() + duration : null,
71
97
  actions: options.actions,
72
98
  onClick: options.onClick,
99
+ details: options.details,
73
100
  }
74
101
 
75
102
  const same = options.key === undefined ? -1 : queue.findIndex((t) => t.key === options.key)
@@ -91,7 +118,7 @@ function push(
91
118
  export interface ToastPromiseMessages<T> {
92
119
  loading: string
93
120
  success: string | ((value: T) => string)
94
- error: string | ((error: unknown) => string)
121
+ error: string | ((error: unknown) => ToastMessage)
95
122
  }
96
123
 
97
124
  let promiseKey = 0
@@ -135,8 +162,8 @@ export function useToast() {
135
162
  toast: (message: string, o?: ToastOptions) => push("info", message, o),
136
163
  info: (message: string, o?: ToastOptions) => push("info", message, o),
137
164
  success: (message: string, o?: ToastOptions) => push("success", message, o),
138
- warning: (message: string, o?: ToastOptions) => push("warning", message, o),
139
- error: (message: string, o?: ToastOptions) => push("error", message, o),
165
+ warning: (message: ToastMessage, o?: ToastOptions) => push("warning", message, o),
166
+ error: (message: ToastMessage, o?: ToastOptions) => push("error", message, o),
140
167
  promise,
141
168
  /** Low-level push for bridges (e.g. @leavepulse/ui-sdk apiError). */
142
169
  push,
package/src/index.ts CHANGED
@@ -26,7 +26,7 @@ export type { ActionBarAction } from "./components/LpActionBar.vue"
26
26
  export { default as LpAlert } from "./components/LpAlert.vue"
27
27
  export { default as LpAppShell } from "./components/LpAppShell.vue"
28
28
  export { default as LpAutocomplete } from "./components/LpAutocomplete.vue"
29
- export type { AutocompleteOption } from "./components/LpAutocomplete.vue"
29
+ export type { AutocompleteOption, AutocompleteSelection } from "./components/LpAutocomplete.vue"
30
30
  export { default as LpAvatar } from "./components/LpAvatar.vue"
31
31
  export { default as LpBadge } from "./components/LpBadge.vue"
32
32
  export { default as LpBreadcrumbs } from "./components/LpBreadcrumbs.vue"
@@ -175,6 +175,7 @@ export type { SelectOption, SelectValue } from "./components/LpSelect.vue"
175
175
  export { default as LpServiceNode } from "./components/LpServiceNode.vue"
176
176
  export type { ServiceNodeData } from "./components/LpServiceNode.vue"
177
177
  export { default as LpShift } from "./components/LpShift.vue"
178
+ export { default as LpSwap } from "./components/LpSwap.vue"
178
179
  export { default as LpShortcutsDialog } from "./components/LpShortcutsDialog.vue"
179
180
  export { default as LpSidebar } from "./components/LpSidebar.vue"
180
181
  export type { SidebarItem, SidebarSection } from "./components/LpSidebar.vue"
@@ -364,6 +365,7 @@ export { useToast } from "./composables/useToast"
364
365
  export type {
365
366
  ToastAction,
366
367
  ToastItem,
368
+ ToastMessage,
367
369
  ToastOptions,
368
370
  ToastVariant,
369
371
  } from "./composables/useToast"
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Text selection in the theme's colours — opt-in, like scrollbar.css. Without
3
+ * it the browser paints selected text with the operating system's accent
4
+ * (red, green, whatever the desktop theme picked), which clashes with the kit
5
+ * palette and changes from machine to machine; WebKitGTK and Chromium on Linux
6
+ * both read it from the GTK theme.
7
+ *
8
+ * Global on purpose, so it lives outside tokens.css (which stays embeddable).
9
+ * Reads --color-brand, so import it AFTER the tokens:
10
+ * @import "@leavepulse/ui";
11
+ * @import "@leavepulse/ui/selection.css";
12
+ *
13
+ * In `@layer base`, so any rule of the app's own wins over it.
14
+ */
15
+ @layer base {
16
+ ::selection {
17
+ background-color: color-mix(in oklab, var(--color-brand, #7a8a84) 38%, transparent);
18
+ color: inherit;
19
+ }
20
+ }