@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.
- package/README.md +8 -0
- package/dist/assets/{LpTable-BZZf_rrt.css → LpTable-DpXyPzs8.css} +19 -19
- package/dist/chunks/{LpAddressInput-DD4zTN4P.js → LpAddressInput-CzdZguBo.js} +1 -1
- package/dist/chunks/{LpAlert--KLL4NLk.js → LpAlert-BAOujnXy.js} +8 -6
- package/dist/chunks/{LpAppShell-Cv-Y9qNA.js → LpAppShell-BXMNDEpF.js} +1 -1
- package/dist/chunks/{LpAutocomplete-PnL2ntKl.js → LpAutocomplete-D9mY5Tqk.js} +101 -36
- package/dist/chunks/{LpAvatar-CwGs1JCQ.js → LpAvatar-K6MOMfgn.js} +1 -1
- package/dist/chunks/{LpCard-B2D0GIUU.js → LpCard-D9HVgX8Y.js} +1 -1
- package/dist/chunks/{LpColorPicker-CvBF8TeL.js → LpColorPicker-BO1ZTY51.js} +1 -1
- package/dist/chunks/{LpContextMenu-BnN1k7ga.js → LpContextMenu-Cv_F2iCG.js} +6 -3
- package/dist/chunks/{LpFileTree-DEqh2ySW.js → LpFileTree-DTwUj0L-.js} +1 -1
- package/dist/chunks/{LpFileTreeNode-DCZA7sQ-.js → LpFileTreeNode-D-XLPZ6T.js} +1 -1
- package/dist/chunks/{LpHints-BTpvisgP.js → LpHints-B_85eSmm.js} +1 -1
- package/dist/chunks/{LpLogViewer-BuPe23sZ.js → LpLogViewer-D9UgNz2m.js} +1 -1
- package/dist/chunks/{LpModalNav-Cw7m-eks.js → LpModalNav-D4rmflQn.js} +1 -1
- package/dist/chunks/{LpNotificationBell-xH_JphpS.js → LpNotificationBell-CyylOlZR.js} +1 -1
- package/dist/chunks/{LpPie-BD5Lsh-2.js → LpPie-B2deu8CI.js} +1 -1
- package/dist/chunks/{LpSidebar-B3E_iViL.js → LpSidebar-D8tub3bt.js} +1 -1
- package/dist/chunks/{LpSidebarNav-32sAXNM-.js → LpSidebarNav-BBSRg34G.js} +1 -1
- package/dist/chunks/LpSwap-D82jiecV.js +38 -0
- package/dist/chunks/{LpThemeSwitcher-CeBG0e_D.js → LpThemeSwitcher-ZFFftBQn.js} +1 -1
- package/dist/chunks/{LpToaster-BcKs7vX7.js → LpToaster-D3OisETH.js} +1 -1
- package/dist/chunks/{LpTour-C29-1wha.js → LpTour-ByaL36fN.js} +75 -53
- package/dist/component-names.d.ts +1 -1
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAlert.vue.d.ts +7 -0
- package/dist/components/LpAlert.vue.js +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpAutocomplete.vue.d.ts +24 -0
- package/dist/components/LpAutocomplete.vue.js +1 -1
- package/dist/components/LpAvatar.vue.js +1 -1
- package/dist/components/LpCard.vue.js +1 -1
- package/dist/components/LpChart.vue.js +1 -1
- package/dist/components/LpColorPicker.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.js +1 -1
- package/dist/components/LpContextMenu.vue.js +1 -1
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpHints.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpPaletteHeader.vue.js +1 -1
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpSwap.vue.d.ts +19 -0
- package/dist/components/LpSwap.vue.js +4 -0
- package/dist/components/LpTable.vue.js +5 -4
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpToaster.vue.js +1 -1
- package/dist/components/LpTour.vue.js +1 -1
- package/dist/components/tour.d.ts +9 -0
- package/dist/components/tour.js +22 -0
- package/dist/composables/useHintSystem.d.ts +2 -0
- package/dist/composables/useHintSystem.js +16 -4
- package/dist/composables/useToast.d.ts +13 -4
- package/dist/composables/useToast.js +13 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +63 -61
- package/package.json +2 -1
- package/src/component-names.ts +1 -0
- package/src/components/LpAlert.vue +16 -5
- package/src/components/LpAutocomplete.vue +110 -24
- package/src/components/LpContextMenu.vue +6 -3
- package/src/components/LpSwap.vue +45 -0
- package/src/components/LpTable.vue +1 -0
- package/src/components/LpToaster.vue +1 -1
- package/src/components/LpTour.vue +26 -8
- package/src/components/tour.ts +31 -0
- package/src/composables/useHintSystem.ts +26 -4
- package/src/composables/useToast.ts +32 -5
- package/src/index.ts +3 -1
- 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
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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="
|
|
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="
|
|
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>
|
package/src/components/tour.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
58
|
-
|
|
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) =>
|
|
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:
|
|
139
|
-
error: (message:
|
|
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
|
+
}
|