@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,326 @@
|
|
|
1
|
+
// Pure helpers for chart annotations — the day-key matching + marker positioning
|
|
2
|
+
// extracted from UiChartFrame so they're unit-testable and shareable by every
|
|
3
|
+
// annotated chart (UiChartFrame's tooltip, the UiChartAnnotations marker overlay,
|
|
4
|
+
// and the CWV / indexing charts that compose unovis directly).
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A single chart annotation — a thin vertical marker line + interactive dot.
|
|
8
|
+
* A plain UI shape any chart consumer can populate from any data source
|
|
9
|
+
* (timeline events, deploy markers, …); the design system never imports the
|
|
10
|
+
* domain that produces it.
|
|
11
|
+
*/
|
|
12
|
+
export interface ChartAnnotation {
|
|
13
|
+
/** X position as a Date, ISO string, or epoch-ms number matching the chart's x domain. */
|
|
14
|
+
x: number | string | Date
|
|
15
|
+
/** Short label shown in the marker's native title + the chart tooltip. */
|
|
16
|
+
label: string
|
|
17
|
+
/** Optional producer-owned explanation or value shown below the label. */
|
|
18
|
+
description?: string
|
|
19
|
+
/**
|
|
20
|
+
* Semantic tone that tints the marker (semantic CSS classes only).
|
|
21
|
+
* 'success' → bg-success 'error' → bg-error 'warning' → bg-warning
|
|
22
|
+
* 'neutral' (default) → bg-accented
|
|
23
|
+
*/
|
|
24
|
+
tone?: 'success' | 'error' | 'warning' | 'neutral'
|
|
25
|
+
/** Compact semantic or explicitly sourced icon rendered beside the marker dot. */
|
|
26
|
+
icon?: string
|
|
27
|
+
/** Stable producer id — lets marker affordances (edit/delete) address the row. */
|
|
28
|
+
id?: string
|
|
29
|
+
/** Optional link opened from the marker tooltip. Only an http(s) URL renders; any other scheme is dropped. */
|
|
30
|
+
href?: string
|
|
31
|
+
/**
|
|
32
|
+
* User-authored marker: rendered with the editable glyph (square vs dot) and
|
|
33
|
+
* offered edit/delete affordances by consumers that support them.
|
|
34
|
+
*/
|
|
35
|
+
editable?: boolean
|
|
36
|
+
/** Producer category token (e.g. 'release', 'custom') — opaque to the design system. */
|
|
37
|
+
category?: string
|
|
38
|
+
/** Original reporting day retained when a date annotation is re-anchored to an index x-axis. */
|
|
39
|
+
dayKey?: string
|
|
40
|
+
/** Optional producer image shown under the description, such as the page as the change left it. */
|
|
41
|
+
image?: { src: string, alt: string }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What ONE chart accepts, on top of whatever categories the caller treats as
|
|
46
|
+
* global (every chart gets those for free — see `annotationInScope`'s
|
|
47
|
+
* `rules.globalCategories`). Stays domain-opaque like `ChartAnnotation`
|
|
48
|
+
* itself: the design system knows the SHAPE of a per-chart declaration, never
|
|
49
|
+
* which category/kind strings a producer actually uses.
|
|
50
|
+
*/
|
|
51
|
+
export interface ChartAnnotationScope {
|
|
52
|
+
/** Non-global categories this chart accepts (e.g. an 'algo-update' producer). */
|
|
53
|
+
categories: readonly string[]
|
|
54
|
+
/** Topic allow-list scoped to `rules.kindBearingCategory`. Omit to accept every kind within it. */
|
|
55
|
+
kinds?: readonly string[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The two facts a caller supplies about its own category vocabulary — see `annotationInScope`. */
|
|
59
|
+
export interface ChartAnnotationScopeRules {
|
|
60
|
+
/** Categories every chart accepts regardless of `scope` (e.g. user notes, releases). */
|
|
61
|
+
globalCategories: readonly string[]
|
|
62
|
+
/** The one category whose annotations carry a producer `kind` for topic-level filtering. */
|
|
63
|
+
kindBearingCategory: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* True when `annotation` should render on a chart declaring `scope`. A chart
|
|
68
|
+
* silently accepting every category it was never asked for is the bug this
|
|
69
|
+
* closes (manual-review-2026-08 §Annotations: Search Console algorithm
|
|
70
|
+
* updates rendering on a performance chart) — a category outside both
|
|
71
|
+
* `rules.globalCategories` and `scope.categories` is rejected, full stop.
|
|
72
|
+
*/
|
|
73
|
+
export function annotationInScope(
|
|
74
|
+
annotation: { category?: string, kind?: string },
|
|
75
|
+
scope: ChartAnnotationScope,
|
|
76
|
+
rules: ChartAnnotationScopeRules,
|
|
77
|
+
): boolean {
|
|
78
|
+
const category = annotation.category
|
|
79
|
+
if (category == null || rules.globalCategories.includes(category))
|
|
80
|
+
return true
|
|
81
|
+
if (!scope.categories.includes(category))
|
|
82
|
+
return false
|
|
83
|
+
if (category === rules.kindBearingCategory && scope.kinds)
|
|
84
|
+
return annotation.kind != null && scope.kinds.includes(annotation.kind)
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Marker icon count. Three is the visual ceiling; zero shows only the overflow count. */
|
|
89
|
+
export type ChartAnnotationIconLimit = 0 | 1 | 2 | 3
|
|
90
|
+
|
|
91
|
+
export interface ChartAnnotationOptions {
|
|
92
|
+
/** Annotation icons shown beside the marker before a +N overflow count. Default 3. */
|
|
93
|
+
maxIcons?: ChartAnnotationIconLimit
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ChartAnnotationControlItem<K extends string = string> {
|
|
97
|
+
key: K
|
|
98
|
+
label: string
|
|
99
|
+
icon: string
|
|
100
|
+
count?: number
|
|
101
|
+
enabled: boolean
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const DEFAULT_ANNOTATION_ICON_LIMIT: ChartAnnotationIconLimit = 3
|
|
105
|
+
|
|
106
|
+
export function annotationIconPreview(
|
|
107
|
+
items: readonly ChartAnnotation[],
|
|
108
|
+
maxIcons: ChartAnnotationIconLimit = DEFAULT_ANNOTATION_ICON_LIMIT,
|
|
109
|
+
): { visible: readonly ChartAnnotation[], overflowCount: number } {
|
|
110
|
+
return {
|
|
111
|
+
visible: items.slice(0, maxIcons),
|
|
112
|
+
overflowCount: Math.max(0, items.length - maxIcons),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Semantic tone → marker tailwind background class (line + dot). */
|
|
117
|
+
export const ANNOTATION_TONE_CLASS: Record<NonNullable<ChartAnnotation['tone']>, string> = {
|
|
118
|
+
success: 'bg-success',
|
|
119
|
+
error: 'bg-error',
|
|
120
|
+
warning: 'bg-warning',
|
|
121
|
+
neutral: 'bg-accented',
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function toMs(v: Date | string | number): number {
|
|
125
|
+
if (v instanceof Date)
|
|
126
|
+
return v.getTime()
|
|
127
|
+
if (typeof v === 'number')
|
|
128
|
+
return v
|
|
129
|
+
return new Date(v).getTime()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Normalise any x value to a `YYYY-MM-DD` day key.
|
|
134
|
+
*
|
|
135
|
+
* A `YYYY-MM-DD…` string keeps its own day verbatim (no Date round-trip, so no
|
|
136
|
+
* UTC shift); a `Date`/epoch-ms is keyed by its **UTC** day.
|
|
137
|
+
*
|
|
138
|
+
* TIMEZONE NOTE (ADR-0082): chart x-domains here are GSC reporting days
|
|
139
|
+
* (Pacific) rendered as UTC-midnight Dates, and an annotation's `x` is typically
|
|
140
|
+
* an event instant (`occurredAt`, a UTC Date). Marker placement and the tooltip
|
|
141
|
+
* match BOTH key off that UTC day, so the tooltip always matches the day the
|
|
142
|
+
* marker is drawn on — they stay mutually consistent. The only artefact: a
|
|
143
|
+
* detection-time event late in the Pacific evening keys one calendar day ahead
|
|
144
|
+
* of the viewer's local day, and its marker sits on that same (next) day.
|
|
145
|
+
*/
|
|
146
|
+
export function toChartDayKey(v: Date | string | number): string | null {
|
|
147
|
+
if (typeof v === 'string' && /^\d{4}-\d{2}-\d{2}/.test(v))
|
|
148
|
+
return v.slice(0, 10)
|
|
149
|
+
// A non-date x value (a category axis, "Mobile") has no day. `toISOString`
|
|
150
|
+
// throws a RangeError on an Invalid Date, which failed the whole chart render.
|
|
151
|
+
const ms = toMs(v)
|
|
152
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString().slice(0, 10) : null
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Annotations whose day matches the hovered x value (same `YYYY-MM-DD`). */
|
|
156
|
+
export function annotationsOnDay<T extends { x: Date | string | number }>(
|
|
157
|
+
annotations: readonly T[],
|
|
158
|
+
hoverX: Date | string | number | null | undefined,
|
|
159
|
+
): T[] {
|
|
160
|
+
if (hoverX == null || !annotations.length)
|
|
161
|
+
return []
|
|
162
|
+
const day = toChartDayKey(hoverX)
|
|
163
|
+
if (!day)
|
|
164
|
+
return []
|
|
165
|
+
return annotations.filter(a => toChartDayKey(a.x) === day)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The cheap diagnose line for a marker tooltip: "N days later, <metric> +X%".
|
|
170
|
+
* Computed from the series already on screen (3-day means around the marker
|
|
171
|
+
* day and `days` later) — the render-time half of the ROADMAP "tether" idea,
|
|
172
|
+
* with zero backend. Returns undefined when the series doesn't extend far
|
|
173
|
+
* enough past the marker or the baseline is zero.
|
|
174
|
+
*/
|
|
175
|
+
export function annotationImpactLine(
|
|
176
|
+
series: readonly { date: string, value: number }[],
|
|
177
|
+
markerX: Date | string | number,
|
|
178
|
+
metricLabel: string,
|
|
179
|
+
days = 7,
|
|
180
|
+
): string | undefined {
|
|
181
|
+
if (series.length < days + 1)
|
|
182
|
+
return undefined
|
|
183
|
+
const day = toChartDayKey(markerX)
|
|
184
|
+
if (!day)
|
|
185
|
+
return undefined
|
|
186
|
+
const anchor = series.findIndex((row) => {
|
|
187
|
+
const rowDay = toChartDayKey(row.date)
|
|
188
|
+
return rowDay != null && rowDay >= day
|
|
189
|
+
})
|
|
190
|
+
if (anchor < 1 || anchor + days >= series.length)
|
|
191
|
+
return undefined
|
|
192
|
+
const mean = (end: number) => {
|
|
193
|
+
const slice = series.slice(Math.max(0, end - 2), end + 1)
|
|
194
|
+
return slice.reduce((sum, row) => sum + row.value, 0) / slice.length
|
|
195
|
+
}
|
|
196
|
+
// Baseline ends the day BEFORE the marker — the marker day itself already
|
|
197
|
+
// carries the event's effect and would dilute the delta.
|
|
198
|
+
const before = mean(anchor - 1)
|
|
199
|
+
const after = mean(anchor + days)
|
|
200
|
+
if (before <= 0)
|
|
201
|
+
return undefined
|
|
202
|
+
const pct = Math.round(((after - before) / before) * 100)
|
|
203
|
+
if (!Number.isFinite(pct))
|
|
204
|
+
return undefined
|
|
205
|
+
return `${days} days later, ${metricLabel} ${pct >= 0 ? '+' : ''}${pct}%`
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The CSS `left` percentage for an annotation within a zero-horizontal-margin
|
|
210
|
+
* plot (wrap width == plot x-span). Clamped to [0,100]. Returns '50%' for a
|
|
211
|
+
* degenerate domain.
|
|
212
|
+
*/
|
|
213
|
+
export function annotationLeftPct(x: Date | string | number, domain: [Date | string | number, Date | string | number]): string {
|
|
214
|
+
const xMs = toMs(x)
|
|
215
|
+
const minMs = toMs(domain[0])
|
|
216
|
+
const maxMs = toMs(domain[1])
|
|
217
|
+
if (maxMs <= minMs)
|
|
218
|
+
return '50%'
|
|
219
|
+
const pct = Math.max(0, Math.min(100, ((xMs - minMs) / (maxMs - minMs)) * 100))
|
|
220
|
+
return `${pct.toFixed(4)}%`
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Re-anchor annotations onto an INDEX-based x-axis (`x = (_d, i) => i`, e.g. the
|
|
225
|
+
* CWV + indexing charts, whose bars are evenly spaced by array index rather than
|
|
226
|
+
* by time). Maps each annotation's day to the data index sharing that day and
|
|
227
|
+
* drops annotations with no matching data point. Pair with `xDomain = [0, len-1]`
|
|
228
|
+
* so `UiChartAnnotations` positions markers on the right bar.
|
|
229
|
+
*/
|
|
230
|
+
export function indexAnnotationsByDay(
|
|
231
|
+
annotations: readonly ChartAnnotation[] | undefined,
|
|
232
|
+
days: readonly (string | null)[],
|
|
233
|
+
): ChartAnnotation[] {
|
|
234
|
+
if (!annotations?.length || !days.length)
|
|
235
|
+
return []
|
|
236
|
+
const dayToIndex = new Map<string, number>()
|
|
237
|
+
days.forEach((d, i) => {
|
|
238
|
+
const key = d == null ? null : toChartDayKey(d)
|
|
239
|
+
if (key && !dayToIndex.has(key))
|
|
240
|
+
dayToIndex.set(key, i)
|
|
241
|
+
})
|
|
242
|
+
return annotations.flatMap((a) => {
|
|
243
|
+
const key = toChartDayKey(a.x)
|
|
244
|
+
const i = key ? dayToIndex.get(key) : undefined
|
|
245
|
+
return key == null || i == null ? [] : [{ ...a, x: i, dayKey: key }]
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface ResolvedAnnotationMarker {
|
|
250
|
+
id: number
|
|
251
|
+
label: string
|
|
252
|
+
description?: string
|
|
253
|
+
leftPct: string
|
|
254
|
+
toneClass: string
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Resolve annotations to positioned markers; [] when no domain (silently skipped). */
|
|
258
|
+
export interface ResolvedAnnotationMarkerGroup {
|
|
259
|
+
/** The shared day key (`YYYY-MM-DD`) — one marker per day. */
|
|
260
|
+
id: string
|
|
261
|
+
leftPct: string
|
|
262
|
+
/** Dominant tone across the day's annotations (error > warning > success > neutral). */
|
|
263
|
+
toneClass: string
|
|
264
|
+
count: number
|
|
265
|
+
/** True when any of the day's annotations is user-editable. */
|
|
266
|
+
editable: boolean
|
|
267
|
+
items: ChartAnnotation[]
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const TONE_RANK: Record<NonNullable<ChartAnnotation['tone']>, number> = {
|
|
271
|
+
error: 3,
|
|
272
|
+
warning: 2,
|
|
273
|
+
success: 1,
|
|
274
|
+
neutral: 0,
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Day-grouped markers: annotations sharing a calendar day collapse into ONE
|
|
279
|
+
* marker (a "+N" count pill signals the stack; the tooltip lists every item)
|
|
280
|
+
* instead of piling identical lines on the same pixel column.
|
|
281
|
+
*/
|
|
282
|
+
export function resolveAnnotationMarkerGroups(
|
|
283
|
+
annotations: readonly ChartAnnotation[] | undefined,
|
|
284
|
+
xDomain: [Date | string | number, Date | string | number] | undefined,
|
|
285
|
+
): ResolvedAnnotationMarkerGroup[] {
|
|
286
|
+
if (!annotations?.length || !xDomain)
|
|
287
|
+
return []
|
|
288
|
+
const byDay = new Map<string, ChartAnnotation[]>()
|
|
289
|
+
for (const ann of annotations) {
|
|
290
|
+
// An index-anchored annotation always carries its dayKey. Without one, a
|
|
291
|
+
// non-date x still renders as its own marker, keyed by its raw value.
|
|
292
|
+
const day = ann.dayKey ?? toChartDayKey(ann.x) ?? String(ann.x)
|
|
293
|
+
const bucket = byDay.get(day)
|
|
294
|
+
if (bucket)
|
|
295
|
+
bucket.push(ann)
|
|
296
|
+
else
|
|
297
|
+
byDay.set(day, [ann])
|
|
298
|
+
}
|
|
299
|
+
return Array.from(byDay.entries(), ([day, items]) => {
|
|
300
|
+
const tone = items.reduce<NonNullable<ChartAnnotation['tone']>>((acc, item) =>
|
|
301
|
+
TONE_RANK[item.tone ?? 'neutral'] > TONE_RANK[acc] ? (item.tone ?? 'neutral') : acc, 'neutral')
|
|
302
|
+
return {
|
|
303
|
+
id: day,
|
|
304
|
+
leftPct: annotationLeftPct(items[0]!.x, xDomain),
|
|
305
|
+
toneClass: ANNOTATION_TONE_CLASS[tone],
|
|
306
|
+
count: items.length,
|
|
307
|
+
editable: items.some(item => item.editable === true),
|
|
308
|
+
items,
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function resolveAnnotationMarkers(
|
|
314
|
+
annotations: readonly ChartAnnotation[] | undefined,
|
|
315
|
+
xDomain: [Date | string | number, Date | string | number] | undefined,
|
|
316
|
+
): ResolvedAnnotationMarker[] {
|
|
317
|
+
if (!annotations?.length || !xDomain)
|
|
318
|
+
return []
|
|
319
|
+
return annotations.map((ann, i) => ({
|
|
320
|
+
id: i,
|
|
321
|
+
label: ann.label,
|
|
322
|
+
...(ann.description ? { description: ann.description } : {}),
|
|
323
|
+
leftPct: annotationLeftPct(ann.x, xDomain),
|
|
324
|
+
toneClass: ANNOTATION_TONE_CLASS[ann.tone ?? 'neutral'],
|
|
325
|
+
}))
|
|
326
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import type { UiIcon } from '../shared/ui-icons'
|
|
2
|
+
import { DISPLAY_LOCALE } from '../composables/formatting'
|
|
3
|
+
|
|
4
|
+
// ISO 3166-1 alpha-3 to alpha-2 and country names
|
|
5
|
+
export const countryData: Record<string, { alpha2: string, name: string }> = {
|
|
6
|
+
deu: { alpha2: 'de', name: 'Germany' },
|
|
7
|
+
fra: { alpha2: 'fr', name: 'France' },
|
|
8
|
+
ind: { alpha2: 'in', name: 'India' },
|
|
9
|
+
usa: { alpha2: 'us', name: 'United States' },
|
|
10
|
+
gbr: { alpha2: 'gb', name: 'United Kingdom' },
|
|
11
|
+
nld: { alpha2: 'nl', name: 'Netherlands' },
|
|
12
|
+
pol: { alpha2: 'pl', name: 'Poland' },
|
|
13
|
+
rus: { alpha2: 'ru', name: 'Russia' },
|
|
14
|
+
esp: { alpha2: 'es', name: 'Spain' },
|
|
15
|
+
ita: { alpha2: 'it', name: 'Italy' },
|
|
16
|
+
bra: { alpha2: 'br', name: 'Brazil' },
|
|
17
|
+
can: { alpha2: 'ca', name: 'Canada' },
|
|
18
|
+
aus: { alpha2: 'au', name: 'Australia' },
|
|
19
|
+
jpn: { alpha2: 'jp', name: 'Japan' },
|
|
20
|
+
chn: { alpha2: 'cn', name: 'China' },
|
|
21
|
+
kor: { alpha2: 'kr', name: 'South Korea' },
|
|
22
|
+
mex: { alpha2: 'mx', name: 'Mexico' },
|
|
23
|
+
arg: { alpha2: 'ar', name: 'Argentina' },
|
|
24
|
+
tur: { alpha2: 'tr', name: 'Turkey' },
|
|
25
|
+
sau: { alpha2: 'sa', name: 'Saudi Arabia' },
|
|
26
|
+
zaf: { alpha2: 'za', name: 'South Africa' },
|
|
27
|
+
egy: { alpha2: 'eg', name: 'Egypt' },
|
|
28
|
+
nga: { alpha2: 'ng', name: 'Nigeria' },
|
|
29
|
+
ken: { alpha2: 'ke', name: 'Kenya' },
|
|
30
|
+
idn: { alpha2: 'id', name: 'Indonesia' },
|
|
31
|
+
tha: { alpha2: 'th', name: 'Thailand' },
|
|
32
|
+
vnm: { alpha2: 'vn', name: 'Vietnam' },
|
|
33
|
+
phl: { alpha2: 'ph', name: 'Philippines' },
|
|
34
|
+
mys: { alpha2: 'my', name: 'Malaysia' },
|
|
35
|
+
sgp: { alpha2: 'sg', name: 'Singapore' },
|
|
36
|
+
pak: { alpha2: 'pk', name: 'Pakistan' },
|
|
37
|
+
bgd: { alpha2: 'bd', name: 'Bangladesh' },
|
|
38
|
+
ukr: { alpha2: 'ua', name: 'Ukraine' },
|
|
39
|
+
rou: { alpha2: 'ro', name: 'Romania' },
|
|
40
|
+
cze: { alpha2: 'cz', name: 'Czechia' },
|
|
41
|
+
hun: { alpha2: 'hu', name: 'Hungary' },
|
|
42
|
+
aut: { alpha2: 'at', name: 'Austria' },
|
|
43
|
+
che: { alpha2: 'ch', name: 'Switzerland' },
|
|
44
|
+
bel: { alpha2: 'be', name: 'Belgium' },
|
|
45
|
+
swe: { alpha2: 'se', name: 'Sweden' },
|
|
46
|
+
nor: { alpha2: 'no', name: 'Norway' },
|
|
47
|
+
dnk: { alpha2: 'dk', name: 'Denmark' },
|
|
48
|
+
fin: { alpha2: 'fi', name: 'Finland' },
|
|
49
|
+
prt: { alpha2: 'pt', name: 'Portugal' },
|
|
50
|
+
grc: { alpha2: 'gr', name: 'Greece' },
|
|
51
|
+
irl: { alpha2: 'ie', name: 'Ireland' },
|
|
52
|
+
nzl: { alpha2: 'nz', name: 'New Zealand' },
|
|
53
|
+
isr: { alpha2: 'il', name: 'Israel' },
|
|
54
|
+
are: { alpha2: 'ae', name: 'UAE' },
|
|
55
|
+
col: { alpha2: 'co', name: 'Colombia' },
|
|
56
|
+
chl: { alpha2: 'cl', name: 'Chile' },
|
|
57
|
+
per: { alpha2: 'pe', name: 'Peru' },
|
|
58
|
+
ven: { alpha2: 've', name: 'Venezuela' },
|
|
59
|
+
irn: { alpha2: 'ir', name: 'Iran' },
|
|
60
|
+
irq: { alpha2: 'iq', name: 'Iraq' },
|
|
61
|
+
twn: { alpha2: 'tw', name: 'Taiwan' },
|
|
62
|
+
hkg: { alpha2: 'hk', name: 'Hong Kong' },
|
|
63
|
+
mac: { alpha2: 'mo', name: 'Macau' },
|
|
64
|
+
svk: { alpha2: 'sk', name: 'Slovakia' },
|
|
65
|
+
bgr: { alpha2: 'bg', name: 'Bulgaria' },
|
|
66
|
+
hrv: { alpha2: 'hr', name: 'Croatia' },
|
|
67
|
+
srb: { alpha2: 'rs', name: 'Serbia' },
|
|
68
|
+
svn: { alpha2: 'si', name: 'Slovenia' },
|
|
69
|
+
ltu: { alpha2: 'lt', name: 'Lithuania' },
|
|
70
|
+
lva: { alpha2: 'lv', name: 'Latvia' },
|
|
71
|
+
est: { alpha2: 'ee', name: 'Estonia' },
|
|
72
|
+
blr: { alpha2: 'by', name: 'Belarus' },
|
|
73
|
+
kaz: { alpha2: 'kz', name: 'Kazakhstan' },
|
|
74
|
+
uzb: { alpha2: 'uz', name: 'Uzbekistan' },
|
|
75
|
+
aze: { alpha2: 'az', name: 'Azerbaijan' },
|
|
76
|
+
geo: { alpha2: 'ge', name: 'Georgia' },
|
|
77
|
+
arm: { alpha2: 'am', name: 'Armenia' },
|
|
78
|
+
// Remaining ISO 3166-1 countries + common GSC-reported territories. GSC emits
|
|
79
|
+
// alpha-3 (lowercase) for every country, so an incomplete map left the long
|
|
80
|
+
// tail rendering as raw codes (`lbn`, `isl`) with a globe instead of a flag.
|
|
81
|
+
afg: { alpha2: 'af', name: 'Afghanistan' },
|
|
82
|
+
alb: { alpha2: 'al', name: 'Albania' },
|
|
83
|
+
dza: { alpha2: 'dz', name: 'Algeria' },
|
|
84
|
+
and: { alpha2: 'ad', name: 'Andorra' },
|
|
85
|
+
ago: { alpha2: 'ao', name: 'Angola' },
|
|
86
|
+
atg: { alpha2: 'ag', name: 'Antigua and Barbuda' },
|
|
87
|
+
bhs: { alpha2: 'bs', name: 'Bahamas' },
|
|
88
|
+
bhr: { alpha2: 'bh', name: 'Bahrain' },
|
|
89
|
+
brb: { alpha2: 'bb', name: 'Barbados' },
|
|
90
|
+
ben: { alpha2: 'bj', name: 'Benin' },
|
|
91
|
+
btn: { alpha2: 'bt', name: 'Bhutan' },
|
|
92
|
+
bol: { alpha2: 'bo', name: 'Bolivia' },
|
|
93
|
+
bih: { alpha2: 'ba', name: 'Bosnia and Herzegovina' },
|
|
94
|
+
bwa: { alpha2: 'bw', name: 'Botswana' },
|
|
95
|
+
brn: { alpha2: 'bn', name: 'Brunei' },
|
|
96
|
+
bfa: { alpha2: 'bf', name: 'Burkina Faso' },
|
|
97
|
+
bdi: { alpha2: 'bi', name: 'Burundi' },
|
|
98
|
+
khm: { alpha2: 'kh', name: 'Cambodia' },
|
|
99
|
+
cmr: { alpha2: 'cm', name: 'Cameroon' },
|
|
100
|
+
cpv: { alpha2: 'cv', name: 'Cape Verde' },
|
|
101
|
+
caf: { alpha2: 'cf', name: 'Central African Republic' },
|
|
102
|
+
tcd: { alpha2: 'td', name: 'Chad' },
|
|
103
|
+
com: { alpha2: 'km', name: 'Comoros' },
|
|
104
|
+
cog: { alpha2: 'cg', name: 'Congo' },
|
|
105
|
+
cod: { alpha2: 'cd', name: 'DR Congo' },
|
|
106
|
+
cri: { alpha2: 'cr', name: 'Costa Rica' },
|
|
107
|
+
civ: { alpha2: 'ci', name: 'Côte d\'Ivoire' },
|
|
108
|
+
cub: { alpha2: 'cu', name: 'Cuba' },
|
|
109
|
+
cyp: { alpha2: 'cy', name: 'Cyprus' },
|
|
110
|
+
dji: { alpha2: 'dj', name: 'Djibouti' },
|
|
111
|
+
dma: { alpha2: 'dm', name: 'Dominica' },
|
|
112
|
+
dom: { alpha2: 'do', name: 'Dominican Republic' },
|
|
113
|
+
ecu: { alpha2: 'ec', name: 'Ecuador' },
|
|
114
|
+
slv: { alpha2: 'sv', name: 'El Salvador' },
|
|
115
|
+
gnq: { alpha2: 'gq', name: 'Equatorial Guinea' },
|
|
116
|
+
eri: { alpha2: 'er', name: 'Eritrea' },
|
|
117
|
+
swz: { alpha2: 'sz', name: 'Eswatini' },
|
|
118
|
+
eth: { alpha2: 'et', name: 'Ethiopia' },
|
|
119
|
+
fji: { alpha2: 'fj', name: 'Fiji' },
|
|
120
|
+
gab: { alpha2: 'ga', name: 'Gabon' },
|
|
121
|
+
gmb: { alpha2: 'gm', name: 'Gambia' },
|
|
122
|
+
gha: { alpha2: 'gh', name: 'Ghana' },
|
|
123
|
+
gib: { alpha2: 'gi', name: 'Gibraltar' },
|
|
124
|
+
grd: { alpha2: 'gd', name: 'Grenada' },
|
|
125
|
+
gtm: { alpha2: 'gt', name: 'Guatemala' },
|
|
126
|
+
gin: { alpha2: 'gn', name: 'Guinea' },
|
|
127
|
+
gnb: { alpha2: 'gw', name: 'Guinea-Bissau' },
|
|
128
|
+
guy: { alpha2: 'gy', name: 'Guyana' },
|
|
129
|
+
hti: { alpha2: 'ht', name: 'Haiti' },
|
|
130
|
+
hnd: { alpha2: 'hn', name: 'Honduras' },
|
|
131
|
+
isl: { alpha2: 'is', name: 'Iceland' },
|
|
132
|
+
jam: { alpha2: 'jm', name: 'Jamaica' },
|
|
133
|
+
jor: { alpha2: 'jo', name: 'Jordan' },
|
|
134
|
+
kwt: { alpha2: 'kw', name: 'Kuwait' },
|
|
135
|
+
kgz: { alpha2: 'kg', name: 'Kyrgyzstan' },
|
|
136
|
+
lao: { alpha2: 'la', name: 'Laos' },
|
|
137
|
+
lbn: { alpha2: 'lb', name: 'Lebanon' },
|
|
138
|
+
lso: { alpha2: 'ls', name: 'Lesotho' },
|
|
139
|
+
lbr: { alpha2: 'lr', name: 'Liberia' },
|
|
140
|
+
lby: { alpha2: 'ly', name: 'Libya' },
|
|
141
|
+
lie: { alpha2: 'li', name: 'Liechtenstein' },
|
|
142
|
+
lux: { alpha2: 'lu', name: 'Luxembourg' },
|
|
143
|
+
mkd: { alpha2: 'mk', name: 'North Macedonia' },
|
|
144
|
+
mdg: { alpha2: 'mg', name: 'Madagascar' },
|
|
145
|
+
mwi: { alpha2: 'mw', name: 'Malawi' },
|
|
146
|
+
mdv: { alpha2: 'mv', name: 'Maldives' },
|
|
147
|
+
mli: { alpha2: 'ml', name: 'Mali' },
|
|
148
|
+
mlt: { alpha2: 'mt', name: 'Malta' },
|
|
149
|
+
mrt: { alpha2: 'mr', name: 'Mauritania' },
|
|
150
|
+
mus: { alpha2: 'mu', name: 'Mauritius' },
|
|
151
|
+
mda: { alpha2: 'md', name: 'Moldova' },
|
|
152
|
+
mco: { alpha2: 'mc', name: 'Monaco' },
|
|
153
|
+
mng: { alpha2: 'mn', name: 'Mongolia' },
|
|
154
|
+
mne: { alpha2: 'me', name: 'Montenegro' },
|
|
155
|
+
mar: { alpha2: 'ma', name: 'Morocco' },
|
|
156
|
+
moz: { alpha2: 'mz', name: 'Mozambique' },
|
|
157
|
+
mmr: { alpha2: 'mm', name: 'Myanmar' },
|
|
158
|
+
nam: { alpha2: 'na', name: 'Namibia' },
|
|
159
|
+
npl: { alpha2: 'np', name: 'Nepal' },
|
|
160
|
+
nic: { alpha2: 'ni', name: 'Nicaragua' },
|
|
161
|
+
ner: { alpha2: 'ne', name: 'Niger' },
|
|
162
|
+
omn: { alpha2: 'om', name: 'Oman' },
|
|
163
|
+
pan: { alpha2: 'pa', name: 'Panama' },
|
|
164
|
+
png: { alpha2: 'pg', name: 'Papua New Guinea' },
|
|
165
|
+
pry: { alpha2: 'py', name: 'Paraguay' },
|
|
166
|
+
pse: { alpha2: 'ps', name: 'Palestine' },
|
|
167
|
+
pri: { alpha2: 'pr', name: 'Puerto Rico' },
|
|
168
|
+
qat: { alpha2: 'qa', name: 'Qatar' },
|
|
169
|
+
rwa: { alpha2: 'rw', name: 'Rwanda' },
|
|
170
|
+
kna: { alpha2: 'kn', name: 'Saint Kitts and Nevis' },
|
|
171
|
+
lca: { alpha2: 'lc', name: 'Saint Lucia' },
|
|
172
|
+
vct: { alpha2: 'vc', name: 'Saint Vincent and the Grenadines' },
|
|
173
|
+
wsm: { alpha2: 'ws', name: 'Samoa' },
|
|
174
|
+
smr: { alpha2: 'sm', name: 'San Marino' },
|
|
175
|
+
stp: { alpha2: 'st', name: 'São Tomé and Príncipe' },
|
|
176
|
+
sen: { alpha2: 'sn', name: 'Senegal' },
|
|
177
|
+
syc: { alpha2: 'sc', name: 'Seychelles' },
|
|
178
|
+
sle: { alpha2: 'sl', name: 'Sierra Leone' },
|
|
179
|
+
slb: { alpha2: 'sb', name: 'Solomon Islands' },
|
|
180
|
+
som: { alpha2: 'so', name: 'Somalia' },
|
|
181
|
+
ssd: { alpha2: 'ss', name: 'South Sudan' },
|
|
182
|
+
lka: { alpha2: 'lk', name: 'Sri Lanka' },
|
|
183
|
+
sdn: { alpha2: 'sd', name: 'Sudan' },
|
|
184
|
+
sur: { alpha2: 'sr', name: 'Suriname' },
|
|
185
|
+
syr: { alpha2: 'sy', name: 'Syria' },
|
|
186
|
+
tjk: { alpha2: 'tj', name: 'Tajikistan' },
|
|
187
|
+
tza: { alpha2: 'tz', name: 'Tanzania' },
|
|
188
|
+
tls: { alpha2: 'tl', name: 'Timor-Leste' },
|
|
189
|
+
tgo: { alpha2: 'tg', name: 'Togo' },
|
|
190
|
+
ton: { alpha2: 'to', name: 'Tonga' },
|
|
191
|
+
tto: { alpha2: 'tt', name: 'Trinidad and Tobago' },
|
|
192
|
+
tun: { alpha2: 'tn', name: 'Tunisia' },
|
|
193
|
+
tkm: { alpha2: 'tm', name: 'Turkmenistan' },
|
|
194
|
+
uga: { alpha2: 'ug', name: 'Uganda' },
|
|
195
|
+
ury: { alpha2: 'uy', name: 'Uruguay' },
|
|
196
|
+
vut: { alpha2: 'vu', name: 'Vanuatu' },
|
|
197
|
+
yem: { alpha2: 'ye', name: 'Yemen' },
|
|
198
|
+
zmb: { alpha2: 'zm', name: 'Zambia' },
|
|
199
|
+
zwe: { alpha2: 'zw', name: 'Zimbabwe' },
|
|
200
|
+
// Dependencies, overseas territories and the remaining sovereign states the
|
|
201
|
+
// list above skipped. GSC reports on the full ISO 3166-1 set, so anything
|
|
202
|
+
// absent here rendered as a raw id (`reu`, `blz`) beside a globe. `name` is
|
|
203
|
+
// the offline fallback only — `getCountryName` prefers `Intl.DisplayNames`,
|
|
204
|
+
// and these English strings were generated FROM that same CLDR data so the
|
|
205
|
+
// two agree. Every alpha-2 below has a `i-circle-flags-*` glyph (checked
|
|
206
|
+
// against @iconify-json/circle-flags).
|
|
207
|
+
abw: { alpha2: 'aw', name: 'Aruba' },
|
|
208
|
+
aia: { alpha2: 'ai', name: 'Anguilla' },
|
|
209
|
+
ala: { alpha2: 'ax', name: 'Åland Islands' },
|
|
210
|
+
asm: { alpha2: 'as', name: 'American Samoa' },
|
|
211
|
+
ata: { alpha2: 'aq', name: 'Antarctica' },
|
|
212
|
+
atf: { alpha2: 'tf', name: 'French Southern Territories' },
|
|
213
|
+
bes: { alpha2: 'bq', name: 'Caribbean Netherlands' },
|
|
214
|
+
blm: { alpha2: 'bl', name: 'St. Barthélemy' },
|
|
215
|
+
blz: { alpha2: 'bz', name: 'Belize' },
|
|
216
|
+
bmu: { alpha2: 'bm', name: 'Bermuda' },
|
|
217
|
+
bvt: { alpha2: 'bv', name: 'Bouvet Island' },
|
|
218
|
+
cck: { alpha2: 'cc', name: 'Cocos (Keeling) Islands' },
|
|
219
|
+
cok: { alpha2: 'ck', name: 'Cook Islands' },
|
|
220
|
+
cuw: { alpha2: 'cw', name: 'Curaçao' },
|
|
221
|
+
cxr: { alpha2: 'cx', name: 'Christmas Island' },
|
|
222
|
+
cym: { alpha2: 'ky', name: 'Cayman Islands' },
|
|
223
|
+
esh: { alpha2: 'eh', name: 'Western Sahara' },
|
|
224
|
+
flk: { alpha2: 'fk', name: 'Falkland Islands' },
|
|
225
|
+
fro: { alpha2: 'fo', name: 'Faroe Islands' },
|
|
226
|
+
fsm: { alpha2: 'fm', name: 'Micronesia' },
|
|
227
|
+
ggy: { alpha2: 'gg', name: 'Guernsey' },
|
|
228
|
+
glp: { alpha2: 'gp', name: 'Guadeloupe' },
|
|
229
|
+
grl: { alpha2: 'gl', name: 'Greenland' },
|
|
230
|
+
guf: { alpha2: 'gf', name: 'French Guiana' },
|
|
231
|
+
gum: { alpha2: 'gu', name: 'Guam' },
|
|
232
|
+
hmd: { alpha2: 'hm', name: 'Heard & McDonald Islands' },
|
|
233
|
+
imn: { alpha2: 'im', name: 'Isle of Man' },
|
|
234
|
+
iot: { alpha2: 'io', name: 'British Indian Ocean Territory' },
|
|
235
|
+
jey: { alpha2: 'je', name: 'Jersey' },
|
|
236
|
+
kir: { alpha2: 'ki', name: 'Kiribati' },
|
|
237
|
+
maf: { alpha2: 'mf', name: 'St. Martin' },
|
|
238
|
+
mhl: { alpha2: 'mh', name: 'Marshall Islands' },
|
|
239
|
+
mnp: { alpha2: 'mp', name: 'Northern Mariana Islands' },
|
|
240
|
+
msr: { alpha2: 'ms', name: 'Montserrat' },
|
|
241
|
+
mtq: { alpha2: 'mq', name: 'Martinique' },
|
|
242
|
+
myt: { alpha2: 'yt', name: 'Mayotte' },
|
|
243
|
+
ncl: { alpha2: 'nc', name: 'New Caledonia' },
|
|
244
|
+
nfk: { alpha2: 'nf', name: 'Norfolk Island' },
|
|
245
|
+
niu: { alpha2: 'nu', name: 'Niue' },
|
|
246
|
+
nru: { alpha2: 'nr', name: 'Nauru' },
|
|
247
|
+
pcn: { alpha2: 'pn', name: 'Pitcairn Islands' },
|
|
248
|
+
plw: { alpha2: 'pw', name: 'Palau' },
|
|
249
|
+
prk: { alpha2: 'kp', name: 'North Korea' },
|
|
250
|
+
pyf: { alpha2: 'pf', name: 'French Polynesia' },
|
|
251
|
+
reu: { alpha2: 're', name: 'Réunion' },
|
|
252
|
+
sgs: { alpha2: 'gs', name: 'South Georgia & South Sandwich Islands' },
|
|
253
|
+
shn: { alpha2: 'sh', name: 'St. Helena' },
|
|
254
|
+
sjm: { alpha2: 'sj', name: 'Svalbard & Jan Mayen' },
|
|
255
|
+
spm: { alpha2: 'pm', name: 'St. Pierre & Miquelon' },
|
|
256
|
+
sxm: { alpha2: 'sx', name: 'Sint Maarten' },
|
|
257
|
+
tca: { alpha2: 'tc', name: 'Turks & Caicos Islands' },
|
|
258
|
+
tkl: { alpha2: 'tk', name: 'Tokelau' },
|
|
259
|
+
tuv: { alpha2: 'tv', name: 'Tuvalu' },
|
|
260
|
+
umi: { alpha2: 'um', name: 'U.S. Outlying Islands' },
|
|
261
|
+
vat: { alpha2: 'va', name: 'Vatican City' },
|
|
262
|
+
vgb: { alpha2: 'vg', name: 'British Virgin Islands' },
|
|
263
|
+
vir: { alpha2: 'vi', name: 'U.S. Virgin Islands' },
|
|
264
|
+
wlf: { alpha2: 'wf', name: 'Wallis & Futuna' },
|
|
265
|
+
// Kosovo has no ISO 3166-1 assignment; GSC emits the user-assigned `xkk`,
|
|
266
|
+
// which resolves to the `xk` region CLDR and Iconify both carry. Kept to the
|
|
267
|
+
// one spelling GSC actually sends — a second alias would render as a
|
|
268
|
+
// duplicate "Kosovo" row in every dropdown built from `Object.values`.
|
|
269
|
+
xkk: { alpha2: 'xk', name: 'Kosovo' },
|
|
270
|
+
// GSC's sentinel for traffic it cannot attribute to a region. It appears in
|
|
271
|
+
// essentially every property and is a real category, not an error — so it
|
|
272
|
+
// gets a name, and `UNKNOWN_REGION_CODES` keeps it off the flag path.
|
|
273
|
+
zzz: { alpha2: 'zz', name: 'Unknown region' },
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Codes that name "we don't know", in every spelling a caller might pass. */
|
|
277
|
+
const UNKNOWN_REGION_CODES = new Set(['zz', 'zzz'])
|
|
278
|
+
|
|
279
|
+
// Convert country code to circle-flags icon name
|
|
280
|
+
export function countryCodeToFlagIcon(code: string): UiIcon {
|
|
281
|
+
if (!code)
|
|
282
|
+
return 'globe'
|
|
283
|
+
const lowerCode = code.toLowerCase()
|
|
284
|
+
// There is no `zz` flag (nor should there be) — the globe IS the answer.
|
|
285
|
+
if (UNKNOWN_REGION_CODES.has(lowerCode))
|
|
286
|
+
return 'globe'
|
|
287
|
+
const data = countryData[lowerCode]
|
|
288
|
+
const alpha2 = data?.alpha2 || (lowerCode.length === 2 ? lowerCode : null)
|
|
289
|
+
if (!alpha2)
|
|
290
|
+
return 'globe'
|
|
291
|
+
return `i-circle-flags-${alpha2}`
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Localized region-name lookup. The bundled `name` field is English-only, so
|
|
295
|
+
// prefer `Intl.DisplayNames` in the pinned display locale (formatting.ts): the
|
|
296
|
+
// runtime locale differs between server and browser and broke hydration
|
|
297
|
+
// and fall back to the map → raw code when the runtime lacks region data.
|
|
298
|
+
let regionDisplay: Intl.DisplayNames | null | undefined
|
|
299
|
+
function getRegionDisplay(): Intl.DisplayNames | null {
|
|
300
|
+
if (regionDisplay !== undefined)
|
|
301
|
+
return regionDisplay
|
|
302
|
+
try {
|
|
303
|
+
regionDisplay = new Intl.DisplayNames(DISPLAY_LOCALE, { type: 'region' })
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
regionDisplay = null // runtime without region display data
|
|
307
|
+
}
|
|
308
|
+
return regionDisplay
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Get country display name from code
|
|
312
|
+
export function getCountryName(code: string, fallback?: string): string {
|
|
313
|
+
if (!code)
|
|
314
|
+
return fallback || 'Unknown'
|
|
315
|
+
const lowerCode = code.toLowerCase()
|
|
316
|
+
// Ahead of the Intl lookup on purpose: CLDR titles this "Unknown Region",
|
|
317
|
+
// which reads as a proper noun in a list of real ones.
|
|
318
|
+
if (UNKNOWN_REGION_CODES.has(lowerCode))
|
|
319
|
+
return 'Unknown region'
|
|
320
|
+
const data = countryData[lowerCode]
|
|
321
|
+
// MUST be uppercased before `.of()`. V8 does not canonicalise the region
|
|
322
|
+
// subtag: `.of('us')` returns the string `'us'` while `.of('US')` returns
|
|
323
|
+
// "United States". `countryData` stores alpha-2 lowercase (it feeds the
|
|
324
|
+
// `i-circle-flags-*` name), so passing it straight through made every
|
|
325
|
+
// mapped country render as its own lowercase code.
|
|
326
|
+
const alpha2 = (data?.alpha2 || (code.length === 2 ? code : '')).toUpperCase() || null
|
|
327
|
+
if (alpha2) {
|
|
328
|
+
// `.of()` echoes the input back when it can't resolve the region.
|
|
329
|
+
const localized = getRegionDisplay()?.of(alpha2)
|
|
330
|
+
if (localized && localized.toUpperCase() !== alpha2)
|
|
331
|
+
return localized
|
|
332
|
+
}
|
|
333
|
+
return data?.name || fallback || code.toUpperCase()
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Shorter aliases used at call sites.
|
|
337
|
+
export { countryCodeToFlagIcon as countryFlag, getCountryName as countryName }
|