@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,81 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { VNodeChild } from 'vue'
|
|
3
|
+
import { nextTick, ref, useId } from 'vue'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* UiChartDataReveal
|
|
7
|
+
*
|
|
8
|
+
* The keyboard path to a chart's values. Chart readouts (crosshair, point and
|
|
9
|
+
* band tooltips) open on pointer hover only, and each chart already carries its
|
|
10
|
+
* data as a screen-reader table. This wraps that table: it stays visually
|
|
11
|
+
* hidden, and a "Show data table" button appears on keyboard focus and lays the
|
|
12
|
+
* same table over the chart. Escape or the button hides it again. Pointer users
|
|
13
|
+
* see no change.
|
|
14
|
+
*
|
|
15
|
+
* The host chart root must be `position: relative`; the open table covers it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
defineSlots<{ default?: () => VNodeChild }>()
|
|
19
|
+
|
|
20
|
+
const shown = ref(false)
|
|
21
|
+
const panelId = useId()
|
|
22
|
+
const toggle = ref<HTMLButtonElement>()
|
|
23
|
+
|
|
24
|
+
async function hide(): Promise<void> {
|
|
25
|
+
shown.value = false
|
|
26
|
+
await nextTick()
|
|
27
|
+
toggle.value?.focus()
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<button
|
|
33
|
+
ref="toggle"
|
|
34
|
+
type="button"
|
|
35
|
+
data-ui="UiChartDataReveal"
|
|
36
|
+
class="ui-chart-data-reveal__toggle rounded-md bg-default px-2 py-1 focus-visible:px-2 focus-visible:py-1 text-xs font-medium text-default ring-1 ring-default focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
|
37
|
+
:class="shown ? 'absolute top-1 right-1 z-30' : 'sr-only focus-visible:not-sr-only focus-visible:absolute focus-visible:top-1 focus-visible:right-1 focus-visible:z-30'"
|
|
38
|
+
:aria-expanded="shown"
|
|
39
|
+
:aria-controls="panelId"
|
|
40
|
+
@click="shown ? hide() : shown = true"
|
|
41
|
+
@keydown.esc="shown && hide()"
|
|
42
|
+
>
|
|
43
|
+
{{ shown ? 'Hide data table' : 'Show data table' }}
|
|
44
|
+
</button>
|
|
45
|
+
<div
|
|
46
|
+
:id="panelId"
|
|
47
|
+
:class="shown ? 'ui-chart-data-reveal__panel absolute inset-0 z-20 overflow-auto rounded-md bg-default p-2 pt-9 ring-1 ring-default' : 'sr-only'"
|
|
48
|
+
@keydown.esc.stop="shown && hide()"
|
|
49
|
+
>
|
|
50
|
+
<slot />
|
|
51
|
+
</div>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<style scoped>
|
|
55
|
+
.ui-chart-data-reveal__panel :deep(table) {
|
|
56
|
+
width: 100%;
|
|
57
|
+
border-collapse: collapse;
|
|
58
|
+
font-size: 0.75rem;
|
|
59
|
+
font-variant-numeric: tabular-nums;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.ui-chart-data-reveal__panel :deep(caption) {
|
|
63
|
+
padding-bottom: 0.25rem;
|
|
64
|
+
text-align: left;
|
|
65
|
+
font-weight: 600;
|
|
66
|
+
color: var(--ui-text-highlighted);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.ui-chart-data-reveal__panel :deep(th),
|
|
70
|
+
.ui-chart-data-reveal__panel :deep(td) {
|
|
71
|
+
padding: 0.25rem 0.5rem;
|
|
72
|
+
border-bottom: 1px solid var(--ui-border);
|
|
73
|
+
text-align: left;
|
|
74
|
+
white-space: nowrap;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.ui-chart-data-reveal__panel :deep(thead th) {
|
|
78
|
+
color: var(--ui-text-muted);
|
|
79
|
+
font-weight: 500;
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { MotionValue } from 'motion-v'
|
|
3
|
+
import type { ChartBrushRange } from '../../composables/useChartBrush'
|
|
4
|
+
import type { ChartAnnotation, ChartAnnotationOptions } from '../../utils/chartAnnotations'
|
|
5
|
+
import { computed, ref } from 'vue'
|
|
6
|
+
import { ClientOnly, UiChartAnnotations, UiChartBrushOverlay, UiChartTooltip } from '#components'
|
|
7
|
+
import { ANNOTATION_TONE_CLASS, annotationsOnDay } from '../../utils/chartAnnotations'
|
|
8
|
+
|
|
9
|
+
// Bundles the chart's hover tooltip + brush selection overlay into one tag, and
|
|
10
|
+
// (opt-in) the annotation marker overlay + same-day annotations inside the
|
|
11
|
+
// tooltip. Card owns the wrap div + ref binding (the brush composable's `wrapRef`
|
|
12
|
+
// doesn't survive being passed through a child); this just consolidates the
|
|
13
|
+
// overlay calls + their prop wiring.
|
|
14
|
+
//
|
|
15
|
+
// Tooltip anchors to the cursor (spring-tracked) — see UiChartTooltip. Markers
|
|
16
|
+
// render via UiChartAnnotations (shared with non-frame charts); both require the
|
|
17
|
+
// host wrap to be `position: relative` with zero horizontal margin (wrap width
|
|
18
|
+
// == plot x-span) so the CSS left-percentage maps to the right screen position.
|
|
19
|
+
|
|
20
|
+
const props = defineProps<{
|
|
21
|
+
// --- brush state (useChartBrush) ---
|
|
22
|
+
dragRange: ChartBrushRange | null
|
|
23
|
+
selectionLeft: number
|
|
24
|
+
selectionWidth: number
|
|
25
|
+
// --- hover state (useChartHover) ---
|
|
26
|
+
tooltipVisible: boolean
|
|
27
|
+
cardSpring: MotionValue<number>
|
|
28
|
+
cursorYSpring: MotionValue<number>
|
|
29
|
+
placement?: 'above' | 'below'
|
|
30
|
+
/** Label shown on the leading row of the tooltip (typically the hovered date). */
|
|
31
|
+
hoverLabel: string
|
|
32
|
+
/**
|
|
33
|
+
* Optional event annotations rendered as thin vertical marker lines + a dot at
|
|
34
|
+
* the bottom of the plot area. Default [] — no change to existing charts (opt-in).
|
|
35
|
+
* Requires `xDomain` to resolve pixel positions. When `xDomain` is absent,
|
|
36
|
+
* annotations are silently skipped.
|
|
37
|
+
*/
|
|
38
|
+
annotations?: ChartAnnotation[]
|
|
39
|
+
/** Marker presentation shared by single and stacked annotation popovers. */
|
|
40
|
+
annotationOptions?: ChartAnnotationOptions
|
|
41
|
+
/**
|
|
42
|
+
* The visible x range [min, max] of the chart. Pass Date objects, ISO strings,
|
|
43
|
+
* or epoch-ms numbers. Used to convert each annotation's `x` value to a CSS
|
|
44
|
+
* left-percentage. Required when `annotations` are provided.
|
|
45
|
+
*/
|
|
46
|
+
xDomain?: [Date | string | number, Date | string | number]
|
|
47
|
+
/**
|
|
48
|
+
* The currently-hovered x value (the day the crosshair is on). When it lands on
|
|
49
|
+
* a day that carries annotations, those annotations are surfaced inside the
|
|
50
|
+
* hover tooltip (a tone dot + label) so the marker isn't a hover-only native
|
|
51
|
+
* title. Pass the hovered datum's date; omit to keep annotations marker-only.
|
|
52
|
+
*/
|
|
53
|
+
hoverX?: Date | string | number | null
|
|
54
|
+
}>()
|
|
55
|
+
|
|
56
|
+
const emit = defineEmits<{
|
|
57
|
+
/** Forwarded from the marker overlay: a user-editable annotation was activated. */
|
|
58
|
+
annotationEdit: [annotation: ChartAnnotation]
|
|
59
|
+
}>()
|
|
60
|
+
|
|
61
|
+
const annotationInteracting = ref(false)
|
|
62
|
+
|
|
63
|
+
/** Annotations on the hovered day — surfaced inside the tooltip (dot + label). */
|
|
64
|
+
const tooltipAnnotations = computed(() =>
|
|
65
|
+
annotationsOnDay(props.annotations ?? [], props.hoverX).map((ann, id) => ({
|
|
66
|
+
id,
|
|
67
|
+
label: ann.label,
|
|
68
|
+
description: ann.description,
|
|
69
|
+
toneClass: ANNOTATION_TONE_CLASS[ann.tone ?? 'neutral'],
|
|
70
|
+
})),
|
|
71
|
+
)
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<ClientOnly>
|
|
76
|
+
<UiChartTooltip
|
|
77
|
+
:x="cardSpring"
|
|
78
|
+
:y="cursorYSpring"
|
|
79
|
+
:visible="tooltipVisible && !annotationInteracting"
|
|
80
|
+
:placement="placement"
|
|
81
|
+
:label="hoverLabel"
|
|
82
|
+
>
|
|
83
|
+
<template v-if="$slots['tooltip-header']" #header>
|
|
84
|
+
<slot name="tooltip-header" />
|
|
85
|
+
</template>
|
|
86
|
+
<slot name="tooltip-rows" />
|
|
87
|
+
<!-- Annotations on the hovered day: a tone dot + label, separated from the
|
|
88
|
+
metric rows. Dot carries the semantic tone, label stays neutral text
|
|
89
|
+
(color-budget: colored dot + neutral text). -->
|
|
90
|
+
<div v-if="tooltipAnnotations.length" class="mt-1.5 pt-1.5 border-t border-default space-y-1">
|
|
91
|
+
<div v-for="ann in tooltipAnnotations" :key="ann.id" class="grid grid-cols-[auto_minmax(0,1fr)] gap-x-1.5">
|
|
92
|
+
<span :class="ann.toneClass" class="size-1.5 rounded-full shrink-0 mt-1" aria-hidden="true" />
|
|
93
|
+
<div class="min-w-0">
|
|
94
|
+
<p class="text-sm font-medium text-default truncate">
|
|
95
|
+
{{ ann.label }}
|
|
96
|
+
</p>
|
|
97
|
+
<p v-if="ann.description" class="text-sm text-muted mt-0.5">
|
|
98
|
+
{{ ann.description }}
|
|
99
|
+
</p>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
<template v-if="$slots['tooltip-footer']" #footer>
|
|
104
|
+
<slot name="tooltip-footer" />
|
|
105
|
+
</template>
|
|
106
|
+
</UiChartTooltip>
|
|
107
|
+
|
|
108
|
+
<UiChartBrushOverlay
|
|
109
|
+
:drag-range="dragRange"
|
|
110
|
+
:selection-left="selectionLeft"
|
|
111
|
+
:selection-width="selectionWidth"
|
|
112
|
+
/>
|
|
113
|
+
|
|
114
|
+
<!-- Annotation markers (thin vertical lines + dots) — shared overlay. -->
|
|
115
|
+
<UiChartAnnotations
|
|
116
|
+
:annotations="annotations"
|
|
117
|
+
:x-domain="xDomain"
|
|
118
|
+
:options="annotationOptions"
|
|
119
|
+
@edit="emit('annotationEdit', $event)"
|
|
120
|
+
@interaction-change="annotationInteracting = $event"
|
|
121
|
+
/>
|
|
122
|
+
</ClientOnly>
|
|
123
|
+
</template>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { MotionValue } from 'motion-v'
|
|
3
|
+
import { AnimatePresence, motion, useReducedMotion } from 'motion-v'
|
|
4
|
+
import { computed } from 'vue'
|
|
5
|
+
// Single floating readout for time-series chart hover. Anchored to the cursor
|
|
6
|
+
// position (spring-tracked x + y), sitting either above or below depending on
|
|
7
|
+
// available room. Uses the shared overlay chrome (`.ui-popover-content` +
|
|
8
|
+
// `bg-default`) so it reads as a solid raised card in both light/dark — the
|
|
9
|
+
// metric rows consumers pass in use the standard text tokens (text-muted /
|
|
10
|
+
// text-default / text-dimmed), which only stay legible on a non-inverted
|
|
11
|
+
// surface. No companion crosshair pill, no chart-edge pinning.
|
|
12
|
+
//
|
|
13
|
+
// Position decomposition:
|
|
14
|
+
// - Outer motion.div: spring-tracked translate to cursor (x, y) in chart
|
|
15
|
+
// coordinates. Acts as the anchor point.
|
|
16
|
+
// - Inner div: applies `-translate-x-1/2` (centre horizontally on cursor)
|
|
17
|
+
// plus `-translate-y-full -mt-3` (anchor bottom edge above cursor) or
|
|
18
|
+
// `mt-3` (anchor top edge below cursor) based on `placement`.
|
|
19
|
+
//
|
|
20
|
+
// Slots:
|
|
21
|
+
// #header — optional badge row (e.g. GSC's "Estimated")
|
|
22
|
+
// default — metric rows (each card decides layout)
|
|
23
|
+
// #footer — optional comparison-date strip
|
|
24
|
+
|
|
25
|
+
defineProps<{
|
|
26
|
+
x: MotionValue<number>
|
|
27
|
+
y: MotionValue<number>
|
|
28
|
+
visible: boolean
|
|
29
|
+
/** Date label always rendered on the leading row. */
|
|
30
|
+
label: string
|
|
31
|
+
/** Which side of the cursor the tooltip sits on. */
|
|
32
|
+
placement?: 'above' | 'below'
|
|
33
|
+
}>()
|
|
34
|
+
|
|
35
|
+
// Reduced motion: drop the scale pop + placement-translate easing; the tooltip
|
|
36
|
+
// fades only (sibling chart overlays gate the same way).
|
|
37
|
+
const reduced = useReducedMotion()
|
|
38
|
+
const enter = computed(() => (reduced.value ? { opacity: 1 } : { opacity: 1, scale: 1 }))
|
|
39
|
+
const initial = computed(() => (reduced.value ? { opacity: 0 } : { opacity: 0, scale: 0.96 }))
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<AnimatePresence>
|
|
44
|
+
<motion.div
|
|
45
|
+
v-if="visible"
|
|
46
|
+
key="chart-tooltip"
|
|
47
|
+
:initial="initial"
|
|
48
|
+
:animate="enter"
|
|
49
|
+
:exit="initial"
|
|
50
|
+
:transition="{ duration: 0.12, ease: [0.22, 1, 0.36, 1] }"
|
|
51
|
+
:style="{ x, y }"
|
|
52
|
+
class="absolute top-0 left-0 z-10 pointer-events-none"
|
|
53
|
+
>
|
|
54
|
+
<div
|
|
55
|
+
class="ui-popover-content min-w-[120px] max-w-[280px] rounded-md bg-default text-default ring ring-default -translate-x-1/2 transition-transform duration-150 ease-out motion-reduce:transition-none" :class="[
|
|
56
|
+
(placement ?? 'above') === 'above' ? '-translate-y-full -mt-3' : 'mt-3',
|
|
57
|
+
]"
|
|
58
|
+
>
|
|
59
|
+
<div
|
|
60
|
+
v-if="$slots.header"
|
|
61
|
+
class="px-2 pt-1.5 pb-1 border-b border-muted flex items-center gap-2"
|
|
62
|
+
>
|
|
63
|
+
<slot name="header" />
|
|
64
|
+
</div>
|
|
65
|
+
<div class="px-2 py-1 flex items-center gap-2 whitespace-nowrap">
|
|
66
|
+
<span class="text-mini font-medium tabular-nums text-muted">{{ label }}</span>
|
|
67
|
+
<div v-if="$slots.default" class="h-3 w-px bg-[var(--ui-border)]" />
|
|
68
|
+
<div class="flex-1 flex flex-col gap-px text-mini">
|
|
69
|
+
<slot />
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
<div
|
|
73
|
+
v-if="$slots.footer"
|
|
74
|
+
class="px-2 pt-1 pb-1.5 border-t border-muted flex items-center justify-between gap-2 text-mini"
|
|
75
|
+
>
|
|
76
|
+
<slot name="footer" />
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</motion.div>
|
|
80
|
+
</AnimatePresence>
|
|
81
|
+
</template>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { NuxtLink, UiIcon } from '#components'
|
|
4
|
+
/**
|
|
5
|
+
* UiCoverageNote — the quiet "N of M known · View all" evidence-coverage line
|
|
6
|
+
* that sits under a typed evidence browser. Mirrors pro-actions's
|
|
7
|
+
* `collectionCoverageSchema` (`layers/pro/actions/shared/contracts/action-evidence.ts`)
|
|
8
|
+
* structurally, per the `UiActionTitle`/`ActionTitleSegment` "keep in sync"
|
|
9
|
+
* precedent (ADR-0042: design-system MAY know the shape, never the runtime
|
|
10
|
+
* that builds it — importing the pro-layer's zod schema here would invert the
|
|
11
|
+
* layer graph).
|
|
12
|
+
*
|
|
13
|
+
* Color budget: this is a caption, not a status. It never tints its value —
|
|
14
|
+
* only a single fixed neutral dot (the `UiFactsGrid` status-dot rhythm,
|
|
15
|
+
* without the semantic color) marks it as a coverage caption at a glance.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Keep in sync with pro-actions's `CollectionCoverage` (`action-evidence.ts`). */
|
|
19
|
+
export type CollectionCoverage
|
|
20
|
+
= | { _tag: 'complete', knownTotal: number }
|
|
21
|
+
| { _tag: 'partial', reportedTotal: number, knownTotal: number, limitation: string }
|
|
22
|
+
| { _tag: 'count-only', reportedTotal: number, limitation: string }
|
|
23
|
+
| { _tag: 'unknown', knownTotal: number, limitation: string }
|
|
24
|
+
| { _tag: 'unavailable', limitation: string }
|
|
25
|
+
|
|
26
|
+
const { coverage, to } = defineProps<{
|
|
27
|
+
coverage: CollectionCoverage
|
|
28
|
+
/** Deep link for the "View all" tail. Canonical copy — never re-worded per call site. */
|
|
29
|
+
to?: string
|
|
30
|
+
}>()
|
|
31
|
+
|
|
32
|
+
// Split copy into a leading numeral (numerals-display) + trailing words, so
|
|
33
|
+
// "7 of 9 known" reads with figures set apart from prose the same way every
|
|
34
|
+
// other numeric fact does. `complete`/`unavailable` have no leading count
|
|
35
|
+
// worth isolating (either it's redundant with the check glyph, or there's
|
|
36
|
+
// nothing counted yet) and render as plain text.
|
|
37
|
+
const parts = computed((): { lead?: string, rest: string } => {
|
|
38
|
+
switch (coverage._tag) {
|
|
39
|
+
case 'complete':
|
|
40
|
+
return { rest: `All ${coverage.knownTotal} known` }
|
|
41
|
+
case 'partial':
|
|
42
|
+
return { lead: `${coverage.knownTotal} of ${coverage.reportedTotal}`, rest: 'known' }
|
|
43
|
+
case 'count-only':
|
|
44
|
+
return { lead: `${coverage.reportedTotal}`, rest: 'total, not yet itemized' }
|
|
45
|
+
case 'unknown':
|
|
46
|
+
return { lead: `${coverage.knownTotal}`, rest: 'known, total not yet counted' }
|
|
47
|
+
case 'unavailable':
|
|
48
|
+
return { rest: 'Evidence still compiling' }
|
|
49
|
+
default:
|
|
50
|
+
return { rest: '' }
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// `partial` is the one state where evidence is actively still growing — the
|
|
55
|
+
// dot earns a single settle-once pulse to say "still counting" without
|
|
56
|
+
// nagging on every re-render. Every other tag is a steady-state read.
|
|
57
|
+
const isGrowing = computed(() => coverage._tag === 'partial')
|
|
58
|
+
const isComplete = computed(() => coverage._tag === 'complete')
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<template>
|
|
62
|
+
<p class="flex items-center gap-1.5 text-xs text-muted" data-ui="UiCoverageNote">
|
|
63
|
+
<UiIcon
|
|
64
|
+
v-if="isComplete"
|
|
65
|
+
name="check"
|
|
66
|
+
class="size-3 shrink-0 text-dimmed"
|
|
67
|
+
aria-hidden="true"
|
|
68
|
+
/>
|
|
69
|
+
<span
|
|
70
|
+
v-else
|
|
71
|
+
class="size-1 shrink-0 rounded-full bg-[var(--ui-border)]"
|
|
72
|
+
:class="isGrowing && 'coverage-dot-pulse-once'"
|
|
73
|
+
aria-hidden="true"
|
|
74
|
+
/>
|
|
75
|
+
<span>
|
|
76
|
+
<span v-if="parts.lead" class="numerals-display text-default">{{ parts.lead }}</span>
|
|
77
|
+
<template v-if="parts.lead">{{ ' ' }}</template>{{ parts.rest }}
|
|
78
|
+
</span>
|
|
79
|
+
<template v-if="to">
|
|
80
|
+
<span class="text-dimmed" aria-hidden="true"> · </span>
|
|
81
|
+
<component
|
|
82
|
+
:is="NuxtLink"
|
|
83
|
+
:to="to"
|
|
84
|
+
class="text-muted underline-offset-2 hover:text-default hover:underline"
|
|
85
|
+
>
|
|
86
|
+
View all
|
|
87
|
+
</component>
|
|
88
|
+
</template>
|
|
89
|
+
</p>
|
|
90
|
+
</template>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { motion, stagger, useReducedMotion } from 'motion-v'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { indexingVizColors } from '../../composables/dataVizColors'
|
|
5
|
+
|
|
6
|
+
// Mini, dependency-free echo of CwvMetricChart: faint good/ni/poor stacked bars
|
|
7
|
+
// across time, meant as an ambient background for a KPI tile (the Core Web
|
|
8
|
+
// Vitals card). It is decorative — the exact composition does not need to be
|
|
9
|
+
// precise — so when no per-period histogram is supplied it synthesises a
|
|
10
|
+
// plausible series from the overall `status`. Sibling to UiSparkline.
|
|
11
|
+
|
|
12
|
+
interface Band { good: number, ni: number, poor: number }
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
/** Per-period composition (good/ni/poor percentages, 0–100). When set, the svg is named with the latest period's shares; when absent, the synthesised bars are hidden from assistive tech. */
|
|
16
|
+
data?: Band[]
|
|
17
|
+
/** Drives the synthesised series + bar emphasis when `data` is absent. */
|
|
18
|
+
status?: 'pass' | 'ni' | 'fail' | null
|
|
19
|
+
width?: number | string
|
|
20
|
+
height?: number | string
|
|
21
|
+
/** Number of bars when synthesising. */
|
|
22
|
+
bars?: number
|
|
23
|
+
/** SVG preserveAspectRatio — "none" stretches to fill a container. */
|
|
24
|
+
preserveAspectRatio?: string
|
|
25
|
+
/** Base opacity for the bars. Kept moderate so the card value stays dominant
|
|
26
|
+
while the composition reads clearly. */
|
|
27
|
+
opacity?: number
|
|
28
|
+
/** Hover state, fed from a parent (e.g. the enclosing UiStat card). On hover
|
|
29
|
+
the bars rise to full height one by one. */
|
|
30
|
+
hovered?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
data,
|
|
35
|
+
status = null,
|
|
36
|
+
width = '100%',
|
|
37
|
+
height = '100%',
|
|
38
|
+
bars = 22,
|
|
39
|
+
preserveAspectRatio = 'none',
|
|
40
|
+
opacity = 0.6,
|
|
41
|
+
hovered = false,
|
|
42
|
+
} = defineProps<Props>()
|
|
43
|
+
|
|
44
|
+
// Respect reduced-motion: skip the staggered hover rise entirely.
|
|
45
|
+
const reduced = useReducedMotion()
|
|
46
|
+
|
|
47
|
+
// On hover the bars settle up to full height in sequence; at rest they sit a
|
|
48
|
+
// touch compressed. scaleY anchored at each bar's base keeps the good/ni/poor
|
|
49
|
+
// proportions intact while the whole column rises.
|
|
50
|
+
const container = {
|
|
51
|
+
rest: {},
|
|
52
|
+
hover: { transition: { delayChildren: stagger(0.05, { ease: 'easeInOut' }) } },
|
|
53
|
+
}
|
|
54
|
+
// A smooth spring with a single gentle overshoot — fluid wave up the bars
|
|
55
|
+
// rather than a jittery bounce.
|
|
56
|
+
const barVariants = {
|
|
57
|
+
rest: { scaleY: 0.72, opacity: 0.85 },
|
|
58
|
+
hover: {
|
|
59
|
+
scaleY: 1,
|
|
60
|
+
opacity: 1,
|
|
61
|
+
transition: { type: 'spring' as const, stiffness: 190, damping: 16, mass: 0.8 },
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const vbW = 120
|
|
66
|
+
const vbH = 40
|
|
67
|
+
|
|
68
|
+
// Deterministic wobble (no Math.random — keeps SSR + reloads stable).
|
|
69
|
+
function wobble(i: number, amp: number) {
|
|
70
|
+
return (Math.sin(i * 1.3) * 0.5 + Math.sin(i * 0.5) * 0.3 + Math.cos(i * 2.7) * 0.2) * amp
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const series = computed<Band[]>(() => {
|
|
74
|
+
if (data?.length)
|
|
75
|
+
return data
|
|
76
|
+
// Synthesise a plausible composition trend from the status.
|
|
77
|
+
const base: Band = status === 'pass'
|
|
78
|
+
? { good: 78, ni: 16, poor: 6 }
|
|
79
|
+
: status === 'fail'
|
|
80
|
+
? { good: 36, ni: 30, poor: 34 }
|
|
81
|
+
: { good: 56, ni: 31, poor: 13 } // ni / unknown
|
|
82
|
+
return Array.from({ length: bars }, (_, i) => {
|
|
83
|
+
const poor = Math.max(2, base.poor + wobble(i, 8))
|
|
84
|
+
const ni = Math.max(4, base.ni + wobble(i + 7, 6))
|
|
85
|
+
const good = Math.max(0, 100 - poor - ni)
|
|
86
|
+
return { good, ni, poor }
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// The chart fills, not the raw semantic hexes: light mode steps success and
|
|
91
|
+
// warning down so the bars clear 3:1 on white (dataVizColors.ts).
|
|
92
|
+
function withAlpha(color: string, a: number): string {
|
|
93
|
+
return `color-mix(in srgb, ${color} ${Math.round(a * 10000) / 100}%, transparent)`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const colors = computed(() => ({
|
|
97
|
+
good: withAlpha(indexingVizColors.indexed.hex, opacity),
|
|
98
|
+
ni: withAlpha(indexingVizColors.notIndexed.hex, opacity),
|
|
99
|
+
poor: withAlpha(indexingVizColors.error.hex, opacity),
|
|
100
|
+
}))
|
|
101
|
+
|
|
102
|
+
// A synthesised series is decorative: it echoes `status`, not measured data,
|
|
103
|
+
// so it stays out of the accessibility tree. Measured data names the latest
|
|
104
|
+
// period's shares, so the composition does not rest on colour alone.
|
|
105
|
+
const pct = (n: number, total: number) => `${Math.round(n / total * 100)}%`
|
|
106
|
+
const ariaLabel = computed(() => {
|
|
107
|
+
const last = data?.at(-1)
|
|
108
|
+
if (!last)
|
|
109
|
+
return undefined
|
|
110
|
+
const total = last.good + last.ni + last.poor || 1
|
|
111
|
+
return `Core Web Vitals, latest period: ${pct(last.good, total)} good, ${pct(last.ni, total)} needs improvement, ${pct(last.poor, total)} poor`
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
interface Seg { y: number, h: number, fill: string }
|
|
115
|
+
interface Bar { x: number, w: number, segs: Seg[] }
|
|
116
|
+
|
|
117
|
+
const barColumns = computed<Bar[]>(() => {
|
|
118
|
+
const s = series.value
|
|
119
|
+
if (!s.length)
|
|
120
|
+
return []
|
|
121
|
+
const gap = 0.25
|
|
122
|
+
const slot = vbW / s.length
|
|
123
|
+
const w = Math.max(0.5, slot * (1 - gap))
|
|
124
|
+
return s.map((band, i) => {
|
|
125
|
+
const total = band.good + band.ni + band.poor || 1
|
|
126
|
+
const x = i * slot + (slot - w) / 2
|
|
127
|
+
// Stack from the bottom: good → ni → poor (poor sits on top, like CrUX).
|
|
128
|
+
let yTop = vbH
|
|
129
|
+
const stack: [number, string][] = [
|
|
130
|
+
[band.good / total, colors.value.good],
|
|
131
|
+
[band.ni / total, colors.value.ni],
|
|
132
|
+
[band.poor / total, colors.value.poor],
|
|
133
|
+
]
|
|
134
|
+
const segs: Seg[] = stack.map(([frac, fill]) => {
|
|
135
|
+
const h = frac * vbH
|
|
136
|
+
yTop -= h
|
|
137
|
+
return { y: yTop, h, fill }
|
|
138
|
+
})
|
|
139
|
+
return { x, w, segs }
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
</script>
|
|
143
|
+
|
|
144
|
+
<template>
|
|
145
|
+
<svg
|
|
146
|
+
data-ui="UiCwvSparkline"
|
|
147
|
+
:width="width"
|
|
148
|
+
:height="height"
|
|
149
|
+
:viewBox="`0 0 ${vbW} ${vbH}`"
|
|
150
|
+
:preserveAspectRatio="preserveAspectRatio"
|
|
151
|
+
:role="ariaLabel ? 'img' : undefined"
|
|
152
|
+
:aria-label="ariaLabel"
|
|
153
|
+
:aria-hidden="ariaLabel ? undefined : 'true'"
|
|
154
|
+
>
|
|
155
|
+
<!-- tabindex -1: motion-v binds focus listeners for its gesture support,
|
|
156
|
+
and Chromium makes any SVG element with a focus listener a Tab stop.
|
|
157
|
+
These groups carry no name or action. -->
|
|
158
|
+
<motion.g
|
|
159
|
+
:variants="container"
|
|
160
|
+
initial="rest"
|
|
161
|
+
:animate="hovered && !reduced ? 'hover' : 'rest'"
|
|
162
|
+
tabindex="-1"
|
|
163
|
+
>
|
|
164
|
+
<motion.g
|
|
165
|
+
v-for="(bar, i) in barColumns"
|
|
166
|
+
:key="i"
|
|
167
|
+
:variants="barVariants"
|
|
168
|
+
:style="{ transformBox: 'fill-box', transformOrigin: 'bottom' }"
|
|
169
|
+
tabindex="-1"
|
|
170
|
+
>
|
|
171
|
+
<!-- The page-coloured edge splits good, needs improvement and poor,
|
|
172
|
+
so the bands read apart without relying on hue. -->
|
|
173
|
+
<rect
|
|
174
|
+
v-for="(seg, j) in bar.segs"
|
|
175
|
+
:key="j"
|
|
176
|
+
:x="bar.x"
|
|
177
|
+
:y="seg.y"
|
|
178
|
+
:width="bar.w"
|
|
179
|
+
:height="seg.h"
|
|
180
|
+
:fill="seg.fill"
|
|
181
|
+
stroke="var(--ui-bg)"
|
|
182
|
+
stroke-width="0.6"
|
|
183
|
+
vector-effect="non-scaling-stroke"
|
|
184
|
+
/>
|
|
185
|
+
</motion.g>
|
|
186
|
+
</motion.g>
|
|
187
|
+
</svg>
|
|
188
|
+
</template>
|