@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,281 @@
|
|
|
1
|
+
<script lang="ts" setup generic="T extends object">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { formatGroupedNumber } from '../../composables/formatting'
|
|
4
|
+
// Horizontal ranked bar chart. Sorts rows by their numeric value and draws a labelled bar per
|
|
5
|
+
// row, scaled to the largest value — the canonical "top N pages / queries / countries" view.
|
|
6
|
+
// Generic + dependency-free (CSS bars), so any row array drives it.
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
data,
|
|
10
|
+
labelKey,
|
|
11
|
+
valueKey,
|
|
12
|
+
max = 8,
|
|
13
|
+
sort = true,
|
|
14
|
+
size = 'sm',
|
|
15
|
+
density = 'regular',
|
|
16
|
+
color = 'var(--ui-primary)',
|
|
17
|
+
format,
|
|
18
|
+
ceilingKey,
|
|
19
|
+
truncatedKey,
|
|
20
|
+
} = defineProps<{
|
|
21
|
+
data: T[]
|
|
22
|
+
/** Row key for the category label. */
|
|
23
|
+
labelKey: keyof T & string
|
|
24
|
+
/** Row key for the numeric value. */
|
|
25
|
+
valueKey: keyof T & string
|
|
26
|
+
/**
|
|
27
|
+
* Row key holding the observation CEILING — how far the row could have gone
|
|
28
|
+
* had the producer finished (sitemap-expected pages, sampled population).
|
|
29
|
+
* The span between `valueKey` and this draws hatched: observed, then
|
|
30
|
+
* unobserved. Without it a short bar reads as "measured and small" when it
|
|
31
|
+
* often means "we stopped looking".
|
|
32
|
+
*/
|
|
33
|
+
ceilingKey?: keyof T & string
|
|
34
|
+
/**
|
|
35
|
+
* Row key holding a truthy "this value is a floor" flag — the scan hit a row
|
|
36
|
+
* budget or per-rule cap, so the true count is at or above what is drawn.
|
|
37
|
+
* The whole bar hatches and the value label renders `≥ n`.
|
|
38
|
+
*/
|
|
39
|
+
truncatedKey?: keyof T & string
|
|
40
|
+
/** Max rows to show (top N). Default 8. */
|
|
41
|
+
max?: number
|
|
42
|
+
/** Sort largest first. Disable for ordered buckets such as histograms. */
|
|
43
|
+
sort?: boolean
|
|
44
|
+
/** Row density. Use md when the chart is the primary comparison. */
|
|
45
|
+
size?: 'sm' | 'md'
|
|
46
|
+
/** Compact keeps readable type and condenses to one row when space permits. */
|
|
47
|
+
density?: 'regular' | 'compact'
|
|
48
|
+
/** Bar colour (any CSS colour). */
|
|
49
|
+
color?: string
|
|
50
|
+
/** Format the value label. */
|
|
51
|
+
format?: (value: number) => string
|
|
52
|
+
}>()
|
|
53
|
+
|
|
54
|
+
defineSlots<{
|
|
55
|
+
/** Replace the text label while preserving the ranked row and exact value. */
|
|
56
|
+
label: (props: { item: T, label: string, value: number, index: number }) => unknown
|
|
57
|
+
}>()
|
|
58
|
+
|
|
59
|
+
function num(v: unknown) {
|
|
60
|
+
const n = typeof v === 'number' ? v : Number(String(v).replace(/^"+|"+$/g, ''))
|
|
61
|
+
return Number.isFinite(n) ? n : 0
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const rows = computed(() => {
|
|
65
|
+
const values = Array.from(data, item => ({
|
|
66
|
+
item,
|
|
67
|
+
label: String(item[labelKey] ?? ''),
|
|
68
|
+
value: num(item[valueKey]),
|
|
69
|
+
// A ceiling below the observed value is meaningless, so clamp rather than
|
|
70
|
+
// draw a negative provisional span.
|
|
71
|
+
ceiling: ceilingKey ? Math.max(num(item[ceilingKey]), num(item[valueKey])) : 0,
|
|
72
|
+
truncated: truncatedKey ? Boolean(item[truncatedKey]) : false,
|
|
73
|
+
}))
|
|
74
|
+
const sorted = sort ? values.sort((a, b) => b.value - a.value) : values
|
|
75
|
+
const visible = sorted
|
|
76
|
+
.slice(0, max)
|
|
77
|
+
// The ceiling shares the value's scale — a bar whose unobserved span ran past
|
|
78
|
+
// the peak would otherwise clip and read as complete.
|
|
79
|
+
const peak = Math.max(1, ...visible.map(r => Math.max(r.value, r.ceiling)))
|
|
80
|
+
const pct = (v: number) => Math.min(100, Math.max(0, (v / peak) * 100))
|
|
81
|
+
return visible.map(r => ({
|
|
82
|
+
...r,
|
|
83
|
+
pct: pct(r.value),
|
|
84
|
+
// Width of the hatched span, in track percent. Zero when the row is a
|
|
85
|
+
// complete observation.
|
|
86
|
+
provisionalPct: r.ceiling > r.value ? pct(r.ceiling) - pct(r.value) : 0,
|
|
87
|
+
}))
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
function fmt(value: number): string {
|
|
91
|
+
return format ? format(value) : formatGroupedNumber(value)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// `141 / 188` needs a wider value column than `141`. Widen once for the whole
|
|
95
|
+
// chart so the numbers stay on one right-aligned edge.
|
|
96
|
+
const hasCeiling = computed(() => rows.value.some(r => r.provisionalPct > 0))
|
|
97
|
+
|
|
98
|
+
/** Truncated counts are floors — say so in the label, not only in the fill. */
|
|
99
|
+
function fmtValue(r: { value: number, truncated: boolean }): string {
|
|
100
|
+
return r.truncated ? `≥ ${fmt(r.value)}` : fmt(r.value)
|
|
101
|
+
}
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<template>
|
|
105
|
+
<ol
|
|
106
|
+
data-ui="UiBarChart"
|
|
107
|
+
:data-density="density"
|
|
108
|
+
class="ui-bar-chart flex flex-col"
|
|
109
|
+
:class="[
|
|
110
|
+
size === 'md' ? 'ui-bar-chart--md' : 'ui-bar-chart--sm',
|
|
111
|
+
hasCeiling ? 'ui-bar-chart--ceiling' : '',
|
|
112
|
+
density === 'compact' ? 'ui-bar-chart--compact gap-1' : size === 'md' ? 'gap-3' : 'gap-1.5',
|
|
113
|
+
]"
|
|
114
|
+
>
|
|
115
|
+
<li
|
|
116
|
+
v-for="(r, i) in rows"
|
|
117
|
+
:key="`${r.label}:${i}`"
|
|
118
|
+
class="ui-bar-chart__row"
|
|
119
|
+
>
|
|
120
|
+
<span class="ui-bar-chart__label min-w-0 text-muted" :title="r.label">
|
|
121
|
+
<slot
|
|
122
|
+
name="label"
|
|
123
|
+
:item="r.item"
|
|
124
|
+
:label="r.label"
|
|
125
|
+
:value="r.value"
|
|
126
|
+
:index="i"
|
|
127
|
+
>{{ r.label }}</slot>
|
|
128
|
+
</span>
|
|
129
|
+
<div
|
|
130
|
+
data-bar-track
|
|
131
|
+
class="ui-bar-chart__track relative overflow-hidden rounded bg-muted/40"
|
|
132
|
+
:class="density === 'compact' ? 'h-2' : size === 'md' ? 'h-7' : 'h-5'"
|
|
133
|
+
aria-hidden="true"
|
|
134
|
+
>
|
|
135
|
+
<!-- Truncated rows hatch the whole bar: the value itself is a floor, so
|
|
136
|
+
there is no solid span to separate. Width, not scaleX — a scaled
|
|
137
|
+
hatch stretches its pitch and stops reading as one texture. -->
|
|
138
|
+
<div
|
|
139
|
+
v-if="r.truncated"
|
|
140
|
+
class="provisional-hatch absolute inset-y-0 left-0 rounded ring-1 ring-inset ring-current/35"
|
|
141
|
+
:style="{ width: `${r.pct}%`, color }"
|
|
142
|
+
/>
|
|
143
|
+
<div
|
|
144
|
+
v-else
|
|
145
|
+
class="absolute inset-y-0 left-0 w-full origin-left rounded"
|
|
146
|
+
:style="{ transform: `scaleX(${r.pct / 100})`, background: color, opacity: 0.85 }"
|
|
147
|
+
/>
|
|
148
|
+
<!-- Observed → unobserved. Starts where the solid span ends. -->
|
|
149
|
+
<div
|
|
150
|
+
v-if="r.provisionalPct > 0"
|
|
151
|
+
class="provisional-hatch absolute inset-y-0 rounded-r ring-1 ring-inset ring-current/30"
|
|
152
|
+
:style="{ left: `${r.pct}%`, width: `${r.provisionalPct}%`, color }"
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
<span class="ui-bar-chart__value text-right tabular-nums text-highlighted" :class="size === 'md' ? 'numerals-display' : ''">
|
|
156
|
+
{{ fmtValue(r) }}
|
|
157
|
+
<!-- The ceiling reads in the label too, not only in the fill — a hatch
|
|
158
|
+
survives neither greyscale nor a cropped screenshot. -->
|
|
159
|
+
<span v-if="r.provisionalPct > 0" class="text-muted">/ {{ fmt(r.ceiling) }}</span>
|
|
160
|
+
<span v-if="r.provisionalPct > 0" class="sr-only">expected, the rest unobserved</span>
|
|
161
|
+
<span v-else-if="r.truncated" class="sr-only">at least — the scan hit its row budget</span>
|
|
162
|
+
</span>
|
|
163
|
+
</li>
|
|
164
|
+
</ol>
|
|
165
|
+
</template>
|
|
166
|
+
|
|
167
|
+
<style scoped>
|
|
168
|
+
.ui-bar-chart {
|
|
169
|
+
container-type: inline-size;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.ui-bar-chart__row {
|
|
173
|
+
align-items: center;
|
|
174
|
+
column-gap: 0.75rem;
|
|
175
|
+
display: grid;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.ui-bar-chart--sm .ui-bar-chart__row {
|
|
179
|
+
font-size: 0.75rem;
|
|
180
|
+
grid-template-columns: minmax(0, 2fr) minmax(0, 3fr) 3.5rem;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.ui-bar-chart--sm.ui-bar-chart--ceiling .ui-bar-chart__row {
|
|
184
|
+
grid-template-columns: minmax(0, 2fr) minmax(0, 3fr) 5.5rem;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.ui-bar-chart--sm .ui-bar-chart__label {
|
|
188
|
+
overflow: hidden;
|
|
189
|
+
text-overflow: ellipsis;
|
|
190
|
+
white-space: nowrap;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.ui-bar-chart--md .ui-bar-chart__row {
|
|
194
|
+
font-size: 0.875rem;
|
|
195
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
196
|
+
row-gap: 0.5rem;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.ui-bar-chart--md .ui-bar-chart__label {
|
|
200
|
+
overflow-wrap: anywhere;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.ui-bar-chart--md .ui-bar-chart__track {
|
|
204
|
+
grid-column: 1 / -1;
|
|
205
|
+
grid-row: 2;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.ui-bar-chart__value {
|
|
209
|
+
min-width: 4rem;
|
|
210
|
+
white-space: nowrap;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.ui-bar-chart--compact .ui-bar-chart__row {
|
|
214
|
+
column-gap: 0.5rem;
|
|
215
|
+
font-size: 0.875rem;
|
|
216
|
+
grid-template-columns: minmax(0, 1fr) max-content;
|
|
217
|
+
row-gap: 0.25rem;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.ui-bar-chart--compact .ui-bar-chart__label {
|
|
221
|
+
overflow: visible;
|
|
222
|
+
overflow-wrap: anywhere;
|
|
223
|
+
text-overflow: clip;
|
|
224
|
+
white-space: normal;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.ui-bar-chart--compact .ui-bar-chart__track {
|
|
228
|
+
grid-column: 1 / -1;
|
|
229
|
+
grid-row: 2;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.ui-bar-chart--compact .ui-bar-chart__value {
|
|
233
|
+
grid-column: 2;
|
|
234
|
+
grid-row: 1;
|
|
235
|
+
min-width: 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
@container (min-width: 16rem) {
|
|
239
|
+
.ui-bar-chart--compact .ui-bar-chart__row {
|
|
240
|
+
grid-template-columns: minmax(0, 1.15fr) minmax(3rem, 1fr) minmax(3.5rem, max-content);
|
|
241
|
+
row-gap: 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.ui-bar-chart--compact .ui-bar-chart__label {
|
|
245
|
+
overflow: hidden;
|
|
246
|
+
text-overflow: ellipsis;
|
|
247
|
+
white-space: nowrap;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.ui-bar-chart--compact .ui-bar-chart__track {
|
|
251
|
+
grid-column: 2;
|
|
252
|
+
grid-row: 1;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.ui-bar-chart--compact .ui-bar-chart__value {
|
|
256
|
+
grid-column: 3;
|
|
257
|
+
grid-row: 1;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
@container (min-width: 36rem) {
|
|
262
|
+
.ui-bar-chart--md:not(.ui-bar-chart--compact) .ui-bar-chart__row {
|
|
263
|
+
grid-template-columns: minmax(10rem, 1fr) minmax(12rem, 2fr) 4rem;
|
|
264
|
+
row-gap: 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.ui-bar-chart--md.ui-bar-chart--ceiling:not(.ui-bar-chart--compact) .ui-bar-chart__row {
|
|
268
|
+
grid-template-columns: minmax(10rem, 1fr) minmax(12rem, 2fr) 6.5rem;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.ui-bar-chart--md:not(.ui-bar-chart--compact) .ui-bar-chart__track {
|
|
272
|
+
grid-column: 2;
|
|
273
|
+
grid-row: 1;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.ui-bar-chart--md:not(.ui-bar-chart--compact) .ui-bar-chart__value {
|
|
277
|
+
grid-column: 3;
|
|
278
|
+
grid-row: 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
</style>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script lang="ts" setup generic="K extends string">
|
|
2
|
+
import type { ChartAnnotationControlItem } from '../../utils/chartAnnotations'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { UCheckbox, UiButton, UiIcon, UiPopover } from '#components'
|
|
5
|
+
|
|
6
|
+
const { items, dense = false } = defineProps<{
|
|
7
|
+
items: readonly ChartAnnotationControlItem<K>[]
|
|
8
|
+
/** Compact icon+chevron trigger for a joined toolbar pill (e.g. beside a metric toggle) instead of the standalone quiet button. */
|
|
9
|
+
dense?: boolean
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits<{
|
|
13
|
+
toggle: [key: K, enabled: boolean]
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
const hiddenCount = computed(() => items.filter(item => !item.enabled).length)
|
|
17
|
+
const triggerLabel = computed(() => hiddenCount.value
|
|
18
|
+
? `Configure chart annotations, ${hiddenCount.value} hidden`
|
|
19
|
+
: 'Configure chart annotations')
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<UiPopover :content="{ side: 'bottom', align: 'end' }">
|
|
24
|
+
<button
|
|
25
|
+
v-if="dense"
|
|
26
|
+
type="button"
|
|
27
|
+
data-annotation-controls
|
|
28
|
+
:aria-label="triggerLabel"
|
|
29
|
+
class="cursor-pointer inline-flex items-center justify-center gap-0.5 size-11 sm:size-7 rounded-md transition-colors duration-150 outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
|
30
|
+
:class="hiddenCount ? 'text-primary' : 'text-dimmed hover:text-default'"
|
|
31
|
+
>
|
|
32
|
+
<UiIcon name="flag" class="size-3.5" aria-hidden="true" />
|
|
33
|
+
<UiIcon name="chevron-down" class="size-2.5" aria-hidden="true" />
|
|
34
|
+
</button>
|
|
35
|
+
<UiButton
|
|
36
|
+
v-else
|
|
37
|
+
data-annotation-controls
|
|
38
|
+
purpose="quiet"
|
|
39
|
+
size="xs"
|
|
40
|
+
icon="sliders"
|
|
41
|
+
:aria-label="triggerLabel"
|
|
42
|
+
:class="hiddenCount ? 'size-11 text-primary bg-primary/10 sm:size-8' : 'size-11 text-muted sm:size-8'"
|
|
43
|
+
/>
|
|
44
|
+
|
|
45
|
+
<template #panel>
|
|
46
|
+
<div data-annotation-controls-panel class="w-64 max-w-[calc(100vw-2rem)] p-3">
|
|
47
|
+
<p class="px-1 pb-2 text-label">
|
|
48
|
+
Show on charts
|
|
49
|
+
</p>
|
|
50
|
+
<div class="divide-y divide-default" role="group" aria-label="Annotation sources">
|
|
51
|
+
<label
|
|
52
|
+
v-for="item in items"
|
|
53
|
+
:key="item.key"
|
|
54
|
+
class="min-h-11 grid grid-cols-[1.5rem_minmax(0,1fr)_auto_auto] items-center gap-2 px-1 text-sm text-default cursor-pointer"
|
|
55
|
+
>
|
|
56
|
+
<span class="size-6 rounded-full bg-default ring-1 ring-default grid place-items-center text-muted" aria-hidden="true">
|
|
57
|
+
<UiIcon :name="item.icon" class="size-3.5" />
|
|
58
|
+
</span>
|
|
59
|
+
<span class="min-w-0 truncate">{{ item.label }}</span>
|
|
60
|
+
<span v-if="item.count != null" class="text-mini text-dimmed tabular-nums">
|
|
61
|
+
{{ item.count }}
|
|
62
|
+
</span>
|
|
63
|
+
<UCheckbox
|
|
64
|
+
:model-value="item.enabled"
|
|
65
|
+
size="sm"
|
|
66
|
+
:aria-label="`Show ${item.label}`"
|
|
67
|
+
@update:model-value="emit('toggle', item.key, $event === true)"
|
|
68
|
+
/>
|
|
69
|
+
</label>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</template>
|
|
73
|
+
</UiPopover>
|
|
74
|
+
</template>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { ChartAnnotation, ChartAnnotationOptions } from '../../utils/chartAnnotations'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { UiIcon, UiPopover } from '#components'
|
|
5
|
+
import { safeHttpHref } from '../../../shared/utils/urls'
|
|
6
|
+
import { annotationIconPreview, DEFAULT_ANNOTATION_ICON_LIMIT, resolveAnnotationMarkerGroups } from '../../utils/chartAnnotations'
|
|
7
|
+
|
|
8
|
+
// Annotation marker overlay: thin vertical lines + interactive dots at specific
|
|
9
|
+
// x-positions, for any time-series chart. Extracted from UiChartFrame so charts
|
|
10
|
+
// that compose unovis directly (CWV, indexing) get the same markers without
|
|
11
|
+
// adopting the whole tooltip/brush frame.
|
|
12
|
+
//
|
|
13
|
+
// Same-day annotations collapse into ONE marker. Every marker opens the same
|
|
14
|
+
// click/keyboard popover, so interactive source and edit actions never depend
|
|
15
|
+
// on hover. Up to three item icons preview the stack beside the marker dot;
|
|
16
|
+
// remaining items collapse into a +N count.
|
|
17
|
+
//
|
|
18
|
+
// Positioning: drop this INSIDE the chart's plot wrap (the element whose box is
|
|
19
|
+
// `position: relative` and whose width == the plot x-span, i.e. a chart drawn
|
|
20
|
+
// with zero horizontal margin). Each marker is `position: absolute` with a CSS
|
|
21
|
+
// left-percentage; `width: 0` so it never affects layout, `-translate-x-1/2` to
|
|
22
|
+
// centre on the anchor. `bottom-9` (36px) clears a standard x-axis label band.
|
|
23
|
+
|
|
24
|
+
const { annotations, xDomain, options } = defineProps<{
|
|
25
|
+
/** Event annotations. Empty/undefined renders nothing. */
|
|
26
|
+
annotations?: ChartAnnotation[]
|
|
27
|
+
/** Visible x range [min, max] (Date | ISO string | epoch-ms). Required to position. */
|
|
28
|
+
xDomain?: [Date | string | number, Date | string | number]
|
|
29
|
+
/** Marker presentation. Icon previews are capped at three by type. */
|
|
30
|
+
options?: ChartAnnotationOptions
|
|
31
|
+
}>()
|
|
32
|
+
|
|
33
|
+
const emit = defineEmits<{
|
|
34
|
+
/** Activation of a marker carrying a user-editable annotation. */
|
|
35
|
+
edit: [annotation: ChartAnnotation]
|
|
36
|
+
/** Whether the pointer is currently over a marker trigger. */
|
|
37
|
+
interactionChange: [active: boolean]
|
|
38
|
+
}>()
|
|
39
|
+
|
|
40
|
+
const maxIcons = computed(() => options?.maxIcons ?? DEFAULT_ANNOTATION_ICON_LIMIT)
|
|
41
|
+
const markers = computed(() => resolveAnnotationMarkerGroups(annotations, xDomain).map(marker => ({
|
|
42
|
+
...marker,
|
|
43
|
+
iconPreview: annotationIconPreview(marker.items, maxIcons.value),
|
|
44
|
+
})))
|
|
45
|
+
|
|
46
|
+
function editItem(item: ChartAnnotation, close?: () => void) {
|
|
47
|
+
emit('edit', item)
|
|
48
|
+
close?.()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function markerAriaLabel(items: ChartAnnotation[]): string {
|
|
52
|
+
return items.map(item => item.description ? `${item.label}. ${item.description}` : item.label).join('; ')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function itemIcon(item: ChartAnnotation): string {
|
|
56
|
+
return item.icon ?? (item.editable ? 'edit' : 'note')
|
|
57
|
+
}
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<template>
|
|
61
|
+
<div
|
|
62
|
+
v-for="m in markers"
|
|
63
|
+
:key="m.id"
|
|
64
|
+
class="absolute top-0 bottom-9 z-[5] pointer-events-none w-0"
|
|
65
|
+
:style="{ left: m.leftPct }"
|
|
66
|
+
>
|
|
67
|
+
<!-- Thin 1px vertical marker line centred on the anchor point. -->
|
|
68
|
+
<div class="absolute inset-y-0 w-px -translate-x-1/2 opacity-50" :class="m.toneClass" aria-hidden="true" />
|
|
69
|
+
<UiPopover :content="{ side: 'top', align: 'center' }">
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
data-annotation-marker
|
|
73
|
+
class="group absolute -bottom-4.5 size-11 -translate-x-1/2 pointer-events-auto rounded-full grid place-items-center cursor-pointer active:scale-[0.97] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
|
74
|
+
:aria-label="markerAriaLabel(m.items)"
|
|
75
|
+
@pointerenter.stop="emit('interactionChange', true)"
|
|
76
|
+
@pointerleave.stop="emit('interactionChange', false)"
|
|
77
|
+
@pointerdown.stop
|
|
78
|
+
@pointermove.stop
|
|
79
|
+
@mousemove.stop
|
|
80
|
+
@click.stop
|
|
81
|
+
>
|
|
82
|
+
<span
|
|
83
|
+
class="size-1.5 transition-transform duration-100 group-hover:scale-125 group-hover:ring-2 group-hover:ring-primary"
|
|
84
|
+
:class="[m.toneClass, m.editable ? 'rounded-[2px]' : 'rounded-full']"
|
|
85
|
+
aria-hidden="true"
|
|
86
|
+
/>
|
|
87
|
+
<span
|
|
88
|
+
v-if="m.iconPreview.visible.length || m.iconPreview.overflowCount"
|
|
89
|
+
data-annotation-icon-preview
|
|
90
|
+
class="absolute left-7 top-1/2 -translate-y-1/2 pointer-events-auto flex items-center -space-x-1 cursor-pointer transition-transform duration-100 group-hover:-translate-y-[55%]"
|
|
91
|
+
aria-hidden="true"
|
|
92
|
+
>
|
|
93
|
+
<span
|
|
94
|
+
v-for="(item, index) in m.iconPreview.visible"
|
|
95
|
+
:key="item.id ?? index"
|
|
96
|
+
data-annotation-icon
|
|
97
|
+
class="size-4 shrink-0 rounded-full bg-default ring-1 ring-default grid place-items-center text-muted transition-colors duration-100 group-hover:ring-primary group-hover:text-default"
|
|
98
|
+
>
|
|
99
|
+
<UiIcon :name="itemIcon(item)" class="size-2.5" />
|
|
100
|
+
</span>
|
|
101
|
+
<span
|
|
102
|
+
v-if="m.iconPreview.overflowCount"
|
|
103
|
+
data-annotation-overflow
|
|
104
|
+
class="h-4 min-w-4 rounded-full bg-default px-1 ring-1 ring-default grid place-items-center font-mono text-[9px] leading-none text-muted transition-colors duration-100 group-hover:ring-primary group-hover:text-default"
|
|
105
|
+
>
|
|
106
|
+
+{{ m.iconPreview.overflowCount }}
|
|
107
|
+
</span>
|
|
108
|
+
</span>
|
|
109
|
+
</button>
|
|
110
|
+
<template #panel="{ close }">
|
|
111
|
+
<div data-annotation-popover-panel class="w-96 max-w-[calc(100vw-2rem)] p-4">
|
|
112
|
+
<p class="pb-3 text-label">
|
|
113
|
+
{{ m.count === 1 ? 'Annotation' : `${m.count} annotations` }}
|
|
114
|
+
</p>
|
|
115
|
+
<div class="divide-y divide-default">
|
|
116
|
+
<div
|
|
117
|
+
v-for="(item, i) in m.items"
|
|
118
|
+
:key="item.id ?? i"
|
|
119
|
+
class="grid grid-cols-[1.25rem_minmax(0,1fr)_auto] items-start gap-x-3 gap-y-1 py-3 first:pt-0 last:pb-0"
|
|
120
|
+
>
|
|
121
|
+
<span class="mt-0.5 size-5 rounded-full bg-default ring-1 ring-default grid place-items-center text-muted" aria-hidden="true">
|
|
122
|
+
<UiIcon :name="itemIcon(item)" class="size-3" />
|
|
123
|
+
</span>
|
|
124
|
+
<button
|
|
125
|
+
v-if="item.editable"
|
|
126
|
+
type="button"
|
|
127
|
+
class="min-h-11 min-w-0 text-left text-sm font-semibold leading-5 text-default hover:text-primary sm:min-h-0"
|
|
128
|
+
@click="editItem(item, close)"
|
|
129
|
+
>
|
|
130
|
+
{{ item.label }}
|
|
131
|
+
</button>
|
|
132
|
+
<p v-else class="min-w-0 text-sm font-semibold leading-5 text-default">
|
|
133
|
+
{{ item.label }}
|
|
134
|
+
</p>
|
|
135
|
+
<a
|
|
136
|
+
v-if="safeHttpHref(item.href)"
|
|
137
|
+
:href="safeHttpHref(item.href)"
|
|
138
|
+
target="_blank"
|
|
139
|
+
rel="noopener noreferrer"
|
|
140
|
+
class="size-11 -my-3 shrink-0 rounded-md grid place-items-center text-muted hover:text-default focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary sm:-my-1 sm:size-7"
|
|
141
|
+
:aria-label="`Open source for ${item.label}`"
|
|
142
|
+
>
|
|
143
|
+
<UiIcon name="arrow-up-right" class="size-4" aria-hidden="true" />
|
|
144
|
+
</a>
|
|
145
|
+
<p v-if="item.description" class="col-start-2 col-end-4 text-sm leading-5 text-muted">
|
|
146
|
+
{{ item.description }}
|
|
147
|
+
</p>
|
|
148
|
+
<img
|
|
149
|
+
v-if="item.image"
|
|
150
|
+
:src="item.image.src"
|
|
151
|
+
:alt="item.image.alt"
|
|
152
|
+
loading="lazy"
|
|
153
|
+
width="1600"
|
|
154
|
+
height="800"
|
|
155
|
+
class="col-start-2 col-end-4 mt-1 aspect-[2/1] w-full rounded-md object-cover object-top ring-1 ring-default"
|
|
156
|
+
>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</template>
|
|
161
|
+
</UiPopover>
|
|
162
|
+
</div>
|
|
163
|
+
</template>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { ChartBrushRange } from '../../composables/useChartBrush'
|
|
3
|
+
import { AnimatePresence, motion, useReducedMotion } from 'motion-v'
|
|
4
|
+
import { computed } from 'vue'
|
|
5
|
+
import { formatReportingDay } from '../../composables/formatting'
|
|
6
|
+
|
|
7
|
+
// Brush overlay for time-series charts. Pairs with useChartBrush — pass through
|
|
8
|
+
// `dragRange`, `selectionLeft`, `selectionWidth`. Renders:
|
|
9
|
+
// 1. The translucent selection rect with dashed primary borders
|
|
10
|
+
// 2. Floating start + end date pills (inverted, anchored to rect edges)
|
|
11
|
+
// 3. Centred duration pill (primary, only when rect is wide enough)
|
|
12
|
+
// Day labels are formatted inline (Month Day) — generic enough that no caller
|
|
13
|
+
// has needed to customise so far.
|
|
14
|
+
|
|
15
|
+
const props = defineProps<{
|
|
16
|
+
dragRange: ChartBrushRange | null
|
|
17
|
+
selectionLeft: number
|
|
18
|
+
selectionWidth: number
|
|
19
|
+
/** Distance from the wrapper's bottom in px (default 36, leaves room for axis ticks). */
|
|
20
|
+
bottomOffset?: number
|
|
21
|
+
/** Minimum width before the duration pill renders (default 56px). */
|
|
22
|
+
durationThreshold?: number
|
|
23
|
+
}>()
|
|
24
|
+
|
|
25
|
+
function formatDragDate(iso: string): string {
|
|
26
|
+
if (!iso)
|
|
27
|
+
return ''
|
|
28
|
+
// Reporting-day label — UTC frame so it doesn't drift per viewer (ADR-0082).
|
|
29
|
+
return formatReportingDay(iso)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Honour reduced-motion: collapse the entry/exit tween to an instant cut.
|
|
33
|
+
const reduced = useReducedMotion()
|
|
34
|
+
const enterTransition = computed(() => reduced.value ? { duration: 0 } : { duration: 0.15, ease: 'easeOut' as const })
|
|
35
|
+
|
|
36
|
+
const startLabel = computed(() => formatDragDate(props.dragRange?.startDate ?? ''))
|
|
37
|
+
const endLabel = computed(() => formatDragDate(props.dragRange?.endDate ?? ''))
|
|
38
|
+
const durationLabel = computed(() => {
|
|
39
|
+
const r = props.dragRange
|
|
40
|
+
if (!r)
|
|
41
|
+
return ''
|
|
42
|
+
const days = r.endIdx - r.startIdx + 1
|
|
43
|
+
return `${days} day${days === 1 ? '' : 's'}`
|
|
44
|
+
})
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<AnimatePresence>
|
|
49
|
+
<motion.div
|
|
50
|
+
v-if="dragRange"
|
|
51
|
+
key="chart-brush-rect"
|
|
52
|
+
:initial="{ opacity: 0 }"
|
|
53
|
+
:animate="{ opacity: 1 }"
|
|
54
|
+
:exit="{ opacity: 0 }"
|
|
55
|
+
:transition="enterTransition"
|
|
56
|
+
:style="{ left: `${selectionLeft}px`, width: `${selectionWidth}px`, bottom: `${bottomOffset ?? 36}px` }"
|
|
57
|
+
class="absolute top-0 z-10 pointer-events-none bg-primary/10"
|
|
58
|
+
/>
|
|
59
|
+
<motion.div
|
|
60
|
+
v-if="dragRange"
|
|
61
|
+
key="chart-brush-start"
|
|
62
|
+
:initial="{ opacity: 0, y: 4 }"
|
|
63
|
+
:animate="{ opacity: 1, y: 0 }"
|
|
64
|
+
:exit="{ opacity: 0, y: 4 }"
|
|
65
|
+
:transition="enterTransition"
|
|
66
|
+
:style="{ left: `${selectionLeft}px`, bottom: `${bottomOffset ?? 36}px` }"
|
|
67
|
+
class="absolute z-10 -translate-x-1/2 px-1.5 py-0.5 rounded-full bg-inverted text-inverted text-mini font-medium tabular-nums whitespace-nowrap pointer-events-none shadow-sm"
|
|
68
|
+
>
|
|
69
|
+
{{ startLabel }}
|
|
70
|
+
</motion.div>
|
|
71
|
+
<motion.div
|
|
72
|
+
v-if="dragRange"
|
|
73
|
+
key="chart-brush-end"
|
|
74
|
+
:initial="{ opacity: 0, y: 4 }"
|
|
75
|
+
:animate="{ opacity: 1, y: 0 }"
|
|
76
|
+
:exit="{ opacity: 0, y: 4 }"
|
|
77
|
+
:transition="enterTransition"
|
|
78
|
+
:style="{ left: `${selectionLeft + selectionWidth}px`, bottom: `${bottomOffset ?? 36}px` }"
|
|
79
|
+
class="absolute z-10 -translate-x-1/2 px-1.5 py-0.5 rounded-full bg-inverted text-inverted text-mini font-medium tabular-nums whitespace-nowrap pointer-events-none shadow-sm"
|
|
80
|
+
>
|
|
81
|
+
{{ endLabel }}
|
|
82
|
+
</motion.div>
|
|
83
|
+
<motion.div
|
|
84
|
+
v-if="dragRange && selectionWidth > (durationThreshold ?? 56)"
|
|
85
|
+
key="chart-brush-duration"
|
|
86
|
+
:initial="{ opacity: 0, scale: 0.9 }"
|
|
87
|
+
:animate="{ opacity: 1, scale: 1 }"
|
|
88
|
+
:exit="{ opacity: 0, scale: 0.9 }"
|
|
89
|
+
:transition="enterTransition"
|
|
90
|
+
:style="{ left: `${selectionLeft + selectionWidth / 2}px` }"
|
|
91
|
+
class="absolute top-2 z-10 -translate-x-1/2 px-2 py-0.5 rounded-full bg-primary/90 text-inverted text-label whitespace-nowrap pointer-events-none shadow-sm"
|
|
92
|
+
>
|
|
93
|
+
{{ durationLabel }}
|
|
94
|
+
</motion.div>
|
|
95
|
+
</AnimatePresence>
|
|
96
|
+
</template>
|