@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.
- package/README.md +181 -2
- package/app/app.config.ts +592 -0
- package/app/component-attributes.d.ts +8 -0
- package/app/components/README.md +38 -0
- package/app/components/brand/UiBrandMark.vue +63 -0
- package/app/components/brand/UiModuleIcon.vue +45 -0
- package/app/components/brand/UiProLettering.vue +26 -0
- package/app/components/brand/UiRangeTerrain.vue +311 -0
- package/app/components/brand/range-glyphs.ts +18 -0
- package/app/components/container/BrowserTab.vue +145 -0
- package/app/components/container/IntegrationRow.vue +120 -0
- package/app/components/container/KnowDoRails.vue +40 -0
- package/app/components/container/RailPanel.vue +93 -0
- package/app/components/container/UiAlert.vue +120 -0
- package/app/components/container/UiAppShell.vue +165 -0
- package/app/components/container/UiCard.vue +204 -0
- package/app/components/container/UiDisclosure.vue +43 -0
- package/app/components/container/UiEmptyState.vue +102 -0
- package/app/components/container/UiErrorState.vue +61 -0
- package/app/components/container/UiLoadingState.vue +20 -0
- package/app/components/container/UiPageHeader.vue +95 -0
- package/app/components/container/UiPopover.vue +102 -0
- package/app/components/container/UiSampleDataOverlay.vue +47 -0
- package/app/components/container/UiSectionHeader.vue +83 -0
- package/app/components/container/UiSummaryStrip.vue +40 -0
- package/app/components/container/UiToast.vue +230 -0
- package/app/components/container/UiToaster.vue +271 -0
- package/app/components/container/UiWidgetState.vue +143 -0
- package/app/components/container/ui-nav-drawer.ts +48 -0
- package/app/components/data/ActionRail.vue +152 -0
- package/app/components/data/FacetStrip.vue +93 -0
- package/app/components/data/UiBarCell.vue +37 -0
- package/app/components/data/UiBarChart.vue +281 -0
- package/app/components/data/UiChartAnnotationControls.vue +74 -0
- package/app/components/data/UiChartAnnotations.vue +163 -0
- package/app/components/data/UiChartBrushOverlay.vue +96 -0
- package/app/components/data/UiChartDataReveal.vue +81 -0
- package/app/components/data/UiChartFrame.vue +123 -0
- package/app/components/data/UiChartTooltip.vue +81 -0
- package/app/components/data/UiCoverageNote.vue +90 -0
- package/app/components/data/UiCwvSparkline.vue +188 -0
- package/app/components/data/UiDataList.vue +276 -0
- package/app/components/data/UiDataTableSection.vue +233 -0
- package/app/components/data/UiDistributionBars.vue +164 -0
- package/app/components/data/UiDonutChart.vue +132 -0
- package/app/components/data/UiDoorwayColumn.vue +108 -0
- package/app/components/data/UiDoorwayGrid.vue +58 -0
- package/app/components/data/UiDoorwayRow.vue +83 -0
- package/app/components/data/UiEntitySummary.vue +261 -0
- package/app/components/data/UiFactsGrid.vue +107 -0
- package/app/components/data/UiImpactMeter.vue +33 -0
- package/app/components/data/UiIssueRow.vue +149 -0
- package/app/components/data/UiLineChart.vue +431 -0
- package/app/components/data/UiMetricBarPopover.vue +121 -0
- package/app/components/data/UiMetricLabel.vue +30 -0
- package/app/components/data/UiMetricStat.vue +73 -0
- package/app/components/data/UiMetricToggle.vue +156 -0
- package/app/components/data/UiMetricsRow.vue +64 -0
- package/app/components/data/UiPositionMetric.vue +53 -0
- package/app/components/data/UiRelatedFixes.vue +113 -0
- package/app/components/data/UiRichText.vue +325 -0
- package/app/components/data/UiScatterPlot.vue +144 -0
- package/app/components/data/UiSeverityMarker.vue +30 -0
- package/app/components/data/UiSparkColumns.vue +160 -0
- package/app/components/data/UiSparkline.vue +681 -0
- package/app/components/data/UiStat.vue +445 -0
- package/app/components/data/UiStats.vue +76 -0
- package/app/components/data/UiTable.vue +517 -0
- package/app/components/data/UiTableExport.vue +40 -0
- package/app/components/data/UiTableFrame.vue +149 -0
- package/app/components/data/UiTablePagination.vue +37 -0
- package/app/components/data/UiTableShell.vue +48 -0
- package/app/components/data/UiTableTd.vue +39 -0
- package/app/components/data/UiTableTh.vue +61 -0
- package/app/components/data/UiTopEntityStackChart.vue +393 -0
- package/app/components/data/cells/UiTableDash.vue +3 -0
- package/app/components/data/cells/UiTableHeaderCell.vue +72 -0
- package/app/components/data/cells/UiTableMetricCell.vue +72 -0
- package/app/components/data/cells/UiTablePathCell.vue +76 -0
- package/app/components/data/cells/UiTableScoreTile.vue +49 -0
- package/app/components/data/cells/UiTableTrendCell.vue +41 -0
- package/app/components/element/UiAtmosphere.vue +64 -0
- package/app/components/element/UiAuthHeading.vue +24 -0
- package/app/components/element/UiAuthProviders.vue +46 -0
- package/app/components/element/UiBrandIcon.vue +39 -0
- package/app/components/element/UiButton.vue +549 -0
- package/app/components/element/UiCelebration.vue +65 -0
- package/app/components/element/UiChannelChips.vue +62 -0
- package/app/components/element/UiChatQuickLink.vue +319 -0
- package/app/components/element/UiChip.vue +101 -0
- package/app/components/element/UiConfirmDialog.vue +84 -0
- package/app/components/element/UiCopyButton.vue +90 -0
- package/app/components/element/UiDitherAvatar.vue +177 -0
- package/app/components/element/UiDotLabel.vue +19 -0
- package/app/components/element/UiEjectMenu.vue +181 -0
- package/app/components/element/UiEjectOverlay.vue +29 -0
- package/app/components/element/UiFavicon.vue +235 -0
- package/app/components/element/UiFaviconStack.vue +84 -0
- package/app/components/element/UiFilterMenu.vue +324 -0
- package/app/components/element/UiFocusedSurface.vue +129 -0
- package/app/components/element/UiHealthDot.vue +32 -0
- package/app/components/element/UiHelpLabel.vue +28 -0
- package/app/components/element/UiIcon.vue +22 -0
- package/app/components/element/UiInput.vue +35 -0
- package/app/components/element/UiLogoPlausible.vue +12 -0
- package/app/components/element/UiLogoPro.vue +25 -0
- package/app/components/element/UiLogoUmami.vue +12 -0
- package/app/components/element/UiNavIcon.vue +107 -0
- package/app/components/element/UiNavList.vue +156 -0
- package/app/components/element/UiOnboardingAnswers.vue +127 -0
- package/app/components/element/UiOverflowCount.vue +31 -0
- package/app/components/element/UiProBadge.vue +19 -0
- package/app/components/element/UiProgressCircle.vue +79 -0
- package/app/components/element/UiProgressPercent.vue +35 -0
- package/app/components/element/UiRelativeTime.vue +58 -0
- package/app/components/element/UiScrollFade.vue +16 -0
- package/app/components/element/UiSelect.vue +68 -0
- package/app/components/element/UiSeverityDot.vue +27 -0
- package/app/components/element/UiSiteFavicon.vue +24 -0
- package/app/components/element/UiSkeleton.vue +148 -0
- package/app/components/element/UiSlotText.vue +65 -0
- package/app/components/element/UiSourceLogos.vue +113 -0
- package/app/components/element/UiSourceRail.vue +71 -0
- package/app/components/element/UiStaticTerminal.vue +107 -0
- package/app/components/element/UiStatusBadge.vue +62 -0
- package/app/components/element/UiSyncDot.vue +46 -0
- package/app/components/element/UiTabs.vue +245 -0
- package/app/components/element/UiTogglePill.vue +86 -0
- package/app/components/element/UiTooltip.vue +390 -0
- package/app/components/element/UiTooltipGrid.vue +457 -0
- package/app/components/element/UiTrend.vue +184 -0
- package/app/components/element/UiTrialBenefits.vue +71 -0
- package/app/components/element/UiUrlLabel.vue +75 -0
- package/app/components/element/UiValenceDot.vue +37 -0
- package/app/components/element/UiWizardModal.vue +284 -0
- package/app/components/element/UiWizardNav.vue +59 -0
- package/app/components/element/UiWizardProgress.vue +78 -0
- package/app/components/element/UiWizardShell.vue +43 -0
- package/app/components/element/eject-menu.ts +359 -0
- package/app/components/element/eject-tracker.ts +34 -0
- package/app/components/element/onboarding-answers.ts +93 -0
- package/app/components/element/onboarding-intents.ts +19 -0
- package/app/composables/dataVizColors.ts +350 -0
- package/app/composables/formatting.ts +252 -0
- package/app/composables/semanticColors.ts +80 -0
- package/app/composables/useActivityOverlay.ts +55 -0
- package/app/composables/useAppShellNav.ts +21 -0
- package/app/composables/useChartBrush.ts +221 -0
- package/app/composables/useChartHover.ts +153 -0
- package/app/composables/useChartTickPlan.ts +186 -0
- package/app/composables/useCloseOnNavigation.ts +7 -0
- package/app/composables/useConfirm.ts +38 -0
- package/app/composables/useEdgeFade.ts +37 -0
- package/app/composables/useEject.ts +69 -0
- package/app/composables/useEntranceMotion.ts +22 -0
- package/app/composables/useFaviconBacking.ts +164 -0
- package/app/composables/useGridCols.ts +39 -0
- package/app/composables/useIssueOverlay.ts +43 -0
- package/app/composables/useOnboardingService.ts +106 -0
- package/app/composables/useOpenedOnce.ts +19 -0
- package/app/composables/useScrollSpy.ts +178 -0
- package/app/composables/useSquircleFallback.ts +21 -0
- package/app/composables/useWizardRoute.ts +123 -0
- package/app/plugins/favicon.ts +15 -0
- package/app/plugins/lazy-motion.ts +22 -0
- package/app/plugins/selection-highlight.client.ts +176 -0
- package/app/shared/cli.ts +10 -0
- package/app/shared/error-state.ts +150 -0
- package/app/shared/icons.ts +1 -0
- package/app/shared/motion.ts +1 -0
- package/app/shared/nav.ts +38 -0
- package/app/shared/navigation.ts +5 -0
- package/app/shared/static-terminal.ts +14 -0
- package/app/shared/table-export.ts +36 -0
- package/app/shared/table.ts +105 -0
- package/app/shared/ui-icons.ts +1 -0
- package/app/shared/view-state.ts +85 -0
- package/app/shared/widget-failure.ts +126 -0
- package/app/utils/app-favicon.ts +64 -0
- package/app/utils/chartAnnotations.ts +326 -0
- package/app/utils/countries.ts +337 -0
- package/app/utils/export-download.ts +100 -0
- package/app/utils/favicon-fallback.ts +12 -0
- package/app/utils/icon-color.ts +105 -0
- package/app/utils/number.ts +74 -0
- package/app/utils/render-link.ts +14 -0
- package/app/utils/slot-text.ts +18 -0
- package/app/utils/squircle.ts +154 -0
- package/app/utils/topEntityStack.ts +197 -0
- package/app/utils/vue.ts +57 -0
- package/build/design-system.ts +69 -0
- package/build/external-links.ts +38 -0
- package/build/fonts.ts +39 -0
- package/build/icon-server-bundle.ts +276 -0
- package/build/runtime/icon-server-bundle.ts +29 -0
- package/css/global.css +1896 -0
- package/nuxt.config.ts +143 -0
- package/package.json +93 -3
- package/public/fonts/README.md +33 -0
- package/public/fonts/fira-code-OFL.txt +93 -0
- package/public/fonts/fira-code-var-latin.woff2 +0 -0
- package/public/fonts/hubot-sans-OFL.txt +93 -0
- package/public/fonts/hubot-sans-var-latin.woff2 +0 -0
- package/server/api/favicon.get.ts +122 -0
- package/server/utils/favicon-source.ts +51 -0
- package/shared/contracts/feedback.ts +42 -0
- package/shared/contracts/index.ts +1 -0
- package/shared/delivery-channels.ts +53 -0
- package/shared/icons/index.ts +143 -0
- package/shared/icons/registry.ts +347 -0
- package/shared/icons/ui-slots.ts +58 -0
- package/shared/markdown.ts +44 -0
- package/shared/motion.ts +120 -0
- package/shared/rangi.ts +60 -0
- package/shared/rich-text.ts +34 -0
- package/shared/ui-icons.ts +6 -0
- package/shared/utils/urls.ts +64 -0
- package/types/index.ts +1 -0
- 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>
|