@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.
- package/README.md +101 -2
- package/app/app.config.ts +596 -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 +232 -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 +125 -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 +55 -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 +68 -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 +29 -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/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 +47 -0
- package/build/icon-server-bundle.ts +276 -0
- package/build/runtime/icon-server-bundle.ts +29 -0
- package/css/global.css +1892 -0
- package/nuxt.config.ts +132 -0
- package/package.json +86 -3
- package/public/brand-icon-dark.png +0 -0
- package/public/brand-icon-light.png +0 -0
- package/public/favicon-dark.png +0 -0
- package/public/favicon-light.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/fonts/README.md +31 -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/public/icon-dark.png +0 -0
- package/public/icon.png +0 -0
- package/public/touch-icon-dark.png +0 -0
- package/public/touch-icon.png +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,431 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export interface LineTooltipRow {
|
|
3
|
+
color: string
|
|
4
|
+
label: string
|
|
5
|
+
value: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function sharedLineDomain(
|
|
9
|
+
data: Array<Record<string, unknown>>,
|
|
10
|
+
seriesKeys: string[],
|
|
11
|
+
minDomainMaximum = 1,
|
|
12
|
+
): [number, number] {
|
|
13
|
+
const values = data.flatMap(row => seriesKeys.map((key) => {
|
|
14
|
+
const raw = row[key]
|
|
15
|
+
const value = typeof raw === 'number' ? raw : Number(raw)
|
|
16
|
+
return Number.isFinite(value) ? value : 0
|
|
17
|
+
}))
|
|
18
|
+
// Non-negative data keeps its zero baseline. A signed series (a
|
|
19
|
+
// desktop-minus-mobile gap) follows its values below zero; clipped to the
|
|
20
|
+
// floor, "mobile leads" drew the same line as "no gap".
|
|
21
|
+
const lower = Math.min(0, ...values)
|
|
22
|
+
const upper = Math.max(0, minDomainMaximum, ...values)
|
|
23
|
+
return [lower * 1.1, (lower === upper ? 1 : upper) * 1.1]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function lineTickIndices(length: number, width: number): number[] {
|
|
27
|
+
if (length <= 1)
|
|
28
|
+
return [0]
|
|
29
|
+
const widthBudget = width > 0 ? Math.max(2, Math.floor(width / 96)) : 6
|
|
30
|
+
const count = Math.min(6, widthBudget, length)
|
|
31
|
+
const step = (length - 1) / (count - 1)
|
|
32
|
+
return Array.from({ length: count }, (_, i) => Math.round(i * step))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function escapeChartTooltipHtml(value: unknown): string {
|
|
36
|
+
return String(value)
|
|
37
|
+
.replaceAll('&', '&')
|
|
38
|
+
.replaceAll('<', '<')
|
|
39
|
+
.replaceAll('>', '>')
|
|
40
|
+
.replaceAll('"', '"')
|
|
41
|
+
.replaceAll('\'', ''')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function renderLineTooltip(header: string, rows: LineTooltipRow[]): string {
|
|
45
|
+
const renderedRows = rows.map(row => `
|
|
46
|
+
<div style="display:flex;gap:12px;justify-content:space-between;align-items:center">
|
|
47
|
+
<span style="display:flex;align-items:center;gap:6px;color:var(--ui-text-muted)">
|
|
48
|
+
<span style="width:8px;height:8px;border-radius:9999px;background:${escapeChartTooltipHtml(row.color)}"></span>${escapeChartTooltipHtml(row.label)}
|
|
49
|
+
</span>
|
|
50
|
+
<span style="font-variant-numeric:tabular-nums;color:var(--ui-text-highlighted)">${escapeChartTooltipHtml(row.value)}</span>
|
|
51
|
+
</div>`).join('')
|
|
52
|
+
return `<div style="display:flex;flex-direction:column;gap:4px;min-width:140px">
|
|
53
|
+
<div style="font-size:14px;color:var(--ui-text-muted);margin-bottom:2px">${escapeChartTooltipHtml(header)}</div>${renderedRows}
|
|
54
|
+
</div>`
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<script lang="ts" setup generic="T extends Record<string, unknown>">
|
|
59
|
+
import type { ChartAnnotation, ChartAnnotationOptions } from '../../utils/chartAnnotations'
|
|
60
|
+
import { TextAlign } from '@unovis/ts'
|
|
61
|
+
import { VisArea, VisAxis, VisCrosshair, VisLine, VisScatter, VisTooltip, VisXYContainer } from '@unovis/vue'
|
|
62
|
+
import { useElementSize } from '@vueuse/core'
|
|
63
|
+
import { computed, ref, useTemplateRef } from 'vue'
|
|
64
|
+
import { ClientOnly, UiChartAnnotations, UiChartDataReveal } from '#components'
|
|
65
|
+
import { formatGroupedNumber } from '../../composables/formatting'
|
|
66
|
+
import { indexAnnotationsByDay } from '../../utils/chartAnnotations'
|
|
67
|
+
|
|
68
|
+
// Generic interactive line chart. Every series shares one zero-based y scale:
|
|
69
|
+
// callers may compare same-unit series here, while different units belong in
|
|
70
|
+
// separate charts. Crosshair + tooltip make the exact rows available on hover.
|
|
71
|
+
|
|
72
|
+
interface LineSeries {
|
|
73
|
+
/** Row key holding this series' numeric value. */
|
|
74
|
+
key: string
|
|
75
|
+
/** Legend/tooltip label. Defaults to `key`. */
|
|
76
|
+
label?: string
|
|
77
|
+
/** Stroke colour (any CSS colour). Defaults to a rotating palette. */
|
|
78
|
+
color?: string
|
|
79
|
+
/** Draw a translucent area under the line. Default true. */
|
|
80
|
+
area?: boolean
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const {
|
|
84
|
+
data,
|
|
85
|
+
xKey,
|
|
86
|
+
series,
|
|
87
|
+
height = 220,
|
|
88
|
+
selectedX,
|
|
89
|
+
settledThrough,
|
|
90
|
+
xFormat,
|
|
91
|
+
yFormat,
|
|
92
|
+
minDomainMaximum = 1,
|
|
93
|
+
annotations,
|
|
94
|
+
annotationOptions,
|
|
95
|
+
} = defineProps<{
|
|
96
|
+
data: T[]
|
|
97
|
+
/** Row key for the x dimension (e.g. a date column). */
|
|
98
|
+
xKey: keyof T & string
|
|
99
|
+
series: LineSeries[]
|
|
100
|
+
height?: number | string
|
|
101
|
+
/** Highlight one x value, for example the selected crawl generation. */
|
|
102
|
+
selectedX?: T[keyof T] | null
|
|
103
|
+
/**
|
|
104
|
+
* The last x value whose data is FINAL. Everything after it is still
|
|
105
|
+
* settling — a partial sum that will keep rising — and renders dashed with
|
|
106
|
+
* no area fill instead of as a finished point.
|
|
107
|
+
*
|
|
108
|
+
* Search Console lands two to three days behind, so without this every
|
|
109
|
+
* clicks chart ends in a cliff that reads as a traffic collapse. Omit when
|
|
110
|
+
* the whole window is settled.
|
|
111
|
+
*/
|
|
112
|
+
settledThrough?: T[keyof T] | null
|
|
113
|
+
/** Format an x value for axis ticks + the tooltip header. */
|
|
114
|
+
xFormat?: (raw: T[keyof T]) => string
|
|
115
|
+
/** Format a y value for the tooltip, per series key. */
|
|
116
|
+
yFormat?: (value: number, seriesKey: string) => string
|
|
117
|
+
/**
|
|
118
|
+
* The lowest top the y scale may have. The default 1 keeps a count chart of
|
|
119
|
+
* zeros from collapsing. Pass 0 for a small rate (a CTR of 0.02), which the
|
|
120
|
+
* default would flatten against the floor.
|
|
121
|
+
*/
|
|
122
|
+
minDomainMaximum?: number
|
|
123
|
+
/**
|
|
124
|
+
* Day-anchored event annotations (thin marker line + dot). Re-anchored onto
|
|
125
|
+
* this chart's index x-scale by matching each annotation's day against the
|
|
126
|
+
* row's `xKey` value — rows whose x isn't a date simply match nothing, so
|
|
127
|
+
* non-date charts stay marker-free without opting out.
|
|
128
|
+
*/
|
|
129
|
+
annotations?: ChartAnnotation[]
|
|
130
|
+
/** Marker presentation shared by single and stacked annotation popovers. */
|
|
131
|
+
annotationOptions?: ChartAnnotationOptions
|
|
132
|
+
}>()
|
|
133
|
+
|
|
134
|
+
const emit = defineEmits<{
|
|
135
|
+
annotationEdit: [annotation: ChartAnnotation]
|
|
136
|
+
}>()
|
|
137
|
+
|
|
138
|
+
const annotationInteracting = ref(false)
|
|
139
|
+
|
|
140
|
+
// Semantic chart colours keep default series theme-aware. Callers can still
|
|
141
|
+
// provide a more specific colour when the metric has an established meaning.
|
|
142
|
+
const PALETTE = [
|
|
143
|
+
'var(--ui-color-primary-500)',
|
|
144
|
+
'var(--ui-color-success-500)',
|
|
145
|
+
'var(--ui-color-warning-500)',
|
|
146
|
+
'var(--ui-color-error-500)',
|
|
147
|
+
'var(--ui-color-info-500)',
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
const resolvedSeries = computed(() => series.map((s, i) => ({
|
|
151
|
+
key: s.key,
|
|
152
|
+
label: s.label ?? s.key,
|
|
153
|
+
color: s.color ?? PALETTE[i % PALETTE.length]!,
|
|
154
|
+
area: s.area ?? true,
|
|
155
|
+
})))
|
|
156
|
+
|
|
157
|
+
// No horizontal inset — the line bleeds edge-to-edge (matches ProGraphGsc).
|
|
158
|
+
// Edge tick labels (tickIndices always includes the first and last point) are
|
|
159
|
+
// kept inside via the text-anchor overrides in <style> below.
|
|
160
|
+
const margin = { left: 0, right: 0, top: 8, bottom: 28 }
|
|
161
|
+
const chartHeight = computed(() => Number(height) || 220)
|
|
162
|
+
const chartRoot = useTemplateRef<HTMLElement>('chartRoot')
|
|
163
|
+
const { width: containerWidth } = useElementSize(chartRoot)
|
|
164
|
+
// Unovis measures its parent once when `width` is omitted. Feed the observed
|
|
165
|
+
// container width back as a prop so desktop-to-mobile resizes re-render every
|
|
166
|
+
// point and the selected trailing crawl cannot remain clipped off-canvas.
|
|
167
|
+
const measuredWidth = computed(() => Math.max(0, Math.floor(containerWidth.value)))
|
|
168
|
+
|
|
169
|
+
// A missing point (null, undefined, or not a number) is `undefined`, never 0:
|
|
170
|
+
// Unovis leaves a gap for it, and the tooltip and the table print a dash. Read
|
|
171
|
+
// as 0, a sparse series drew a dip to zero and announced "0" for that day.
|
|
172
|
+
function num(v: unknown): number | undefined {
|
|
173
|
+
if (v == null || v === '')
|
|
174
|
+
return undefined
|
|
175
|
+
const n = typeof v === 'number' ? v : Number(v)
|
|
176
|
+
return Number.isFinite(n) ? n : undefined
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// x accessor is the row index — keeps spacing even regardless of x type (dates, labels, …).
|
|
180
|
+
const x = (_d: T, i: number) => i
|
|
181
|
+
// Crosshair can render before its container inherits the line accessors.
|
|
182
|
+
// Give it the complete series so snapping also works during data changes.
|
|
183
|
+
const crosshairY = computed(() => resolvedSeries.value.map(s => (d: T) => num(d[s.key])))
|
|
184
|
+
|
|
185
|
+
const yDomain = computed<[number, number]>(() => {
|
|
186
|
+
return sharedLineDomain(data, resolvedSeries.value.map(series => series.key), minDomainMaximum)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
function selectedSize(d: T): number {
|
|
190
|
+
return selectedX != null && Object.is(d[xKey], selectedX) ? 8 : 0
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Index of the last settled row. `null` (the default, and an x value that
|
|
194
|
+
// isn't in `data`) means the whole window is final and the chart draws exactly
|
|
195
|
+
// as it did before this prop existed.
|
|
196
|
+
const settledIndex = computed<number | null>(() => {
|
|
197
|
+
if (settledThrough == null)
|
|
198
|
+
return null
|
|
199
|
+
const i = data.findIndex(row => Object.is(row[xKey], settledThrough))
|
|
200
|
+
return i === -1 ? null : i
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// The two spans overlap by one point on purpose: the dashed run starts at the
|
|
204
|
+
// last settled row so the line stays visually continuous across the boundary.
|
|
205
|
+
function settledY(d: T, i: number, key: string): number | undefined {
|
|
206
|
+
const cut = settledIndex.value
|
|
207
|
+
return cut != null && i > cut ? undefined : num(d[key])
|
|
208
|
+
}
|
|
209
|
+
function provisionalY(d: T, i: number, key: string): number | undefined {
|
|
210
|
+
const cut = settledIndex.value
|
|
211
|
+
return cut == null || i < cut ? undefined : num(d[key])
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Evenly-spaced tick indices (~6), formatted from the row at that index. Index-based rather
|
|
215
|
+
// than calendar-aware so non-date x values work too.
|
|
216
|
+
const tickIndices = computed(() => {
|
|
217
|
+
return lineTickIndices(data.length, measuredWidth.value)
|
|
218
|
+
})
|
|
219
|
+
function tickFormat(i: number): string {
|
|
220
|
+
const row = data[i]
|
|
221
|
+
if (!row)
|
|
222
|
+
return ''
|
|
223
|
+
return xFormat ? xFormat(row[xKey]) : String(row[xKey])
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Day-anchored annotations onto the index x-scale (unmatched days drop out).
|
|
227
|
+
const anchoredAnnotations = computed(() =>
|
|
228
|
+
indexAnnotationsByDay(annotations, data.map(row => String(row[xKey]))))
|
|
229
|
+
const annotationXDomain = computed<[number, number] | undefined>(() =>
|
|
230
|
+
data.length > 1 ? [0, data.length - 1] : undefined)
|
|
231
|
+
|
|
232
|
+
// Force a remount when the dataset identity changes so unovis recomputes scales cleanly.
|
|
233
|
+
const chartKey = computed(() => `${data.length}-${String(data[0]?.[xKey] ?? '')}-${String(data.at(-1)?.[xKey] ?? '')}`)
|
|
234
|
+
|
|
235
|
+
function fmtY(value: number | undefined, key: string): string {
|
|
236
|
+
if (value === undefined)
|
|
237
|
+
return '—'
|
|
238
|
+
return yFormat ? yFormat(value, key) : formatGroupedNumber(value)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// The drawn lines encode data by position only, so the SVG layers are hidden
|
|
242
|
+
// from assistive tech; a visually-hidden table below carries the same series
|
|
243
|
+
// values as text (mirrors UiScatterPlot). Rendered server-side too (no client
|
|
244
|
+
// deps), so the data is present before hydration.
|
|
245
|
+
const ariaLabel = computed(() => `Line chart of ${resolvedSeries.value.map(s => s.label).join(', ')}`)
|
|
246
|
+
function fmtX(row: T): string {
|
|
247
|
+
return xFormat ? xFormat(row[xKey]) : String(row[xKey])
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Crosshair tooltip: x header + one row per series. Portaled to body, so it leans on inherited
|
|
251
|
+
// CSS custom properties for theming rather than scoped classes.
|
|
252
|
+
function tooltipTemplate(d: T): string {
|
|
253
|
+
return renderLineTooltip(
|
|
254
|
+
String(xFormat ? xFormat(d[xKey]) : d[xKey]),
|
|
255
|
+
resolvedSeries.value.map(s => ({
|
|
256
|
+
color: s.color,
|
|
257
|
+
label: s.label,
|
|
258
|
+
value: fmtY(num(d[s.key]), s.key),
|
|
259
|
+
})),
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
</script>
|
|
263
|
+
|
|
264
|
+
<template>
|
|
265
|
+
<div
|
|
266
|
+
ref="chartRoot"
|
|
267
|
+
data-ui="UiLineChart"
|
|
268
|
+
class="ui-line-chart"
|
|
269
|
+
:style="{ height: `${chartHeight}px` }"
|
|
270
|
+
>
|
|
271
|
+
<div v-if="data.length < 2" class="flex items-center justify-center h-full text-sm text-muted">
|
|
272
|
+
Not enough data to chart.
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<ClientOnly v-else>
|
|
276
|
+
<VisXYContainer
|
|
277
|
+
:key="chartKey"
|
|
278
|
+
:width="measuredWidth || undefined"
|
|
279
|
+
:height="chartHeight"
|
|
280
|
+
:data="data"
|
|
281
|
+
:margin="margin"
|
|
282
|
+
:auto-margin="false"
|
|
283
|
+
:y-domain="yDomain"
|
|
284
|
+
aria-hidden="true"
|
|
285
|
+
class="chart-layer chart-layer--interactive"
|
|
286
|
+
>
|
|
287
|
+
<template v-for="s in resolvedSeries" :key="s.key">
|
|
288
|
+
<!-- Area stops at the settled boundary — filling under a partial sum
|
|
289
|
+
overstates it more than the line alone does. -->
|
|
290
|
+
<VisArea
|
|
291
|
+
v-if="s.area"
|
|
292
|
+
curve-type="monotoneX"
|
|
293
|
+
:x="x"
|
|
294
|
+
:y="(d: T, i: number) => settledY(d, i, s.key)"
|
|
295
|
+
:color="s.color"
|
|
296
|
+
:opacity="0.12"
|
|
297
|
+
/>
|
|
298
|
+
<VisLine
|
|
299
|
+
curve-type="monotoneX"
|
|
300
|
+
:x="x"
|
|
301
|
+
:y="(d: T, i: number) => settledY(d, i, s.key)"
|
|
302
|
+
:color="s.color"
|
|
303
|
+
:line-width="2"
|
|
304
|
+
/>
|
|
305
|
+
<VisLine
|
|
306
|
+
v-if="settledIndex != null"
|
|
307
|
+
curve-type="monotoneX"
|
|
308
|
+
:x="x"
|
|
309
|
+
:y="(d: T, i: number) => provisionalY(d, i, s.key)"
|
|
310
|
+
:color="s.color"
|
|
311
|
+
:line-width="2"
|
|
312
|
+
:line-dash-array="[4, 3]"
|
|
313
|
+
/>
|
|
314
|
+
</template>
|
|
315
|
+
<VisScatter
|
|
316
|
+
v-if="selectedX != null && resolvedSeries[0]"
|
|
317
|
+
:x="x"
|
|
318
|
+
:y="(d: T) => num(d[resolvedSeries[0]!.key])"
|
|
319
|
+
:size="selectedSize"
|
|
320
|
+
:color="resolvedSeries[0].color"
|
|
321
|
+
stroke-color="var(--ui-bg)"
|
|
322
|
+
:stroke-width="2"
|
|
323
|
+
/>
|
|
324
|
+
<VisAxis
|
|
325
|
+
type="x"
|
|
326
|
+
:tick-line="false"
|
|
327
|
+
:grid-line="false"
|
|
328
|
+
:domain-line="false"
|
|
329
|
+
:tick-values="tickIndices"
|
|
330
|
+
:tick-format="tickFormat"
|
|
331
|
+
:tick-text-align="TextAlign.Center"
|
|
332
|
+
tick-text-font-size="14px"
|
|
333
|
+
tick-text-color="var(--ui-text-muted)"
|
|
334
|
+
/>
|
|
335
|
+
<!-- A vertical guide plus the tooltip carries the multi-series read;
|
|
336
|
+
the selected crawl marker remains the only persistent point. -->
|
|
337
|
+
<VisCrosshair v-if="!annotationInteracting" :x="x" :y="crosshairY" color="none" :template="tooltipTemplate" />
|
|
338
|
+
<VisTooltip v-if="!annotationInteracting" />
|
|
339
|
+
</VisXYContainer>
|
|
340
|
+
|
|
341
|
+
<!-- Day-anchored annotation markers (shared overlay; root is position:relative). -->
|
|
342
|
+
<UiChartAnnotations
|
|
343
|
+
:annotations="anchoredAnnotations"
|
|
344
|
+
:x-domain="annotationXDomain"
|
|
345
|
+
:options="annotationOptions"
|
|
346
|
+
@edit="emit('annotationEdit', $event)"
|
|
347
|
+
@interaction-change="annotationInteracting = $event"
|
|
348
|
+
/>
|
|
349
|
+
</ClientOnly>
|
|
350
|
+
|
|
351
|
+
<!-- Screen-reader equivalent of the position-encoded lines, and the
|
|
352
|
+
keyboard path to the hover readout: UiChartDataReveal keeps it hidden
|
|
353
|
+
until a keyboard user asks for it. -->
|
|
354
|
+
<UiChartDataReveal v-if="data.length >= 2">
|
|
355
|
+
<table>
|
|
356
|
+
<caption>{{ ariaLabel }}</caption>
|
|
357
|
+
<thead>
|
|
358
|
+
<tr>
|
|
359
|
+
<th scope="col">
|
|
360
|
+
{{ xKey }}
|
|
361
|
+
</th>
|
|
362
|
+
<th v-for="s in resolvedSeries" :key="s.key" scope="col">
|
|
363
|
+
{{ s.label }}
|
|
364
|
+
</th>
|
|
365
|
+
</tr>
|
|
366
|
+
</thead>
|
|
367
|
+
<tbody>
|
|
368
|
+
<tr v-for="(row, i) in data" :key="i">
|
|
369
|
+
<th scope="row">
|
|
370
|
+
{{ fmtX(row) }}
|
|
371
|
+
</th>
|
|
372
|
+
<td v-for="s in resolvedSeries" :key="s.key">
|
|
373
|
+
{{ fmtY(num(row[s.key]), s.key) }}
|
|
374
|
+
</td>
|
|
375
|
+
</tr>
|
|
376
|
+
</tbody>
|
|
377
|
+
</table>
|
|
378
|
+
</UiChartDataReveal>
|
|
379
|
+
</div>
|
|
380
|
+
</template>
|
|
381
|
+
|
|
382
|
+
<style scoped>
|
|
383
|
+
.ui-line-chart {
|
|
384
|
+
display: grid;
|
|
385
|
+
/* minmax(0, …): a bare 1fr track floors at the stacked SVGs' width, so the
|
|
386
|
+
chart grew with the window but never shrank back and pushed the page into
|
|
387
|
+
a horizontal scroll. */
|
|
388
|
+
grid-template-columns: minmax(0, 1fr);
|
|
389
|
+
position: relative;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.ui-line-chart .chart-layer {
|
|
393
|
+
grid-column-start: 1;
|
|
394
|
+
grid-row-start: 1;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/* Visual series layers don't capture pointer events; only the crosshair layer does. */
|
|
398
|
+
.ui-line-chart .chart-layer {
|
|
399
|
+
pointer-events: none;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.ui-line-chart .chart-layer--interactive {
|
|
403
|
+
pointer-events: auto;
|
|
404
|
+
--vis-crosshair-line-stroke-color: var(--ui-border-accented);
|
|
405
|
+
--vis-crosshair-line-stroke-opacity: 0.6;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.ui-line-chart :deep(.unovis-area-group path) {
|
|
409
|
+
stroke: none;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* Drop the crosshair snap circle entirely. The persistent selected marker and
|
|
413
|
+
vertical guide carry the read without adding a floating point that appears
|
|
414
|
+
to belong to only one series. The crosshair group's class is hashed by
|
|
415
|
+
unovis; the `crosshair-component` suffix is stable. */
|
|
416
|
+
.ui-line-chart :deep([class*="crosshair-component"] circle) {
|
|
417
|
+
display: none;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/* Zero horizontal margin: anchor edge tick labels inward so they don't clip at
|
|
421
|
+
the svg boundary. Unovis sets text-anchor as an SVG attribute, which CSS
|
|
422
|
+
outranks; d3-axis keeps tick <g>s DOM-ordered by value, so first/last-of-type
|
|
423
|
+
are the leftmost/rightmost ticks. */
|
|
424
|
+
.ui-line-chart :deep(g.tick:first-of-type > text) {
|
|
425
|
+
text-anchor: start;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.ui-line-chart :deep(g.tick:last-of-type > text) {
|
|
429
|
+
text-anchor: end;
|
|
430
|
+
}
|
|
431
|
+
</style>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
/**
|
|
3
|
+
* The bar-filled metric cell with a hover popover, shared across every overview
|
|
4
|
+
* "By site" table (Search Console, Indexing, Analytics, DataForSEO, CWV). The
|
|
5
|
+
* trigger is a {@link UiBarCell} carrying the headline value (a `#value` slot,
|
|
6
|
+
* so each tab keeps full control of formatting / threshold colors / share %) and
|
|
7
|
+
* a compact sparkline; the popover panel shows a wide sparkline plus an optional
|
|
8
|
+
* list of `label → value | trend` rows.
|
|
9
|
+
*
|
|
10
|
+
* Replaces the ~60-line block that used to be copy-pasted per metric per tab.
|
|
11
|
+
*/
|
|
12
|
+
import { ref, useId } from 'vue'
|
|
13
|
+
import { UiPopover, UiSparkline, UiTrend } from '#components'
|
|
14
|
+
import UiBarCell from './UiBarCell.vue'
|
|
15
|
+
|
|
16
|
+
export interface MetricPanelRow {
|
|
17
|
+
label: string
|
|
18
|
+
/** Plain value (mono). Ignored when `trend` is set. */
|
|
19
|
+
value?: string
|
|
20
|
+
/** Extra classes for the value (e.g. a threshold color). */
|
|
21
|
+
valueClass?: string
|
|
22
|
+
/** Render a UiTrend instead of a plain value. */
|
|
23
|
+
trend?: number
|
|
24
|
+
trendInverted?: boolean
|
|
25
|
+
trendFormat?: 'percent' | 'points'
|
|
26
|
+
/** Draw a hairline divider above this row (section break). */
|
|
27
|
+
divider?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
sparkline = [],
|
|
32
|
+
panelRows = [],
|
|
33
|
+
} = defineProps<{
|
|
34
|
+
/** Bar fill 0–100. */
|
|
35
|
+
percent: number
|
|
36
|
+
/** Tailwind bg class for the bar fill. */
|
|
37
|
+
barClass?: string
|
|
38
|
+
/** Trend/sparkline series. Rendered only when length > 1. */
|
|
39
|
+
sparkline?: number[]
|
|
40
|
+
/** Sparkline color (named viz color or rgb()). */
|
|
41
|
+
sparklineColor?: string
|
|
42
|
+
/** Popover heading + body. */
|
|
43
|
+
title: string
|
|
44
|
+
description: string
|
|
45
|
+
/** Detail rows under the panel sparkline. */
|
|
46
|
+
panelRows?: MetricPanelRow[]
|
|
47
|
+
}>()
|
|
48
|
+
|
|
49
|
+
const panelId = useId()
|
|
50
|
+
// Reka's HoverCard ignores touch, so a tap opens the panel through the model.
|
|
51
|
+
// Outside tap and Escape still dismiss it.
|
|
52
|
+
const open = ref(false)
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<template>
|
|
56
|
+
<UiPopover v-model:open="open" mode="hover" role="tooltip" :panel-id="panelId" :content="{ side: 'bottom' }">
|
|
57
|
+
<!-- Focusable trigger so the hover panel is also reachable by keyboard
|
|
58
|
+
(reka HoverCard opens on focus) and by tap (the click opens it). The
|
|
59
|
+
headline value stays inline for everyone, so the panel breakdown is a
|
|
60
|
+
true enhancement. The accessible name starts with that visible value
|
|
61
|
+
(WCAG 2.5.3), so voice users can say what they see. -->
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
class="block w-full text-left rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
|
65
|
+
:aria-describedby="panelId"
|
|
66
|
+
@click="open = true"
|
|
67
|
+
>
|
|
68
|
+
<UiBarCell :percent="percent" :bar-class="barClass">
|
|
69
|
+
<slot name="value" />
|
|
70
|
+
<span class="sr-only">{{ title }}, show breakdown</span>
|
|
71
|
+
<UiSparkline
|
|
72
|
+
v-if="sparkline.length > 1"
|
|
73
|
+
:data="sparkline"
|
|
74
|
+
:width="56"
|
|
75
|
+
:height="18"
|
|
76
|
+
:color="sparklineColor"
|
|
77
|
+
class="shrink-0 relative"
|
|
78
|
+
aria-hidden="true"
|
|
79
|
+
/>
|
|
80
|
+
<slot name="trailing" />
|
|
81
|
+
</UiBarCell>
|
|
82
|
+
</button>
|
|
83
|
+
<template #panel>
|
|
84
|
+
<div class="p-3 text-xs space-y-2.5 min-w-[220px] max-w-[250px]">
|
|
85
|
+
<div class="font-semibold text-default">
|
|
86
|
+
{{ title }}
|
|
87
|
+
</div>
|
|
88
|
+
<p class="text-muted leading-relaxed">
|
|
89
|
+
{{ description }}
|
|
90
|
+
</p>
|
|
91
|
+
<UiSparkline
|
|
92
|
+
v-if="sparkline.length > 1"
|
|
93
|
+
:data="sparkline"
|
|
94
|
+
:color="sparklineColor"
|
|
95
|
+
:width="230"
|
|
96
|
+
:height="32"
|
|
97
|
+
/>
|
|
98
|
+
<div v-if="panelRows.length" class="border-t border-default pt-2 space-y-1.5">
|
|
99
|
+
<div
|
|
100
|
+
v-for="row in panelRows"
|
|
101
|
+
:key="row.label"
|
|
102
|
+
class="flex justify-between items-center gap-4"
|
|
103
|
+
:class="row.divider ? 'pt-1 border-t border-default' : ''"
|
|
104
|
+
>
|
|
105
|
+
<span class="text-muted">{{ row.label }}</span>
|
|
106
|
+
<UiTrend
|
|
107
|
+
v-if="row.trend != null"
|
|
108
|
+
:value="row.trend"
|
|
109
|
+
:inverted="row.trendInverted"
|
|
110
|
+
colored
|
|
111
|
+
:format="row.trendFormat ?? 'percent'"
|
|
112
|
+
size="xs"
|
|
113
|
+
/>
|
|
114
|
+
<span v-else class="font-mono font-medium tabular-nums" :class="row.valueClass ?? 'text-default'">{{ row.value }}</span>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<slot name="panel-extra" />
|
|
118
|
+
</div>
|
|
119
|
+
</template>
|
|
120
|
+
</UiPopover>
|
|
121
|
+
</template>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
const { size = 'xs', tone = 'dimmed' } = defineProps<{
|
|
3
|
+
label?: string
|
|
4
|
+
/** xs = dense table/metric labels, sm = popover/list section labels */
|
|
5
|
+
size?: 'xs' | 'sm'
|
|
6
|
+
tone?: 'default' | 'muted' | 'dimmed' | 'success' | 'warning' | 'error'
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
// xs = the dense .text-label tier (10px); sm = a 12px label for popover/list
|
|
10
|
+
// section headings. (Both were 10px before — the sm tier was a dead no-op.)
|
|
11
|
+
const sizeClass = {
|
|
12
|
+
xs: 'text-label',
|
|
13
|
+
sm: 'text-xs uppercase tracking-wider font-semibold',
|
|
14
|
+
} as const
|
|
15
|
+
|
|
16
|
+
const toneClass = {
|
|
17
|
+
default: 'text-default',
|
|
18
|
+
muted: 'text-muted',
|
|
19
|
+
dimmed: 'text-dimmed',
|
|
20
|
+
success: 'text-success',
|
|
21
|
+
warning: 'text-warning',
|
|
22
|
+
error: 'text-error',
|
|
23
|
+
} as const
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<span :class="[sizeClass[size], toneClass[tone]]">
|
|
28
|
+
<slot>{{ label }}</slot>
|
|
29
|
+
</span>
|
|
30
|
+
</template>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { SlotTextOptions } from '../../utils/slot-text'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { UiIcon, UiSlotText, UiTrend } from '#components'
|
|
5
|
+
/**
|
|
6
|
+
* Compact direct-label stat for the card-grid chart area: an optional metric
|
|
7
|
+
* icon, the value, an optional unit label, and a trend delta. Used to label each
|
|
8
|
+
* overview card's chart inline (e.g. "⌖ 16.4K clicks +40%") instead of a
|
|
9
|
+
* separate legend. Shared across every overview tab.
|
|
10
|
+
*/
|
|
11
|
+
const {
|
|
12
|
+
icon,
|
|
13
|
+
value,
|
|
14
|
+
label,
|
|
15
|
+
trend,
|
|
16
|
+
trendInverted,
|
|
17
|
+
trendFormat = 'percent',
|
|
18
|
+
animatedValue = true,
|
|
19
|
+
} = defineProps<{
|
|
20
|
+
/** Lucide/registry icon name for the metric. */
|
|
21
|
+
icon?: string
|
|
22
|
+
/** Formatted value (e.g. "16.4K", "94%"). Omit to show only the label + trend
|
|
23
|
+
* (e.g. when the value is already displayed elsewhere, like a chart readout). */
|
|
24
|
+
value?: string
|
|
25
|
+
/** Rolls compact numeric values when they change. */
|
|
26
|
+
animatedValue?: boolean | SlotTextOptions
|
|
27
|
+
/** Unit/metric label (e.g. "clicks"). */
|
|
28
|
+
label?: string
|
|
29
|
+
/** Trend delta; rendered as a UiTrend when non-null. */
|
|
30
|
+
trend?: number | null
|
|
31
|
+
/** Invert trend coloring (lower is better — position, errors, CWV). */
|
|
32
|
+
trendInverted?: boolean
|
|
33
|
+
trendFormat?: 'percent' | 'points'
|
|
34
|
+
}>()
|
|
35
|
+
|
|
36
|
+
const animatedValueOptions = computed<SlotTextOptions | null>(() => {
|
|
37
|
+
if (!value || animatedValue === false)
|
|
38
|
+
return null
|
|
39
|
+
|
|
40
|
+
const compactMetricValue = /^[+\-$€£¥]?\s*[\d,.]+(?:\s?[kmbt])?(?:\.\d+)?%?$/i.test(value)
|
|
41
|
+
if (typeof animatedValue === 'object')
|
|
42
|
+
return animatedValue
|
|
43
|
+
|
|
44
|
+
return compactMetricValue && value.length <= 14
|
|
45
|
+
? { direction: 'up', duration: 300, stagger: 22 }
|
|
46
|
+
: null
|
|
47
|
+
})
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<template>
|
|
51
|
+
<span class="inline-flex items-baseline gap-1.5 min-w-0">
|
|
52
|
+
<UiIcon v-if="icon" :name="icon" class="size-3.5 text-dimmed shrink-0 self-center" aria-hidden="true" />
|
|
53
|
+
<span v-if="value" class="text-sm font-semibold tabular-nums text-default">
|
|
54
|
+
<UiSlotText
|
|
55
|
+
v-if="animatedValueOptions"
|
|
56
|
+
:text="value"
|
|
57
|
+
:options="animatedValueOptions"
|
|
58
|
+
/>
|
|
59
|
+
<template v-else>
|
|
60
|
+
{{ value }}
|
|
61
|
+
</template>
|
|
62
|
+
</span>
|
|
63
|
+
<span v-if="label" class="text-xs text-muted truncate" :class="{ 'font-medium text-default': !value }">{{ label }}</span>
|
|
64
|
+
<UiTrend
|
|
65
|
+
v-if="trend != null"
|
|
66
|
+
:value="trend"
|
|
67
|
+
:inverted="trendInverted"
|
|
68
|
+
colored
|
|
69
|
+
:format="trendFormat"
|
|
70
|
+
size="2xs"
|
|
71
|
+
/>
|
|
72
|
+
</span>
|
|
73
|
+
</template>
|