@nuxtseo/design-system 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/README.md +181 -2
  2. package/app/app.config.ts +592 -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 +233 -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 +127 -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 +68 -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 +93 -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 +15 -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/app-favicon.ts +64 -0
  180. package/app/utils/chartAnnotations.ts +326 -0
  181. package/app/utils/countries.ts +337 -0
  182. package/app/utils/export-download.ts +100 -0
  183. package/app/utils/favicon-fallback.ts +12 -0
  184. package/app/utils/icon-color.ts +105 -0
  185. package/app/utils/number.ts +74 -0
  186. package/app/utils/render-link.ts +14 -0
  187. package/app/utils/slot-text.ts +18 -0
  188. package/app/utils/squircle.ts +154 -0
  189. package/app/utils/topEntityStack.ts +197 -0
  190. package/app/utils/vue.ts +57 -0
  191. package/build/design-system.ts +69 -0
  192. package/build/external-links.ts +38 -0
  193. package/build/fonts.ts +39 -0
  194. package/build/icon-server-bundle.ts +276 -0
  195. package/build/runtime/icon-server-bundle.ts +29 -0
  196. package/css/global.css +1896 -0
  197. package/nuxt.config.ts +143 -0
  198. package/package.json +93 -3
  199. package/public/fonts/README.md +33 -0
  200. package/public/fonts/fira-code-OFL.txt +93 -0
  201. package/public/fonts/fira-code-var-latin.woff2 +0 -0
  202. package/public/fonts/hubot-sans-OFL.txt +93 -0
  203. package/public/fonts/hubot-sans-var-latin.woff2 +0 -0
  204. package/server/api/favicon.get.ts +122 -0
  205. package/server/utils/favicon-source.ts +51 -0
  206. package/shared/contracts/feedback.ts +42 -0
  207. package/shared/contracts/index.ts +1 -0
  208. package/shared/delivery-channels.ts +53 -0
  209. package/shared/icons/index.ts +143 -0
  210. package/shared/icons/registry.ts +347 -0
  211. package/shared/icons/ui-slots.ts +58 -0
  212. package/shared/markdown.ts +44 -0
  213. package/shared/motion.ts +120 -0
  214. package/shared/rangi.ts +60 -0
  215. package/shared/rich-text.ts +34 -0
  216. package/shared/ui-icons.ts +6 -0
  217. package/shared/utils/urls.ts +64 -0
  218. package/types/index.ts +1 -0
  219. package/types/ui.ts +339 -0
@@ -0,0 +1,156 @@
1
+ <script setup lang="ts">
2
+ import type { VizColor } from '../../composables/dataVizColors'
3
+ import { ToggleGroupItem, ToggleGroupRoot } from 'reka-ui'
4
+ import { UiIcon, UiTooltip } from '#components'
5
+ import { metricIcon, vizBgColor, vizColorMap } from '../../composables/dataVizColors'
6
+
7
+ export interface MetricToggleOption {
8
+ key: string
9
+ label: string
10
+ color: string
11
+ icon?: string
12
+ /** What this metric means, shown on hover/focus. Icon-only toggles are unreadable without it. */
13
+ tooltip?: string
14
+ special?: boolean
15
+ }
16
+
17
+ const { options, iconOnly = false, chip = false, colors, compare = false } = defineProps<{
18
+ options: MetricToggleOption[]
19
+ /** Render icons only (no labels) in a tighter bordered segment. Label moves to a tooltip/aria. */
20
+ iconOnly?: boolean
21
+ /**
22
+ * Labelled chips that double as the chart legend. A selected chip takes its
23
+ * metric identity colour and a solid swatch; an unselected chip is neutral
24
+ * with its icon and label. At least one chip stays selected.
25
+ */
26
+ chip?: boolean
27
+ /**
28
+ * Metric identity map for `chip` (`gscMetricColors`, `analyticsMetricColors`,
29
+ * `cwvMetricColors`, `indexingVizColors`). A key outside the map is not a
30
+ * chart series: it selects neutrally and keeps its icon.
31
+ */
32
+ colors?: Record<string, VizColor>
33
+ /** A comparison period is drawn: selected chips add the dashed tail. */
34
+ compare?: boolean
35
+ }>()
36
+
37
+ const emit = defineEmits<{
38
+ toggle: [key: string]
39
+ }>()
40
+
41
+ const modelValue = defineModel<string[]>({ required: true })
42
+
43
+ function onUpdate(value: unknown) {
44
+ const next = Array.isArray(value) ? value.filter(v => typeof v === 'string') : []
45
+ // A chip row is the legend: an empty one would leave the chart with no series.
46
+ if (chip && next.length === 0)
47
+ return
48
+ // Surface the single key that flipped so consumers can react per-metric.
49
+ const changed = next.find(k => !modelValue.value.includes(k))
50
+ ?? modelValue.value.find(k => !next.includes(k))
51
+ if (changed)
52
+ emit('toggle', changed)
53
+ modelValue.value = next
54
+ }
55
+
56
+ function isActive(key: string) {
57
+ return modelValue.value.includes(key)
58
+ }
59
+
60
+ // One glyph per metric everywhere: the identity map wins over an option's own
61
+ // (often raw upstream) icon id.
62
+ function iconFor(opt: MetricToggleOption) {
63
+ return metricIcon(opt.key) ?? opt.icon
64
+ }
65
+
66
+ function identity(opt: MetricToggleOption): VizColor | undefined {
67
+ return colors ? colors[opt.key] : vizColorMap[opt.color]
68
+ }
69
+
70
+ function chipClass(opt: MetricToggleOption) {
71
+ if (!isActive(opt.key))
72
+ return 'border-default text-muted hover:text-default hover:border-accented'
73
+ return ['border-accented text-default', identity(opt) ? '' : 'bg-elevated']
74
+ }
75
+
76
+ // The chip is the chart's legend, so it paints from the same `hex` field the
77
+ // chart lines draw with. The Tailwind `dot`/`bg` classes resolve to a
78
+ // slightly different shade, which made a chip and its line disagree.
79
+ function swatchStyle(opt: MetricToggleOption) {
80
+ const id = identity(opt)
81
+ return id ? { backgroundColor: id.hex } : undefined
82
+ }
83
+ function chipStyle(opt: MetricToggleOption) {
84
+ const id = identity(opt)
85
+ return chip && id && isActive(opt.key) ? { backgroundColor: `color-mix(in oklab, ${id.hex} 15%, transparent)` } : undefined
86
+ }
87
+
88
+ function itemClass(opt: MetricToggleOption) {
89
+ if (iconOnly)
90
+ return ['justify-center size-11 sm:size-7 rounded-md', isActive(opt.key) ? 'bg-default text-default' : 'text-dimmed hover:text-default']
91
+ if (chip)
92
+ return ['shrink-0 gap-1.5 px-2.5 min-h-11 sm:min-h-0 sm:h-7 text-xs rounded-lg border whitespace-nowrap', chipClass(opt)]
93
+ return ['gap-1.5 px-2 py-1 min-h-11 sm:min-h-0 text-mini rounded-md border', isActive(opt.key) ? 'border-accented bg-elevated text-default' : 'border-transparent text-dimmed hover:text-muted hover:bg-accented']
94
+ }
95
+ </script>
96
+
97
+ <template>
98
+ <ToggleGroupRoot
99
+ :model-value="modelValue"
100
+ type="multiple"
101
+ :class="iconOnly ? 'flex items-center gap-0.5 p-0.5 rounded-lg bg-[var(--ui-bg-elevated)]/60 border border-default' : (chip ? 'flex items-center gap-1.5' : 'flex gap-1')"
102
+ @update:model-value="onUpdate"
103
+ >
104
+ <!-- `triggerAs="child"` keeps the toggle itself the trigger — an extra
105
+ wrapper span would break the segment's flex row and the hit area.
106
+ A chip already shows its label, so it only tooltips a description. -->
107
+ <UiTooltip
108
+ v-for="opt in options"
109
+ :key="opt.key"
110
+ :title="opt.label"
111
+ :description="opt.tooltip"
112
+ :text="opt.tooltip ? undefined : opt.label"
113
+ :disabled="chip && !opt.tooltip"
114
+ trigger-as="child"
115
+ size="sm"
116
+ >
117
+ <ToggleGroupItem
118
+ :value="opt.key"
119
+ class="cursor-pointer inline-flex items-center font-medium transition-colors duration-150 outline-none focus-visible:ring-2 focus-visible:ring-primary"
120
+ :class="[itemClass(opt), opt.special && !isActive(opt.key) && 'ring-1 ring-[var(--ui-primary)]/50']"
121
+ :style="chipStyle(opt)"
122
+ :aria-label="`Toggle ${opt.label}`"
123
+ >
124
+ <!-- Chip legend swatch: the series stroke, plus the dashed previous
125
+ period tail while a comparison is drawn. -->
126
+ <span
127
+ v-if="chip && isActive(opt.key) && identity(opt)"
128
+ class="flex items-center gap-0.5 shrink-0"
129
+ aria-hidden="true"
130
+ >
131
+ <span class="h-0.5 w-2.5 rounded-full" :style="swatchStyle(opt)" />
132
+ <template v-if="compare">
133
+ <span class="h-0.5 w-1 rounded-full opacity-60" :style="swatchStyle(opt)" />
134
+ <span class="h-0.5 w-1 rounded-full opacity-60" :style="swatchStyle(opt)" />
135
+ </template>
136
+ </span>
137
+ <!-- Consumer-rendered glyph (e.g. `UiSeverityMarker`) for options whose
138
+ identity isn't a named icon or a metric colour — falls back to the
139
+ built-in icon/dot so every existing caller is unaffected. -->
140
+ <slot v-else name="icon" :option="opt" :active="isActive(opt.key)">
141
+ <UiIcon v-if="iconFor(opt)" :name="iconFor(opt)!" class="size-3.5 shrink-0" aria-hidden="true" />
142
+ <span
143
+ v-else
144
+ class="size-1.5 rounded-full shrink-0 transition-colors duration-150"
145
+ :class="isActive(opt.key) ? vizBgColor(opt.color) : 'bg-muted'"
146
+ />
147
+ </slot>
148
+ <template v-if="!iconOnly">
149
+ <slot name="label" :option="opt" :active="isActive(opt.key)">
150
+ {{ opt.label }}
151
+ </slot>
152
+ </template>
153
+ </ToggleGroupItem>
154
+ </UiTooltip>
155
+ </ToggleGroupRoot>
156
+ </template>
@@ -0,0 +1,64 @@
1
+ <script setup lang="ts">
2
+ import { UiIcon, UiTrend } from '#components'
3
+
4
+ interface MetricsRowItem {
5
+ /** Preferred: Lucide icon name. Falls back to dotColor if not provided. */
6
+ icon?: string
7
+ /** Text color class applied to the icon. Prefer semantic or metric identity tokens. */
8
+ iconClass?: string
9
+ /** Legacy dot color class (used when no icon is provided). */
10
+ dotColor?: string
11
+ value: string
12
+ label: string
13
+ trend?: number | null
14
+ trendInverted?: boolean
15
+ trendFormat?: 'percent' | 'points'
16
+ }
17
+
18
+ defineProps<{
19
+ items: MetricsRowItem[]
20
+ }>()
21
+ </script>
22
+
23
+ <template>
24
+ <div data-testid="metrics-row" class="flex items-center gap-3 sm:gap-4 mb-6 flex-wrap">
25
+ <template v-for="(item, i) in items" :key="item.label">
26
+ <div v-if="i > 0" class="w-px self-stretch bg-[var(--ui-border)] hidden sm:block" />
27
+ <div class="flex items-center gap-2 sm:gap-3 leading-none tracking-data">
28
+ <UiIcon
29
+ v-if="item.icon"
30
+ :name="item.icon"
31
+ class="size-4 shrink-0"
32
+ :class="item.iconClass ?? 'text-dimmed'"
33
+ aria-hidden="true"
34
+ />
35
+ <span class="text-2xl sm:text-4xl font-bold tracking-tight tabular-nums">
36
+ {{ item.value }}
37
+ </span>
38
+ <div class="flex flex-col">
39
+ <span class="text-xs text-muted">
40
+ {{ item.label }}
41
+ </span>
42
+ <Transition
43
+ enter-active-class="motion-safe:transition-[opacity,transform] motion-safe:duration-200 ease-out"
44
+ enter-from-class="motion-safe:opacity-0 motion-safe:translate-y-0.5"
45
+ enter-to-class="opacity-100 translate-y-0"
46
+ leave-active-class="motion-safe:transition-[opacity,transform] motion-safe:duration-150 ease-in"
47
+ leave-from-class="opacity-100"
48
+ leave-to-class="motion-safe:opacity-0"
49
+ >
50
+ <UiTrend
51
+ v-if="item.trend != null && item.trend !== 0"
52
+ :value="item.trend"
53
+ :inverted="item.trendInverted"
54
+ colored
55
+ :format="item.trendFormat ?? 'percent'"
56
+ size="sm"
57
+ />
58
+ <span v-else class="text-xs text-dimmed tabular-nums" aria-label="No comparison data">—</span>
59
+ </Transition>
60
+ </div>
61
+ </div>
62
+ </template>
63
+ </div>
64
+ </template>
@@ -0,0 +1,53 @@
1
+ <script lang="ts" setup>
2
+ import { computed } from 'vue'
3
+ import { UBadge, UiTooltip } from '#components'
4
+
5
+ const { value } = defineProps<{ value: number | string, size?: string }>()
6
+
7
+ const val = Number(value)
8
+
9
+ const color = computed(() => {
10
+ if (val < 10)
11
+ return 'success'
12
+ if (val < 20)
13
+ return 'warning'
14
+ if (val < 30)
15
+ return 'warning'
16
+ return 'error'
17
+ })
18
+
19
+ // Text equivalent of the color signal so position quality isn't conveyed by
20
+ // hue alone (WCAG 1.4.1) — voiced via the badge's aria-label.
21
+ const qualitative = computed(() => {
22
+ if (val < 10)
23
+ return 'strong'
24
+ if (val < 30)
25
+ return 'moderate'
26
+ return 'weak'
27
+ })
28
+
29
+ const formattedValue = computed(() => Math.round(val))
30
+ </script>
31
+
32
+ <template>
33
+ <UiTooltip trigger-as="button">
34
+ <UBadge
35
+ :color="color"
36
+ variant="subtle"
37
+ class="font-mono tabular-nums"
38
+ :aria-label="`Average position ${val.toFixed(1)}, ${qualitative}`"
39
+ >
40
+ {{ formattedValue }}
41
+ </UBadge>
42
+ <template #text>
43
+ <div class="max-w-xs text-xs">
44
+ <p class="font-medium mb-1">
45
+ Avg Position: {{ val.toFixed(1) }}
46
+ </p>
47
+ <p class="text-muted">
48
+ Weighted average across all impressions (countries, devices, personalization). May differ from single SERP checks.
49
+ </p>
50
+ </div>
51
+ </template>
52
+ </UiTooltip>
53
+ </template>
@@ -0,0 +1,113 @@
1
+ <script setup lang="ts">
2
+ import type { RouteLocationRaw } from 'vue-router'
3
+ import type { RichSegment } from '../../../shared/rich-text'
4
+
5
+ /**
6
+ * UiRelatedFixes — the related-work strip (ADR-0059 #4): a feature page's slice of
7
+ * open Issues & Opportunities work, rendered beside the data that grounds it.
8
+ *
9
+ * Purely presentational (ADR-0029): rows arrive via props (feature layers
10
+ * query pro-actions's catalog themselves — ADR-0042 lets them consume contracts
11
+ * and queries, never runtime components, so the shared piece lives here).
12
+ * Rows render the shared `UiIssueRow` (compact) so the strip and the board read
13
+ * as one row primitive at two densities.
14
+ *
15
+ * Render contract (deliberate, from the first adopter):
16
+ * - nothing until `loaded` — a skeleton would over-promise a section that is
17
+ * often (healthily) empty;
18
+ * - when loaded + empty: the quiet `emptyLabel` line if given, else nothing.
19
+ *
20
+ * Click contract (Q1, 2026-06): a row opens the act-surface detail in place via
21
+ * `@select` — the owning layer holds the full action and opens the modal, so the
22
+ * user acts (Start / Work on it / claim) without a page hop. Only the header /
23
+ * footer "View all" navigates to the full Issues & Opportunities board.
24
+ */
25
+
26
+ import { NuxtLink, UiButton, UiIcon, UiIssueRow } from '#components'
27
+
28
+ export interface RelatedFixRow {
29
+ id: string
30
+ title: string
31
+ /** Typed headline tokens (`actionDisplay().headline`); falls back to `title` when omitted. */
32
+ richSegments?: RichSegment[]
33
+ evidence?: string | null
34
+ /** The board's deterministic severity — drives the trailing dot. */
35
+ severity: 'error' | 'warning' | 'info'
36
+ }
37
+
38
+ const {
39
+ rows,
40
+ to,
41
+ loaded = false,
42
+ title = 'Related Work',
43
+ emptyLabel,
44
+ moreCount = 0,
45
+ showHeader = true,
46
+ } = defineProps<{
47
+ rows: RelatedFixRow[]
48
+ /** The Issues & Opportunities page (optionally ?cluster=-filtered). */
49
+ to: RouteLocationRaw
50
+ loaded?: boolean
51
+ title?: string
52
+ /** Quiet all-clear line when there is no open work; omit to render nothing. */
53
+ emptyLabel?: string
54
+ /** Matches beyond the rendered rows — folds into a "+N more" footer link. */
55
+ moreCount?: number
56
+ /** False when another primitive already owns the panel header/action. */
57
+ showHeader?: boolean
58
+ }>()
59
+
60
+ const emit = defineEmits<{
61
+ /** Row clicked — the owning layer opens the in-place act-surface modal. */
62
+ select: [row: RelatedFixRow]
63
+ }>()
64
+ </script>
65
+
66
+ <template>
67
+ <section v-if="loaded && (rows.length || emptyLabel)">
68
+ <div v-if="showHeader" class="mb-2 flex items-baseline justify-between">
69
+ <h3 class="text-sm font-semibold text-highlighted">
70
+ {{ title }}
71
+ </h3>
72
+ <UiButton
73
+ v-if="rows.length"
74
+ label="View all"
75
+ size="xs"
76
+ purpose="quiet"
77
+ trailing-icon="next"
78
+ :to="to"
79
+ />
80
+ </div>
81
+ <ul v-if="rows.length" class="divide-y divide-default overflow-hidden rounded-lg border border-default">
82
+ <li v-for="row in rows" :key="row.id">
83
+ <button
84
+ type="button"
85
+ class="block w-full cursor-pointer text-left outline-none focus-visible:ring-2 focus-visible:ring-primary"
86
+ @click="emit('select', row)"
87
+ >
88
+ <UiIssueRow
89
+ density="compact"
90
+ :title="row.title"
91
+ :rich-segments="row.richSegments"
92
+ :evidence="row.evidence"
93
+ :severity="row.severity"
94
+ interactive
95
+ />
96
+ </button>
97
+ </li>
98
+ <li v-if="moreCount > 0">
99
+ <NuxtLink
100
+ :to="to"
101
+ class="flex min-h-11 items-center gap-2 py-2 pl-4 pr-3.5 text-sm text-muted transition-colors hover:bg-elevated/60"
102
+ >
103
+ +{{ moreCount }} more
104
+ <UiIcon name="chevron-right" class="size-3 shrink-0 text-dimmed" aria-hidden="true" />
105
+ </NuxtLink>
106
+ </li>
107
+ </ul>
108
+ <p v-else class="flex items-center gap-1.5 text-xs text-muted">
109
+ <UiIcon name="check" class="size-3.5 shrink-0 text-dimmed" aria-hidden="true" />
110
+ {{ emptyLabel }}
111
+ </p>
112
+ </section>
113
+ </template>