@nuxtseo/design-system 0.0.0-stage → 0.1.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 (225) hide show
  1. package/README.md +101 -2
  2. package/app/app.config.ts +596 -0
  3. package/app/component-attributes.d.ts +8 -0
  4. package/app/components/README.md +38 -0
  5. package/app/components/brand/UiBrandMark.vue +63 -0
  6. package/app/components/brand/UiModuleIcon.vue +45 -0
  7. package/app/components/brand/UiProLettering.vue +26 -0
  8. package/app/components/brand/UiRangeTerrain.vue +311 -0
  9. package/app/components/brand/range-glyphs.ts +18 -0
  10. package/app/components/container/BrowserTab.vue +145 -0
  11. package/app/components/container/IntegrationRow.vue +120 -0
  12. package/app/components/container/KnowDoRails.vue +40 -0
  13. package/app/components/container/RailPanel.vue +93 -0
  14. package/app/components/container/UiAlert.vue +120 -0
  15. package/app/components/container/UiAppShell.vue +165 -0
  16. package/app/components/container/UiCard.vue +204 -0
  17. package/app/components/container/UiDisclosure.vue +43 -0
  18. package/app/components/container/UiEmptyState.vue +102 -0
  19. package/app/components/container/UiErrorState.vue +61 -0
  20. package/app/components/container/UiLoadingState.vue +20 -0
  21. package/app/components/container/UiPageHeader.vue +95 -0
  22. package/app/components/container/UiPopover.vue +102 -0
  23. package/app/components/container/UiSampleDataOverlay.vue +47 -0
  24. package/app/components/container/UiSectionHeader.vue +83 -0
  25. package/app/components/container/UiSummaryStrip.vue +40 -0
  26. package/app/components/container/UiToast.vue +230 -0
  27. package/app/components/container/UiToaster.vue +271 -0
  28. package/app/components/container/UiWidgetState.vue +143 -0
  29. package/app/components/container/ui-nav-drawer.ts +48 -0
  30. package/app/components/data/ActionRail.vue +152 -0
  31. package/app/components/data/FacetStrip.vue +93 -0
  32. package/app/components/data/UiBarCell.vue +37 -0
  33. package/app/components/data/UiBarChart.vue +281 -0
  34. package/app/components/data/UiChartAnnotationControls.vue +74 -0
  35. package/app/components/data/UiChartAnnotations.vue +163 -0
  36. package/app/components/data/UiChartBrushOverlay.vue +96 -0
  37. package/app/components/data/UiChartDataReveal.vue +81 -0
  38. package/app/components/data/UiChartFrame.vue +123 -0
  39. package/app/components/data/UiChartTooltip.vue +81 -0
  40. package/app/components/data/UiCoverageNote.vue +90 -0
  41. package/app/components/data/UiCwvSparkline.vue +188 -0
  42. package/app/components/data/UiDataList.vue +276 -0
  43. package/app/components/data/UiDataTableSection.vue +232 -0
  44. package/app/components/data/UiDistributionBars.vue +164 -0
  45. package/app/components/data/UiDonutChart.vue +132 -0
  46. package/app/components/data/UiDoorwayColumn.vue +108 -0
  47. package/app/components/data/UiDoorwayGrid.vue +58 -0
  48. package/app/components/data/UiDoorwayRow.vue +83 -0
  49. package/app/components/data/UiEntitySummary.vue +261 -0
  50. package/app/components/data/UiFactsGrid.vue +107 -0
  51. package/app/components/data/UiImpactMeter.vue +33 -0
  52. package/app/components/data/UiIssueRow.vue +149 -0
  53. package/app/components/data/UiLineChart.vue +431 -0
  54. package/app/components/data/UiMetricBarPopover.vue +121 -0
  55. package/app/components/data/UiMetricLabel.vue +30 -0
  56. package/app/components/data/UiMetricStat.vue +73 -0
  57. package/app/components/data/UiMetricToggle.vue +156 -0
  58. package/app/components/data/UiMetricsRow.vue +64 -0
  59. package/app/components/data/UiPositionMetric.vue +53 -0
  60. package/app/components/data/UiRelatedFixes.vue +113 -0
  61. package/app/components/data/UiRichText.vue +325 -0
  62. package/app/components/data/UiScatterPlot.vue +144 -0
  63. package/app/components/data/UiSeverityMarker.vue +30 -0
  64. package/app/components/data/UiSparkColumns.vue +160 -0
  65. package/app/components/data/UiSparkline.vue +681 -0
  66. package/app/components/data/UiStat.vue +445 -0
  67. package/app/components/data/UiStats.vue +76 -0
  68. package/app/components/data/UiTable.vue +517 -0
  69. package/app/components/data/UiTableExport.vue +40 -0
  70. package/app/components/data/UiTableFrame.vue +149 -0
  71. package/app/components/data/UiTablePagination.vue +37 -0
  72. package/app/components/data/UiTableShell.vue +48 -0
  73. package/app/components/data/UiTableTd.vue +39 -0
  74. package/app/components/data/UiTableTh.vue +61 -0
  75. package/app/components/data/UiTopEntityStackChart.vue +393 -0
  76. package/app/components/data/cells/UiTableDash.vue +3 -0
  77. package/app/components/data/cells/UiTableHeaderCell.vue +72 -0
  78. package/app/components/data/cells/UiTableMetricCell.vue +72 -0
  79. package/app/components/data/cells/UiTablePathCell.vue +76 -0
  80. package/app/components/data/cells/UiTableScoreTile.vue +49 -0
  81. package/app/components/data/cells/UiTableTrendCell.vue +41 -0
  82. package/app/components/element/UiAtmosphere.vue +64 -0
  83. package/app/components/element/UiAuthHeading.vue +24 -0
  84. package/app/components/element/UiAuthProviders.vue +46 -0
  85. package/app/components/element/UiBrandIcon.vue +39 -0
  86. package/app/components/element/UiButton.vue +549 -0
  87. package/app/components/element/UiCelebration.vue +65 -0
  88. package/app/components/element/UiChannelChips.vue +62 -0
  89. package/app/components/element/UiChatQuickLink.vue +319 -0
  90. package/app/components/element/UiChip.vue +101 -0
  91. package/app/components/element/UiConfirmDialog.vue +84 -0
  92. package/app/components/element/UiCopyButton.vue +90 -0
  93. package/app/components/element/UiDitherAvatar.vue +177 -0
  94. package/app/components/element/UiDotLabel.vue +19 -0
  95. package/app/components/element/UiEjectMenu.vue +181 -0
  96. package/app/components/element/UiEjectOverlay.vue +29 -0
  97. package/app/components/element/UiFavicon.vue +235 -0
  98. package/app/components/element/UiFaviconStack.vue +84 -0
  99. package/app/components/element/UiFilterMenu.vue +324 -0
  100. package/app/components/element/UiFocusedSurface.vue +129 -0
  101. package/app/components/element/UiHealthDot.vue +32 -0
  102. package/app/components/element/UiHelpLabel.vue +28 -0
  103. package/app/components/element/UiIcon.vue +22 -0
  104. package/app/components/element/UiInput.vue +35 -0
  105. package/app/components/element/UiLogoPlausible.vue +12 -0
  106. package/app/components/element/UiLogoPro.vue +25 -0
  107. package/app/components/element/UiLogoUmami.vue +12 -0
  108. package/app/components/element/UiNavIcon.vue +107 -0
  109. package/app/components/element/UiNavList.vue +156 -0
  110. package/app/components/element/UiOnboardingAnswers.vue +125 -0
  111. package/app/components/element/UiOverflowCount.vue +31 -0
  112. package/app/components/element/UiProBadge.vue +19 -0
  113. package/app/components/element/UiProgressCircle.vue +79 -0
  114. package/app/components/element/UiProgressPercent.vue +35 -0
  115. package/app/components/element/UiRelativeTime.vue +58 -0
  116. package/app/components/element/UiScrollFade.vue +16 -0
  117. package/app/components/element/UiSelect.vue +55 -0
  118. package/app/components/element/UiSeverityDot.vue +27 -0
  119. package/app/components/element/UiSiteFavicon.vue +24 -0
  120. package/app/components/element/UiSkeleton.vue +148 -0
  121. package/app/components/element/UiSlotText.vue +65 -0
  122. package/app/components/element/UiSourceLogos.vue +113 -0
  123. package/app/components/element/UiSourceRail.vue +71 -0
  124. package/app/components/element/UiStaticTerminal.vue +107 -0
  125. package/app/components/element/UiStatusBadge.vue +62 -0
  126. package/app/components/element/UiSyncDot.vue +46 -0
  127. package/app/components/element/UiTabs.vue +245 -0
  128. package/app/components/element/UiTogglePill.vue +86 -0
  129. package/app/components/element/UiTooltip.vue +390 -0
  130. package/app/components/element/UiTooltipGrid.vue +457 -0
  131. package/app/components/element/UiTrend.vue +184 -0
  132. package/app/components/element/UiTrialBenefits.vue +71 -0
  133. package/app/components/element/UiUrlLabel.vue +75 -0
  134. package/app/components/element/UiValenceDot.vue +37 -0
  135. package/app/components/element/UiWizardModal.vue +284 -0
  136. package/app/components/element/UiWizardNav.vue +59 -0
  137. package/app/components/element/UiWizardProgress.vue +78 -0
  138. package/app/components/element/UiWizardShell.vue +43 -0
  139. package/app/components/element/eject-menu.ts +359 -0
  140. package/app/components/element/eject-tracker.ts +34 -0
  141. package/app/components/element/onboarding-answers.ts +68 -0
  142. package/app/components/element/onboarding-intents.ts +19 -0
  143. package/app/composables/dataVizColors.ts +350 -0
  144. package/app/composables/formatting.ts +252 -0
  145. package/app/composables/semanticColors.ts +80 -0
  146. package/app/composables/useActivityOverlay.ts +55 -0
  147. package/app/composables/useAppShellNav.ts +21 -0
  148. package/app/composables/useChartBrush.ts +221 -0
  149. package/app/composables/useChartHover.ts +153 -0
  150. package/app/composables/useChartTickPlan.ts +186 -0
  151. package/app/composables/useCloseOnNavigation.ts +7 -0
  152. package/app/composables/useConfirm.ts +38 -0
  153. package/app/composables/useEdgeFade.ts +37 -0
  154. package/app/composables/useEject.ts +69 -0
  155. package/app/composables/useEntranceMotion.ts +22 -0
  156. package/app/composables/useFaviconBacking.ts +164 -0
  157. package/app/composables/useGridCols.ts +39 -0
  158. package/app/composables/useIssueOverlay.ts +43 -0
  159. package/app/composables/useOnboardingService.ts +106 -0
  160. package/app/composables/useOpenedOnce.ts +19 -0
  161. package/app/composables/useScrollSpy.ts +178 -0
  162. package/app/composables/useSquircleFallback.ts +21 -0
  163. package/app/composables/useWizardRoute.ts +123 -0
  164. package/app/plugins/favicon.ts +29 -0
  165. package/app/plugins/lazy-motion.ts +22 -0
  166. package/app/plugins/selection-highlight.client.ts +176 -0
  167. package/app/shared/cli.ts +10 -0
  168. package/app/shared/error-state.ts +150 -0
  169. package/app/shared/icons.ts +1 -0
  170. package/app/shared/motion.ts +1 -0
  171. package/app/shared/nav.ts +38 -0
  172. package/app/shared/navigation.ts +5 -0
  173. package/app/shared/static-terminal.ts +14 -0
  174. package/app/shared/table-export.ts +36 -0
  175. package/app/shared/table.ts +105 -0
  176. package/app/shared/ui-icons.ts +1 -0
  177. package/app/shared/view-state.ts +85 -0
  178. package/app/shared/widget-failure.ts +126 -0
  179. package/app/utils/chartAnnotations.ts +326 -0
  180. package/app/utils/countries.ts +337 -0
  181. package/app/utils/export-download.ts +100 -0
  182. package/app/utils/favicon-fallback.ts +12 -0
  183. package/app/utils/icon-color.ts +105 -0
  184. package/app/utils/number.ts +74 -0
  185. package/app/utils/render-link.ts +14 -0
  186. package/app/utils/slot-text.ts +18 -0
  187. package/app/utils/squircle.ts +154 -0
  188. package/app/utils/topEntityStack.ts +197 -0
  189. package/app/utils/vue.ts +57 -0
  190. package/build/design-system.ts +47 -0
  191. package/build/icon-server-bundle.ts +276 -0
  192. package/build/runtime/icon-server-bundle.ts +29 -0
  193. package/css/global.css +1892 -0
  194. package/nuxt.config.ts +132 -0
  195. package/package.json +86 -3
  196. package/public/brand-icon-dark.png +0 -0
  197. package/public/brand-icon-light.png +0 -0
  198. package/public/favicon-dark.png +0 -0
  199. package/public/favicon-light.png +0 -0
  200. package/public/favicon.ico +0 -0
  201. package/public/fonts/README.md +31 -0
  202. package/public/fonts/fira-code-OFL.txt +93 -0
  203. package/public/fonts/fira-code-var-latin.woff2 +0 -0
  204. package/public/fonts/hubot-sans-OFL.txt +93 -0
  205. package/public/fonts/hubot-sans-var-latin.woff2 +0 -0
  206. package/public/icon-dark.png +0 -0
  207. package/public/icon.png +0 -0
  208. package/public/touch-icon-dark.png +0 -0
  209. package/public/touch-icon.png +0 -0
  210. package/server/api/favicon.get.ts +122 -0
  211. package/server/utils/favicon-source.ts +51 -0
  212. package/shared/contracts/feedback.ts +42 -0
  213. package/shared/contracts/index.ts +1 -0
  214. package/shared/delivery-channels.ts +53 -0
  215. package/shared/icons/index.ts +143 -0
  216. package/shared/icons/registry.ts +347 -0
  217. package/shared/icons/ui-slots.ts +58 -0
  218. package/shared/markdown.ts +44 -0
  219. package/shared/motion.ts +120 -0
  220. package/shared/rangi.ts +60 -0
  221. package/shared/rich-text.ts +34 -0
  222. package/shared/ui-icons.ts +6 -0
  223. package/shared/utils/urls.ts +64 -0
  224. package/types/index.ts +1 -0
  225. package/types/ui.ts +339 -0
@@ -0,0 +1,390 @@
1
+ <script setup lang="ts">
2
+ import type { VNodeChild } from 'vue'
3
+ import { computed, nextTick, onMounted, onUpdated, ref, shallowRef, useSlots, watch } from 'vue'
4
+ import { TooltipContent, TooltipPortal, TooltipProvider, TooltipRoot, TooltipTrigger, UiIcon } from '#components'
5
+ /**
6
+ * UiTooltip
7
+ *
8
+ * Built directly on reka-ui's Tooltip primitives. Skips Nuxt UI's `UTooltip`
9
+ * wrapper because that layer has historically been buggy in dense scenarios
10
+ * (heatmap cells, packed grids).
11
+ *
12
+ * The global `TooltipProvider` is set up once in `app/app.vue`
13
+ * (`<UApp :tooltip="{ delayDuration: 0 }">`) so adjacent tooltips coordinate
14
+ * via `skipDelayDuration` automatically.
15
+ *
16
+ * Defaults match tooltip semantics:
17
+ * - `disableHoverableContent: true` — the content layer cannot be hovered,
18
+ * eliminating the "safe-polygon" cursor traversal flicker between
19
+ * tightly-packed triggers.
20
+ * - `pointer-events: none` on the content node — defence in depth so the
21
+ * tooltip never steals events from underlying interactive content.
22
+ */
23
+
24
+ interface Props {
25
+ /** Plain text body. */
26
+ text?: string
27
+ /** Bold title rendered above description. */
28
+ title?: string
29
+ /** Secondary body text. */
30
+ description?: string
31
+ /** Raw HTML body — only rendered when title/description/text/#text are all absent. */
32
+ html?: string
33
+ /** Renders a label with a (?) trigger icon; tooltip mounts on the icon only. */
34
+ label?: string
35
+ size?: keyof typeof sizes
36
+ iconSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
37
+ side?: 'top' | 'bottom' | 'left' | 'right'
38
+ align?: 'start' | 'center' | 'end'
39
+ sideOffset?: number
40
+ /** Open delay (ms). Falls back to the provider default (0 in this app). */
41
+ delayDuration?: number
42
+ /** Whether the cursor may enter the tooltip body. Default `true` (tooltip semantics). */
43
+ disableHoverableContent?: boolean
44
+ disabled?: boolean
45
+ /**
46
+ * Default-variant trigger element. `'span'` (default) wraps the slot in a
47
+ * span. Keyboard focus on any focusable element inside opens the tooltip and
48
+ * describes that element; when the slot holds nothing focusable, the span
49
+ * itself becomes the Tab stop, so every tooltip stays keyboard-reachable.
50
+ * `'button'` wraps it in a focusable button so a non-interactive trigger
51
+ * (e.g. a bare info icon) stays keyboard- and screen-reader-reachable.
52
+ * `'child'` makes the slot's single root element the trigger directly — use
53
+ * it for full-width links/rows where an extra inline wrapper would alter
54
+ * layout or truncate the hit area.
55
+ */
56
+ triggerAs?: 'span' | 'button' | 'child'
57
+ }
58
+
59
+ const {
60
+ text,
61
+ title,
62
+ description,
63
+ html,
64
+ side = 'top',
65
+ align = 'center',
66
+ sideOffset = 6,
67
+ disableHoverableContent = true,
68
+ disabled = false,
69
+ size = 'md',
70
+ triggerAs = 'span',
71
+ } = defineProps<Props>()
72
+
73
+ defineSlots<{
74
+ default?: () => VNodeChild
75
+ text?: () => VNodeChild
76
+ }>()
77
+
78
+ const slots = useSlots()
79
+ const childTriggerElement = shallowRef<HTMLElement>()
80
+
81
+ // Reka's as-child ref can stop at a Vue component instance. NuxtLink and other
82
+ // component triggers then leave Floating UI with no measurable anchor, so the
83
+ // tooltip falls back to viewport 0,0. Event listeners still reach the rendered
84
+ // DOM root, which gives us the concrete anchor without adding a layout wrapper.
85
+ function captureChildTrigger(event: Event): void {
86
+ if (event.currentTarget instanceof HTMLElement)
87
+ childTriggerElement.value = event.currentTarget
88
+ }
89
+
90
+ // Span trigger. Reka listens for `focus` on the trigger itself, and focus does
91
+ // not bubble, so a focusable child (a button, a link, a tabindex cell) never
92
+ // opened the tooltip and the description landed on the span. Listen for
93
+ // `focusin` instead, open on keyboard focus, and move the description onto the
94
+ // focused element. A span with nothing focusable inside takes the Tab stop.
95
+ // A trigger inside an `aria-hidden` or `inert` subtree (a position-only plot
96
+ // whose data a table carries) never takes a Tab stop: focus there is unnamed.
97
+ const open = ref(false)
98
+ const spanTrigger = shallowRef<HTMLElement>()
99
+ const spanTabStop = ref(false)
100
+ const focusedChild = shallowRef<HTMLElement>()
101
+ const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), summary, [contenteditable="true"], [tabindex]:not([tabindex="-1"])'
102
+
103
+ function syncSpanTabStop(): void {
104
+ const span = spanTrigger.value
105
+ if (!span)
106
+ return
107
+ spanTabStop.value = !disabled && hasContent.value
108
+ && !span.querySelector(FOCUSABLE)
109
+ && !span.closest('[aria-hidden="true"], [inert]')
110
+ }
111
+ onMounted(syncSpanTabStop)
112
+ onUpdated(syncSpanTabStop)
113
+
114
+ function onSpanFocusIn(event: FocusEvent): void {
115
+ const target = event.target
116
+ if (!(target instanceof HTMLElement) || target === spanTrigger.value)
117
+ return
118
+ if (disabled || !hasContent.value || !target.matches(':focus-visible'))
119
+ return
120
+ focusedChild.value = target
121
+ open.value = true
122
+ }
123
+
124
+ function onSpanFocusOut(event: FocusEvent): void {
125
+ if (event.target === spanTrigger.value)
126
+ return
127
+ if (event.relatedTarget instanceof Node && spanTrigger.value?.contains(event.relatedTarget))
128
+ return
129
+ open.value = false
130
+ }
131
+
132
+ // Reka sets `aria-describedby` on the span while open; add it to the focused
133
+ // child, which is what a screen reader announces, and restore it on close.
134
+ let childDescribedBy: string | null = null
135
+ watch(open, async (isOpen) => {
136
+ const child = focusedChild.value
137
+ if (!child)
138
+ return
139
+ if (!isOpen) {
140
+ if (childDescribedBy)
141
+ child.setAttribute('aria-describedby', childDescribedBy)
142
+ else
143
+ child.removeAttribute('aria-describedby')
144
+ focusedChild.value = undefined
145
+ return
146
+ }
147
+ await nextTick()
148
+ const id = spanTrigger.value?.getAttribute('aria-describedby')
149
+ if (!id)
150
+ return
151
+ childDescribedBy = child.getAttribute('aria-describedby')
152
+ child.setAttribute('aria-describedby', childDescribedBy ? `${childDescribedBy} ${id}` : id)
153
+ })
154
+
155
+ const hasContent = computed(() =>
156
+ !!(text || title || description || html || slots.text),
157
+ )
158
+ const tooltipMaxWidth = computed(() =>
159
+ `min(${sizes[size]}px, calc(100vw - 2.5rem), var(--reka-tooltip-content-available-width, 100vw))`,
160
+ )
161
+ </script>
162
+
163
+ <script lang="ts">
164
+ export const sizes = {
165
+ xs: 80,
166
+ sm: 160,
167
+ md: 250,
168
+ lg: 440,
169
+ xl: 640,
170
+ }
171
+ </script>
172
+
173
+ <template>
174
+ <!-- Label variant: tooltip mounts on a small ? icon next to plain text -->
175
+ <span v-if="label" class="inline-flex items-center gap-1">
176
+ <span>{{ label }}</span>
177
+ <TooltipProvider :delay-duration="delayDuration ?? 0" :skip-delay-duration="300" :disable-hoverable-content="disableHoverableContent">
178
+ <TooltipRoot
179
+ :delay-duration="delayDuration"
180
+ :disable-hoverable-content="disableHoverableContent"
181
+ :disabled="disabled || !hasContent"
182
+ >
183
+ <TooltipTrigger
184
+ :aria-label="`More information: ${label}`"
185
+ class="-m-1.5 inline-flex min-h-6 min-w-6 items-center justify-center rounded-full text-dimmed hover:text-muted transition-colors cursor-help focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
186
+ >
187
+ <UiIcon name="life-buoy" class="size-3" aria-hidden="true" />
188
+ </TooltipTrigger>
189
+ <TooltipPortal>
190
+ <TooltipContent
191
+ :side="side"
192
+ :align="align"
193
+ :side-offset="sideOffset"
194
+ :collision-padding="8"
195
+ class="ui-tooltip-content"
196
+ role="tooltip"
197
+ >
198
+ <div class="w-max space-y-2 text-left text-xs font-normal leading-normal" :style="{ maxWidth: tooltipMaxWidth }" data-ui="UiTooltip">
199
+ <template v-if="title">
200
+ <div class="font-semibold">
201
+ {{ title }}
202
+ </div>
203
+ <div v-if="description" class="text-muted text-xs">
204
+ {{ description }}
205
+ </div>
206
+ </template>
207
+ <div v-else>
208
+ {{ text }}
209
+ </div>
210
+ </div>
211
+ </TooltipContent>
212
+ </TooltipPortal>
213
+ </TooltipRoot>
214
+ </TooltipProvider>
215
+ </span>
216
+
217
+ <!-- Default variant: tooltip wraps the trigger (slot, or fallback (?) icon) -->
218
+ <TooltipProvider
219
+ v-else
220
+ :delay-duration="delayDuration ?? 0"
221
+ :skip-delay-duration="300"
222
+ :disable-hoverable-content="disableHoverableContent"
223
+ >
224
+ <TooltipRoot
225
+ v-model:open="open"
226
+ :delay-duration="delayDuration"
227
+ :disable-hoverable-content="disableHoverableContent"
228
+ :disabled="disabled || !hasContent"
229
+ >
230
+ <TooltipTrigger
231
+ v-if="$slots.default && triggerAs === 'child'"
232
+ :reference="childTriggerElement"
233
+ as-child
234
+ @pointermove="captureChildTrigger"
235
+ @focus="captureChildTrigger"
236
+ >
237
+ <slot />
238
+ </TooltipTrigger>
239
+ <TooltipTrigger v-else as-child>
240
+ <!-- `max-w-full`: a trigger never outgrows its container. Without it a
241
+ truncating child (a long query in a table cell) set the span's
242
+ width and spilled over the next column at phone width. -->
243
+ <span
244
+ v-if="$slots.default && triggerAs === 'span'"
245
+ ref="spanTrigger"
246
+ class="inline-block max-w-full"
247
+ :class="spanTabStop ? 'rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary' : undefined"
248
+ :tabindex="spanTabStop ? 0 : undefined"
249
+ @focusin="onSpanFocusIn"
250
+ @focusout="onSpanFocusOut"
251
+ >
252
+ <slot />
253
+ </span>
254
+ <button
255
+ v-else-if="$slots.default"
256
+ type="button"
257
+ :aria-label="text || title || description"
258
+ class="-m-0.5 inline-flex min-h-6 min-w-6 items-center justify-center cursor-help rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
259
+ >
260
+ <slot />
261
+ </button>
262
+ <button
263
+ v-else
264
+ type="button"
265
+ aria-label="More information"
266
+ class="-m-0.5 inline-flex min-h-6 min-w-6 items-center justify-center cursor-help rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
267
+ >
268
+ <UiIcon name="life-buoy" class="text-primary" :class="({ xs: 'size-3', sm: 'size-3.5', md: 'size-4', lg: 'size-5', xl: 'size-6' })[iconSize || 'md']" aria-hidden="true" />
269
+ </button>
270
+ </TooltipTrigger>
271
+ <TooltipPortal>
272
+ <TooltipContent
273
+ :side="side"
274
+ :align="align"
275
+ :side-offset="sideOffset"
276
+ :collision-padding="8"
277
+ class="ui-tooltip-content"
278
+ role="tooltip"
279
+ >
280
+ <div class="w-max space-y-2 text-left text-xs font-normal leading-normal" :style="{ maxWidth: tooltipMaxWidth }" data-ui="UiTooltip">
281
+ <slot v-if="$slots.text" name="text" />
282
+ <template v-else-if="title">
283
+ <div class="font-semibold">
284
+ {{ title }}
285
+ </div>
286
+ <div v-if="description" class="text-muted text-xs">
287
+ {{ description }}
288
+ </div>
289
+ </template>
290
+ <div v-else-if="text">
291
+ {{ text }}
292
+ </div>
293
+ <div v-else-if="html" v-html="html" />
294
+ </div>
295
+ </TooltipContent>
296
+ </TooltipPortal>
297
+ </TooltipRoot>
298
+ </TooltipProvider>
299
+ </template>
300
+
301
+ <style>
302
+ .ui-tooltip-content {
303
+ background-color: var(--ui-bg-elevated);
304
+ /* Popover-tier depth — see global.css. --elevation-popover carries the
305
+ atmospheric shadow + brand bevel; --surface-raised is the top-lit fill. */
306
+ background-image: var(--surface-raised);
307
+ box-shadow: var(--elevation-popover);
308
+ color: var(--ui-text);
309
+ border: 1px solid var(--ui-border);
310
+ border-radius: 0.5rem;
311
+ padding: 0.625rem 0.75rem;
312
+ pointer-events: none;
313
+ z-index: 50;
314
+ will-change: transform, opacity;
315
+ transform-origin: var(--reka-tooltip-content-transform-origin, center);
316
+ letter-spacing: -0.005em;
317
+ -webkit-font-smoothing: antialiased;
318
+ }
319
+
320
+ /* Reka emits data-state + data-side; hook spring-out enter, soft exit. */
321
+ .ui-tooltip-content[data-state="delayed-open"],
322
+ .ui-tooltip-content[data-state="instant-open"] {
323
+ animation: ui-tooltip-in 140ms cubic-bezier(0.22, 1, 0.36, 1);
324
+ }
325
+
326
+ .ui-tooltip-content[data-state="closed"] {
327
+ animation: ui-tooltip-out 90ms ease-out;
328
+ }
329
+
330
+ @keyframes ui-tooltip-in {
331
+ from {
332
+ opacity: 0;
333
+ transform: scale(0.96) translateY(var(--tt-slide-y, 0)) translateX(var(--tt-slide-x, 0));
334
+ }
335
+ to {
336
+ opacity: 1;
337
+ transform: scale(1) translateY(0) translateX(0);
338
+ }
339
+ }
340
+
341
+ @keyframes ui-tooltip-out {
342
+ from {
343
+ opacity: 1;
344
+ transform: scale(1);
345
+ }
346
+ to {
347
+ opacity: 0;
348
+ transform: scale(0.97);
349
+ }
350
+ }
351
+
352
+ /* Slide-from-trigger directional accents */
353
+ .ui-tooltip-content[data-side="top"] { --tt-slide-y: 4px; }
354
+ .ui-tooltip-content[data-side="bottom"] { --tt-slide-y: -4px; }
355
+ .ui-tooltip-content[data-side="left"] { --tt-slide-x: 4px; }
356
+ .ui-tooltip-content[data-side="right"] { --tt-slide-x: -4px; }
357
+
358
+ @media (prefers-reduced-motion: reduce) {
359
+ .ui-tooltip-content[data-state="delayed-open"],
360
+ .ui-tooltip-content[data-state="instant-open"],
361
+ .ui-tooltip-content[data-state="closed"] {
362
+ animation: none;
363
+ }
364
+ }
365
+
366
+ [data-ui="UiTooltip"] {
367
+ p, ul {
368
+ &:not(:last-child) {
369
+ margin-bottom: 0.75rem;
370
+ }
371
+ }
372
+
373
+ ul {
374
+ padding-left: 1.5rem;
375
+ padding-right: 0.5rem;
376
+ list-style-type: disc;
377
+ }
378
+
379
+ li {
380
+ margin-bottom: 0.25rem;
381
+ list-style-type: disc;
382
+ }
383
+
384
+ code {
385
+ padding: 0.125rem 0.25rem;
386
+ border-radius: 0.375rem;
387
+ background-color: var(--ui-bg-accented);
388
+ }
389
+ }
390
+ </style>