@leavepulse/ui 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/dist/component-names.d.ts +1 -0
  4. package/dist/components/LayoutCanvas.vue.d.ts +28 -0
  5. package/dist/components/LayoutNode.vue.d.ts +40 -0
  6. package/dist/components/LpAlert.vue.d.ts +44 -0
  7. package/dist/components/LpAppShell.vue.d.ts +83 -0
  8. package/dist/components/LpAutocomplete.vue.d.ts +50 -0
  9. package/dist/components/LpAvatar.vue.d.ts +20 -0
  10. package/dist/components/LpBadge.vue.d.ts +47 -0
  11. package/dist/components/LpBreadcrumbs.vue.d.ts +14 -0
  12. package/dist/components/LpButton.vue.d.ts +113 -0
  13. package/dist/components/LpCalendar.vue.d.ts +20 -0
  14. package/dist/components/LpCard.vue.d.ts +29 -0
  15. package/dist/components/LpCheckbox.vue.d.ts +28 -0
  16. package/dist/components/LpCodeBlock.vue.d.ts +39 -0
  17. package/dist/components/LpCommandPalette.vue.d.ts +42 -0
  18. package/dist/components/LpConfirmDialog.vue.d.ts +32 -0
  19. package/dist/components/LpContextMenu.vue.d.ts +30 -0
  20. package/dist/components/LpDatePicker.vue.d.ts +25 -0
  21. package/dist/components/LpDisclosure.vue.d.ts +37 -0
  22. package/dist/components/LpDivider.vue.d.ts +17 -0
  23. package/dist/components/LpDrawer.vue.d.ts +88 -0
  24. package/dist/components/LpDropdownMenu.vue.d.ts +23 -0
  25. package/dist/components/LpEmptyState.vue.d.ts +18 -0
  26. package/dist/components/LpFormField.vue.d.ts +21 -0
  27. package/dist/components/LpIcon.vue.d.ts +8 -0
  28. package/dist/components/LpInfraNode.vue.d.ts +21 -0
  29. package/dist/components/LpInput.vue.d.ts +68 -0
  30. package/dist/components/LpLaneNode.vue.d.ts +24 -0
  31. package/dist/components/LpLink.vue.d.ts +27 -0
  32. package/dist/components/LpLogViewer.vue.d.ts +92 -0
  33. package/dist/components/LpModal.vue.d.ts +43 -0
  34. package/dist/components/LpNotificationBell.vue.d.ts +51 -0
  35. package/dist/components/LpNumberField.vue.d.ts +14 -0
  36. package/dist/components/LpOtpInput.vue.d.ts +27 -0
  37. package/dist/components/LpPagination.vue.d.ts +25 -0
  38. package/dist/components/LpPasswordInput.vue.d.ts +28 -0
  39. package/dist/components/LpPhoneInput.vue.d.ts +37 -0
  40. package/dist/components/LpPopover.vue.d.ts +34 -0
  41. package/dist/components/LpProgress.vue.d.ts +12 -0
  42. package/dist/components/LpRadio.vue.d.ts +18 -0
  43. package/dist/components/LpRadioGroup.vue.d.ts +26 -0
  44. package/dist/components/LpScrollArea.vue.d.ts +24 -0
  45. package/dist/components/LpSegmented.vue.d.ts +24 -0
  46. package/dist/components/LpSelect.vue.d.ts +45 -0
  47. package/dist/components/LpServiceNode.vue.d.ts +14 -0
  48. package/dist/components/LpSidebar.vue.d.ts +85 -0
  49. package/dist/components/LpSidebarNav.vue.d.ts +34 -0
  50. package/dist/components/LpSkeleton.vue.d.ts +6 -0
  51. package/dist/components/LpSlider.vue.d.ts +23 -0
  52. package/dist/components/LpStat.vue.d.ts +22 -0
  53. package/dist/components/LpStepper.vue.d.ts +10 -0
  54. package/dist/components/LpSwitch.vue.d.ts +11 -0
  55. package/dist/components/LpTable.vue.d.ts +63 -0
  56. package/dist/components/LpTableOfContents.vue.d.ts +19 -0
  57. package/dist/components/LpTabs.vue.d.ts +45 -0
  58. package/dist/components/LpTextarea.vue.d.ts +17 -0
  59. package/dist/components/LpThemeSwitcher.vue.d.ts +40 -0
  60. package/dist/components/LpToaster.vue.d.ts +3 -0
  61. package/dist/components/LpTooltip.vue.d.ts +19 -0
  62. package/dist/components/LpTopologyCanvas.vue.d.ts +154 -0
  63. package/dist/components/LpUptimeBar.vue.d.ts +30 -0
  64. package/dist/components/codeHighlight.d.ts +8 -0
  65. package/dist/components/countries-data.d.ts +2 -0
  66. package/dist/components/countries.d.ts +20 -0
  67. package/dist/components/dropdown.d.ts +29 -0
  68. package/dist/components/sidebar.d.ts +18 -0
  69. package/dist/composables/useInputFilter.d.ts +14 -0
  70. package/dist/composables/usePillTransition.d.ts +9 -0
  71. package/dist/composables/useToast.d.ts +39 -0
  72. package/dist/countries-data-9rgqJWHo.js +203 -0
  73. package/dist/icons.d.ts +1 -0
  74. package/dist/index.css +1 -0
  75. package/dist/index.d.ts +100 -0
  76. package/dist/index.js +17600 -0
  77. package/dist/layout/registry.d.ts +26 -0
  78. package/dist/layout/tree.d.ts +75 -0
  79. package/dist/layout/useLayout.d.ts +39 -0
  80. package/dist/theme/config.d.ts +16 -0
  81. package/dist/theme/define.d.ts +12 -0
  82. package/dist/theme/presets.d.ts +20 -0
  83. package/dist/theme/tokens.d.ts +72 -0
  84. package/dist/theme/useTheme.d.ts +36 -0
  85. package/package.json +93 -0
  86. package/src/canvas.css +67 -0
  87. package/src/component-names.ts +65 -0
  88. package/src/components/LayoutCanvas.vue +76 -0
  89. package/src/components/LayoutNode.vue +248 -0
  90. package/src/components/LpAlert.vue +59 -0
  91. package/src/components/LpAppShell.vue +210 -0
  92. package/src/components/LpAutocomplete.vue +244 -0
  93. package/src/components/LpAvatar.vue +67 -0
  94. package/src/components/LpBadge.vue +32 -0
  95. package/src/components/LpBreadcrumbs.vue +35 -0
  96. package/src/components/LpButton.vue +111 -0
  97. package/src/components/LpCalendar.vue +134 -0
  98. package/src/components/LpCard.vue +71 -0
  99. package/src/components/LpCheckbox.vue +44 -0
  100. package/src/components/LpCodeBlock.vue +298 -0
  101. package/src/components/LpCommandPalette.vue +323 -0
  102. package/src/components/LpConfirmDialog.vue +49 -0
  103. package/src/components/LpContextMenu.vue +111 -0
  104. package/src/components/LpDatePicker.vue +117 -0
  105. package/src/components/LpDisclosure.vue +92 -0
  106. package/src/components/LpDivider.vue +20 -0
  107. package/src/components/LpDrawer.vue +402 -0
  108. package/src/components/LpDropdownMenu.vue +54 -0
  109. package/src/components/LpEmptyState.vue +21 -0
  110. package/src/components/LpFormField.vue +31 -0
  111. package/src/components/LpIcon.vue +49 -0
  112. package/src/components/LpInfraNode.vue +108 -0
  113. package/src/components/LpInput.vue +147 -0
  114. package/src/components/LpLaneNode.vue +48 -0
  115. package/src/components/LpLink.vue +48 -0
  116. package/src/components/LpLogViewer.vue +569 -0
  117. package/src/components/LpModal.vue +123 -0
  118. package/src/components/LpNotificationBell.vue +238 -0
  119. package/src/components/LpNumberField.vue +56 -0
  120. package/src/components/LpOtpInput.vue +81 -0
  121. package/src/components/LpPagination.vue +165 -0
  122. package/src/components/LpPasswordInput.vue +50 -0
  123. package/src/components/LpPhoneInput.vue +241 -0
  124. package/src/components/LpPopover.vue +42 -0
  125. package/src/components/LpProgress.vue +30 -0
  126. package/src/components/LpRadio.vue +28 -0
  127. package/src/components/LpRadioGroup.vue +43 -0
  128. package/src/components/LpScrollArea.vue +82 -0
  129. package/src/components/LpSegmented.vue +83 -0
  130. package/src/components/LpSelect.vue +0 -0
  131. package/src/components/LpServiceNode.vue +57 -0
  132. package/src/components/LpSidebar.vue +200 -0
  133. package/src/components/LpSidebarNav.vue +118 -0
  134. package/src/components/LpSkeleton.vue +16 -0
  135. package/src/components/LpSlider.vue +53 -0
  136. package/src/components/LpStat.vue +90 -0
  137. package/src/components/LpStepper.vue +46 -0
  138. package/src/components/LpSwitch.vue +19 -0
  139. package/src/components/LpTable.vue +252 -0
  140. package/src/components/LpTableOfContents.vue +183 -0
  141. package/src/components/LpTabs.vue +120 -0
  142. package/src/components/LpTextarea.vue +39 -0
  143. package/src/components/LpThemeSwitcher.vue +168 -0
  144. package/src/components/LpToaster.vue +216 -0
  145. package/src/components/LpTooltip.vue +30 -0
  146. package/src/components/LpTopologyCanvas.vue +370 -0
  147. package/src/components/LpUptimeBar.vue +126 -0
  148. package/src/components/codeHighlight.ts +137 -0
  149. package/src/components/countries-data.ts +207 -0
  150. package/src/components/countries.ts +67 -0
  151. package/src/components/dropdown.ts +64 -0
  152. package/src/components/sidebar.ts +26 -0
  153. package/src/composables/useInputFilter.ts +89 -0
  154. package/src/composables/usePillTransition.ts +19 -0
  155. package/src/composables/useToast.ts +92 -0
  156. package/src/icons.ts +11 -0
  157. package/src/index.css +21 -0
  158. package/src/index.ts +146 -0
  159. package/src/layout/registry.ts +44 -0
  160. package/src/layout/tree.ts +288 -0
  161. package/src/layout/useLayout.ts +134 -0
  162. package/src/nuxt.ts +22 -0
  163. package/src/theme/config.ts +48 -0
  164. package/src/theme/define.ts +50 -0
  165. package/src/theme/presets.ts +279 -0
  166. package/src/theme/tokens.ts +151 -0
  167. package/src/theme/useTheme.ts +288 -0
  168. package/src/tokens/scrollbar.css +138 -0
  169. package/src/tokens/tokens.css +333 -0
  170. package/src/tokens/utilities.css +23 -0
@@ -0,0 +1,168 @@
1
+ <script setup lang="ts">
2
+ /*
3
+ * Theme switcher. The kit convention for theming, so consumers stop hand-rolling
4
+ * their own. Default behaviour mirrors LeaveHosting: LEFT-click cycles to the
5
+ * next theme in the list (with the circular-reveal transition from the click
6
+ * point); RIGHT-click opens a menu to pick any theme directly. Fully data-driven
7
+ * and configurable — pass your own `themes`, bind `v-model` to the active name,
8
+ * and pick how the trigger looks.
9
+ *
10
+ * Applying + caching goes through useTheme() so every kit component re-skins live
11
+ * and the choice survives a reload (anti-flash bootstrap). The component never
12
+ * fetches; it just drives the theme engine.
13
+ */
14
+ import { computed, onMounted } from "vue"
15
+ import { presets } from "../theme/presets"
16
+ import type { TokenSet } from "../theme/tokens"
17
+ import { useTheme } from "../theme/useTheme"
18
+ import LpContextMenu, { type ContextMenuItemDef } from "./LpContextMenu.vue"
19
+ import LpIcon from "./LpIcon.vue"
20
+
21
+ const props = withDefaults(
22
+ defineProps<{
23
+ /** Active theme NAME (v-model). Falls back to the first theme. */
24
+ modelValue?: string
25
+ /** Themes to offer. Defaults to all built-in presets. */
26
+ themes?: TokenSet[]
27
+ /**
28
+ * Trigger style:
29
+ * - "swatch" (default): a square showing the active theme's brand colour.
30
+ * - "icon": a sun/moon icon following the active theme's mode.
31
+ * - "pill": swatch + the theme name in a pill (always shows the label).
32
+ */
33
+ variant?: "swatch" | "icon" | "pill"
34
+ /** Show the active theme's name next to the trigger (forced on for "pill"). */
35
+ showLabel?: boolean
36
+ /**
37
+ * Default theme applied on mount when nothing is cached yet — the theme NAME
38
+ * (must be in `themes`) or a TokenSet. Lets a consuming app pick its default
39
+ * declaratively (<LpThemeSwitcher :default="'Violet'">) with no separate
40
+ * bootstrap plugin; a previously saved choice still wins over it.
41
+ */
42
+ default?: string | TokenSet
43
+ /** Animate theme changes with the circular reveal. On by default. */
44
+ transition?: boolean
45
+ size?: "sm" | "md" | "lg"
46
+ "aria-label"?: string
47
+ }>(),
48
+ {
49
+ variant: "swatch",
50
+ transition: true,
51
+ size: "md",
52
+ },
53
+ )
54
+
55
+ const emit = defineEmits<{
56
+ (e: "update:modelValue", name: string): void
57
+ (e: "change", theme: TokenSet): void
58
+ }>()
59
+
60
+ const theme = useTheme()
61
+
62
+ const list = computed<TokenSet[]>(() =>
63
+ props.themes && props.themes.length ? props.themes : Object.values(presets),
64
+ )
65
+ // Active theme name: an explicit modelValue wins (controlled use); otherwise the
66
+ // kit's self-managed `currentName` (so a consumer needs NO ref/bootstrap — the
67
+ // kit remembers the theme like an i18n locale). Falls back to the first theme.
68
+ const activeName = computed(
69
+ () => props.modelValue ?? (theme.currentName.value || list.value[0]?.name || ""),
70
+ )
71
+ const active = computed<TokenSet | undefined>(
72
+ () => list.value.find((t) => t.name === activeName.value) ?? list.value[0],
73
+ )
74
+
75
+ // Apply the consumer's default on mount when nothing is cached yet. init()
76
+ // hydrates from the cache first (a saved choice wins), so this only takes effect
77
+ // on a first visit / cleared storage. Resolving a name goes through the offered
78
+ // list, so <LpThemeSwitcher :default="'Violet'"> just works.
79
+ onMounted(() => {
80
+ if (props.default == null) return
81
+ const fallback =
82
+ typeof props.default === "string"
83
+ ? list.value.find((t) => t.name === props.default)
84
+ : props.default
85
+ if (fallback) theme.init(fallback)
86
+ })
87
+
88
+ const SIZES = { sm: "size-7", md: "size-8", lg: "size-9" } as const
89
+ const ICON_SIZE = { sm: 15, md: 16, lg: 18 } as const
90
+
91
+ const wantsLabel = computed(() => props.showLabel || props.variant === "pill")
92
+
93
+ // Apply a theme through the kit's self-managed setter (it persists + updates
94
+ // the shared `current`), then notify. The reveal animates from the click point.
95
+ function applyTheme(t: TokenSet, ev?: MouseEvent) {
96
+ if (props.transition) {
97
+ theme.setTheme(t, ev ? { x: ev.clientX, y: ev.clientY } : undefined)
98
+ } else {
99
+ theme.apply(t)
100
+ }
101
+ emit("update:modelValue", t.name)
102
+ emit("change", t)
103
+ }
104
+
105
+ // Left-click → next theme in the list (wraps), revealing from the click point.
106
+ function cycle(ev: MouseEvent) {
107
+ const arr = list.value
108
+ if (!arr.length) return
109
+ const i = arr.findIndex((t) => t.name === activeName.value)
110
+ const next = arr[(i + 1) % arr.length]
111
+ if (next) applyTheme(next, ev)
112
+ }
113
+
114
+ // Right-click menu: every theme, the active one checked. A circular-reveal from
115
+ // the (saved) click point isn't available here, so the menu picks reveal from
116
+ // the trigger centre via applyWithTransition's default origin.
117
+ const menu = computed<ContextMenuItemDef[]>(() =>
118
+ list.value.map((t) => ({
119
+ label: t.name,
120
+ icon:
121
+ t.name === activeName.value
122
+ ? "lucide:check"
123
+ : t.mode === "dark"
124
+ ? "lucide:moon"
125
+ : "lucide:sun",
126
+ onSelect: () => applyTheme(t),
127
+ })),
128
+ )
129
+
130
+ const triggerLabel = computed(
131
+ () => props["aria-label"] ?? `Theme: ${activeName.value}. Click to switch, right-click to choose.`,
132
+ )
133
+ </script>
134
+
135
+ <template>
136
+ <LpContextMenu :items="menu">
137
+ <button
138
+ type="button"
139
+ :aria-label="triggerLabel"
140
+ :title="triggerLabel"
141
+ class="inline-flex items-center gap-2 rounded-control border border-line bg-surface-raised px-1.5 text-ink outline-none transition-colors hover:border-line-strong focus-visible:ring-2 focus-visible:ring-ring"
142
+ :class="wantsLabel ? 'py-1 pr-2.5' : ''"
143
+ @click="cycle"
144
+ >
145
+ <!-- swatch: brand over surface, hinting the theme at a glance -->
146
+ <span
147
+ v-if="variant !== 'icon'"
148
+ class="grid shrink-0 place-items-center rounded-control border border-line/60"
149
+ :class="SIZES[size]"
150
+ :style="{ background: active?.colors.surface }"
151
+ >
152
+ <span
153
+ class="size-3 rounded-full"
154
+ :style="{ background: active?.colors.brand }"
155
+ />
156
+ </span>
157
+ <!-- icon: sun/moon by mode -->
158
+ <span v-else class="grid shrink-0 place-items-center" :class="SIZES[size]">
159
+ <LpIcon
160
+ :name="active?.mode === 'dark' ? 'lucide:moon' : 'lucide:sun'"
161
+ :size="ICON_SIZE[size]"
162
+ />
163
+ </span>
164
+
165
+ <span v-if="wantsLabel" class="text-sm font-medium">{{ activeName }}</span>
166
+ </button>
167
+ </LpContextMenu>
168
+ </template>
@@ -0,0 +1,216 @@
1
+ <script setup lang="ts">
2
+ /*
3
+ * Mount once near the app root. Renders the shared toast queue on the top
4
+ * z-layer (--z-toast) — above modals on purpose. Minimalist look: icon + text
5
+ * + a countdown progress bar. Colors come from tokens, so it follows the theme.
6
+ */
7
+ import {
8
+ ToastDescription,
9
+ ToastProvider,
10
+ ToastRoot,
11
+ ToastTitle,
12
+ ToastViewport,
13
+ } from "reka-ui"
14
+ import { onBeforeUnmount, ref, watch } from "vue"
15
+ import { useToast, type ToastItem } from "../composables/useToast"
16
+ import { CLOSE_ICON } from "./dropdown"
17
+ import LpIcon from "./LpIcon.vue"
18
+
19
+ const { queue, dismiss } = useToast()
20
+
21
+ // Toasts that are playing their exit animation. We don't splice them out of the
22
+ // queue immediately on close — that would unmount before `toast-out` can run.
23
+ // Instead we flip `open` to false (reka sets data-state=closed → the keyframe
24
+ // plays), then drop the toast for real once `animationend` fires. A safety
25
+ // timer removes it even if the animationend event is missed (e.g. tab hidden).
26
+ const closing = ref<Set<number>>(new Set())
27
+
28
+ function beginClose(id: number) {
29
+ if (closing.value.has(id)) return
30
+ closing.value = new Set(closing.value).add(id)
31
+ // Fallback slightly longer than the 140ms toast-out duration.
32
+ window.setTimeout(() => finishClose(id), 220)
33
+ }
34
+
35
+ function finishClose(id: number) {
36
+ if (!closing.value.has(id)) return
37
+ const next = new Set(closing.value)
38
+ next.delete(id)
39
+ closing.value = next
40
+ dismiss(id)
41
+ }
42
+
43
+ function onAnimEnd(e: AnimationEvent, id: number) {
44
+ // Only the exit keyframe should trigger removal, not toast-in.
45
+ if (e.animationName === "toast-out") finishClose(id)
46
+ }
47
+
48
+ const variantIcon: Record<ToastItem["variant"], string> = {
49
+ info: "lucide:info",
50
+ success: "lucide:circle-check",
51
+ warning: "lucide:triangle-alert",
52
+ error: "lucide:circle-x",
53
+ }
54
+
55
+ // One token color drives icon + progress per variant.
56
+ const variantColor: Record<ToastItem["variant"], string> = {
57
+ info: "text-brand",
58
+ success: "text-action",
59
+ warning: "text-accent",
60
+ error: "text-danger",
61
+ }
62
+ const variantBar: Record<ToastItem["variant"], string> = {
63
+ info: "bg-brand",
64
+ success: "bg-action",
65
+ warning: "bg-accent",
66
+ error: "bg-danger",
67
+ }
68
+
69
+ // reka pauses its own auto-dismiss timer while a toast is hovered/focused; mirror
70
+ // that on the visual bar by carrying each toast's dismissAt forward over the
71
+ // paused span, so the countdown matches when it actually closes.
72
+ const pausedAt = new Map<number, number>()
73
+
74
+ // Ticking clock so the progress bar animates smoothly. Runs only while there's
75
+ // at least one auto-dismissing toast on screen — no idle interval otherwise.
76
+ const now = ref(Date.now())
77
+ let timer: ReturnType<typeof setInterval> | null = null
78
+
79
+ function startTimer() {
80
+ if (timer === null) timer = setInterval(() => (now.value = Date.now()), 80)
81
+ }
82
+ function stopTimer() {
83
+ if (timer !== null) {
84
+ clearInterval(timer)
85
+ timer = null
86
+ }
87
+ }
88
+
89
+ watch(
90
+ () => queue.length,
91
+ () => {
92
+ // Drop pause state for toasts that have left the queue (e.g. dismissed
93
+ // while hovered) so the map can't grow unbounded over a long session.
94
+ for (const id of pausedAt.keys()) {
95
+ if (!queue.some((t) => t.id === id)) pausedAt.delete(id)
96
+ }
97
+ // Same for the closing set — a toast evicted by the queue cap or clear()
98
+ // leaves without an animationend, so prune dead ids here.
99
+ if (closing.value.size) {
100
+ const live = new Set([...closing.value].filter((id) => queue.some((t) => t.id === id)))
101
+ if (live.size !== closing.value.size) closing.value = live
102
+ }
103
+ if (queue.some((t) => t.dismissAt !== null)) startTimer()
104
+ else stopTimer()
105
+ },
106
+ { immediate: true },
107
+ )
108
+ onBeforeUnmount(stopTimer)
109
+
110
+ function onPause(t: ToastItem) {
111
+ pausedAt.set(t.id, now.value)
112
+ }
113
+ function onResume(t: ToastItem) {
114
+ const since = pausedAt.get(t.id)
115
+ if (since !== undefined && t.dismissAt !== null) {
116
+ t.dismissAt += Date.now() - since
117
+ }
118
+ pausedAt.delete(t.id)
119
+ }
120
+
121
+ function progress(t: ToastItem): number {
122
+ if (!t.dismissAt || t.duration <= 0) return 0
123
+ // While paused, freeze the bar at the moment hover started.
124
+ const at = pausedAt.get(t.id) ?? now.value
125
+ const remaining = t.dismissAt - at
126
+ return Math.max(0, Math.min(100, (remaining / t.duration) * 100))
127
+ }
128
+
129
+ async function runClick(t: ToastItem) {
130
+ if (t.onClick) {
131
+ await t.onClick()
132
+ beginClose(t.id)
133
+ }
134
+ }
135
+
136
+ async function runAction(t: ToastItem, action: NonNullable<ToastItem["actions"]>[number]) {
137
+ await action.onClick()
138
+ beginClose(t.id)
139
+ }
140
+ </script>
141
+
142
+ <template>
143
+ <ToastProvider>
144
+ <ToastRoot
145
+ v-for="t in queue"
146
+ :key="t.id"
147
+ :open="!closing.has(t.id)"
148
+ :duration="t.duration"
149
+ class="pointer-events-auto relative overflow-hidden rounded-card border border-line bg-surface-raised shadow-panel data-[state=open]:animate-[toast-in_200ms_var(--ease-emphasized)] data-[state=closed]:animate-[toast-out_140ms_ease]"
150
+ :class="{ 'cursor-pointer': t.onClick }"
151
+ @update:open="(open) => !open && beginClose(t.id)"
152
+ @animationend="(e: AnimationEvent) => onAnimEnd(e, t.id)"
153
+ @pause="onPause(t)"
154
+ @resume="onResume(t)"
155
+ @click="runClick(t)"
156
+ >
157
+ <div class="flex items-start gap-3 p-3.5">
158
+ <LpIcon
159
+ :name="variantIcon[t.variant]"
160
+ :size="18"
161
+ class="mt-0.5 shrink-0"
162
+ :class="variantColor[t.variant]"
163
+ />
164
+ <div class="flex min-w-0 flex-1 flex-col gap-0.5">
165
+ <ToastTitle v-if="t.title" class="text-sm font-semibold text-ink">
166
+ {{ t.title }}
167
+ </ToastTitle>
168
+ <ToastDescription class="break-words text-xs text-muted">
169
+ {{ t.message }}
170
+ </ToastDescription>
171
+
172
+ <div
173
+ v-if="t.actions?.length"
174
+ class="mt-2 flex flex-wrap gap-2"
175
+ @click.stop
176
+ >
177
+ <button
178
+ v-for="(action, i) in t.actions"
179
+ :key="i"
180
+ type="button"
181
+ class="rounded-control border border-line bg-surface-soft px-2.5 py-1 text-xs font-medium text-ink outline-none transition-[border-color,background-color,scale] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] hover:border-line-strong hover:bg-surface-soft/70 active:scale-95 motion-reduce:active:scale-100 focus-visible:ring-2 focus-visible:ring-ring"
182
+ @click="runAction(t, action)"
183
+ >
184
+ {{ action.label }}
185
+ </button>
186
+ </div>
187
+ </div>
188
+
189
+ <button
190
+ type="button"
191
+ class="group -mr-1 -mt-1 rounded-md p-1 text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink"
192
+ aria-label="Dismiss"
193
+ @click.stop="beginClose(t.id)"
194
+ >
195
+ <LpIcon
196
+ name="lucide:x"
197
+ :size="16"
198
+ :class="CLOSE_ICON"
199
+ />
200
+ </button>
201
+ </div>
202
+
203
+ <!-- Countdown progress bar -->
204
+ <div
205
+ v-if="t.dismissAt"
206
+ class="absolute inset-x-0 bottom-0 h-0.5 origin-left transition-transform duration-100 ease-linear motion-reduce:transition-none"
207
+ :class="variantBar[t.variant]"
208
+ :style="{ transform: `scaleX(${progress(t) / 100})` }"
209
+ />
210
+ </ToastRoot>
211
+
212
+ <ToastViewport
213
+ class="fixed bottom-0 right-0 z-(--z-toast) flex w-[min(92vw,22rem)] flex-col gap-2 p-4 outline-none"
214
+ />
215
+ </ToastProvider>
216
+ </template>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ TooltipContent,
4
+ TooltipPortal,
5
+ TooltipProvider,
6
+ TooltipRoot,
7
+ TooltipTrigger,
8
+ } from "reka-ui"
9
+ import { PANEL_SURFACE, TOOLTIP_ANIM } from "./dropdown"
10
+
11
+ defineProps<{ content?: string; sideOffset?: number }>()
12
+ </script>
13
+
14
+ <template>
15
+ <TooltipProvider :delay-duration="200">
16
+ <TooltipRoot>
17
+ <TooltipTrigger as-child>
18
+ <slot />
19
+ </TooltipTrigger>
20
+ <TooltipPortal>
21
+ <TooltipContent
22
+ :side-offset="sideOffset ?? 6"
23
+ :class="[PANEL_SURFACE, TOOLTIP_ANIM, 'z-(--z-tooltip) px-2.5 py-1.5 text-xs text-ink']"
24
+ >
25
+ <slot name="content">{{ content }}</slot>
26
+ </TooltipContent>
27
+ </TooltipPortal>
28
+ </TooltipRoot>
29
+ </TooltipProvider>
30
+ </template>