@nuxtseo/design-system 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -2
- package/app/app.config.ts +596 -0
- package/app/component-attributes.d.ts +8 -0
- package/app/components/README.md +38 -0
- package/app/components/brand/UiBrandMark.vue +63 -0
- package/app/components/brand/UiModuleIcon.vue +45 -0
- package/app/components/brand/UiProLettering.vue +26 -0
- package/app/components/brand/UiRangeTerrain.vue +311 -0
- package/app/components/brand/range-glyphs.ts +18 -0
- package/app/components/container/BrowserTab.vue +145 -0
- package/app/components/container/IntegrationRow.vue +120 -0
- package/app/components/container/KnowDoRails.vue +40 -0
- package/app/components/container/RailPanel.vue +93 -0
- package/app/components/container/UiAlert.vue +120 -0
- package/app/components/container/UiAppShell.vue +165 -0
- package/app/components/container/UiCard.vue +204 -0
- package/app/components/container/UiDisclosure.vue +43 -0
- package/app/components/container/UiEmptyState.vue +102 -0
- package/app/components/container/UiErrorState.vue +61 -0
- package/app/components/container/UiLoadingState.vue +20 -0
- package/app/components/container/UiPageHeader.vue +95 -0
- package/app/components/container/UiPopover.vue +102 -0
- package/app/components/container/UiSampleDataOverlay.vue +47 -0
- package/app/components/container/UiSectionHeader.vue +83 -0
- package/app/components/container/UiSummaryStrip.vue +40 -0
- package/app/components/container/UiToast.vue +230 -0
- package/app/components/container/UiToaster.vue +271 -0
- package/app/components/container/UiWidgetState.vue +143 -0
- package/app/components/container/ui-nav-drawer.ts +48 -0
- package/app/components/data/ActionRail.vue +152 -0
- package/app/components/data/FacetStrip.vue +93 -0
- package/app/components/data/UiBarCell.vue +37 -0
- package/app/components/data/UiBarChart.vue +281 -0
- package/app/components/data/UiChartAnnotationControls.vue +74 -0
- package/app/components/data/UiChartAnnotations.vue +163 -0
- package/app/components/data/UiChartBrushOverlay.vue +96 -0
- package/app/components/data/UiChartDataReveal.vue +81 -0
- package/app/components/data/UiChartFrame.vue +123 -0
- package/app/components/data/UiChartTooltip.vue +81 -0
- package/app/components/data/UiCoverageNote.vue +90 -0
- package/app/components/data/UiCwvSparkline.vue +188 -0
- package/app/components/data/UiDataList.vue +276 -0
- package/app/components/data/UiDataTableSection.vue +232 -0
- package/app/components/data/UiDistributionBars.vue +164 -0
- package/app/components/data/UiDonutChart.vue +132 -0
- package/app/components/data/UiDoorwayColumn.vue +108 -0
- package/app/components/data/UiDoorwayGrid.vue +58 -0
- package/app/components/data/UiDoorwayRow.vue +83 -0
- package/app/components/data/UiEntitySummary.vue +261 -0
- package/app/components/data/UiFactsGrid.vue +107 -0
- package/app/components/data/UiImpactMeter.vue +33 -0
- package/app/components/data/UiIssueRow.vue +149 -0
- package/app/components/data/UiLineChart.vue +431 -0
- package/app/components/data/UiMetricBarPopover.vue +121 -0
- package/app/components/data/UiMetricLabel.vue +30 -0
- package/app/components/data/UiMetricStat.vue +73 -0
- package/app/components/data/UiMetricToggle.vue +156 -0
- package/app/components/data/UiMetricsRow.vue +64 -0
- package/app/components/data/UiPositionMetric.vue +53 -0
- package/app/components/data/UiRelatedFixes.vue +113 -0
- package/app/components/data/UiRichText.vue +325 -0
- package/app/components/data/UiScatterPlot.vue +144 -0
- package/app/components/data/UiSeverityMarker.vue +30 -0
- package/app/components/data/UiSparkColumns.vue +160 -0
- package/app/components/data/UiSparkline.vue +681 -0
- package/app/components/data/UiStat.vue +445 -0
- package/app/components/data/UiStats.vue +76 -0
- package/app/components/data/UiTable.vue +517 -0
- package/app/components/data/UiTableExport.vue +40 -0
- package/app/components/data/UiTableFrame.vue +149 -0
- package/app/components/data/UiTablePagination.vue +37 -0
- package/app/components/data/UiTableShell.vue +48 -0
- package/app/components/data/UiTableTd.vue +39 -0
- package/app/components/data/UiTableTh.vue +61 -0
- package/app/components/data/UiTopEntityStackChart.vue +393 -0
- package/app/components/data/cells/UiTableDash.vue +3 -0
- package/app/components/data/cells/UiTableHeaderCell.vue +72 -0
- package/app/components/data/cells/UiTableMetricCell.vue +72 -0
- package/app/components/data/cells/UiTablePathCell.vue +76 -0
- package/app/components/data/cells/UiTableScoreTile.vue +49 -0
- package/app/components/data/cells/UiTableTrendCell.vue +41 -0
- package/app/components/element/UiAtmosphere.vue +64 -0
- package/app/components/element/UiAuthHeading.vue +24 -0
- package/app/components/element/UiAuthProviders.vue +46 -0
- package/app/components/element/UiBrandIcon.vue +39 -0
- package/app/components/element/UiButton.vue +549 -0
- package/app/components/element/UiCelebration.vue +65 -0
- package/app/components/element/UiChannelChips.vue +62 -0
- package/app/components/element/UiChatQuickLink.vue +319 -0
- package/app/components/element/UiChip.vue +101 -0
- package/app/components/element/UiConfirmDialog.vue +84 -0
- package/app/components/element/UiCopyButton.vue +90 -0
- package/app/components/element/UiDitherAvatar.vue +177 -0
- package/app/components/element/UiDotLabel.vue +19 -0
- package/app/components/element/UiEjectMenu.vue +181 -0
- package/app/components/element/UiEjectOverlay.vue +29 -0
- package/app/components/element/UiFavicon.vue +235 -0
- package/app/components/element/UiFaviconStack.vue +84 -0
- package/app/components/element/UiFilterMenu.vue +324 -0
- package/app/components/element/UiFocusedSurface.vue +129 -0
- package/app/components/element/UiHealthDot.vue +32 -0
- package/app/components/element/UiHelpLabel.vue +28 -0
- package/app/components/element/UiIcon.vue +22 -0
- package/app/components/element/UiInput.vue +35 -0
- package/app/components/element/UiLogoPlausible.vue +12 -0
- package/app/components/element/UiLogoPro.vue +25 -0
- package/app/components/element/UiLogoUmami.vue +12 -0
- package/app/components/element/UiNavIcon.vue +107 -0
- package/app/components/element/UiNavList.vue +156 -0
- package/app/components/element/UiOnboardingAnswers.vue +125 -0
- package/app/components/element/UiOverflowCount.vue +31 -0
- package/app/components/element/UiProBadge.vue +19 -0
- package/app/components/element/UiProgressCircle.vue +79 -0
- package/app/components/element/UiProgressPercent.vue +35 -0
- package/app/components/element/UiRelativeTime.vue +58 -0
- package/app/components/element/UiScrollFade.vue +16 -0
- package/app/components/element/UiSelect.vue +55 -0
- package/app/components/element/UiSeverityDot.vue +27 -0
- package/app/components/element/UiSiteFavicon.vue +24 -0
- package/app/components/element/UiSkeleton.vue +148 -0
- package/app/components/element/UiSlotText.vue +65 -0
- package/app/components/element/UiSourceLogos.vue +113 -0
- package/app/components/element/UiSourceRail.vue +71 -0
- package/app/components/element/UiStaticTerminal.vue +107 -0
- package/app/components/element/UiStatusBadge.vue +62 -0
- package/app/components/element/UiSyncDot.vue +46 -0
- package/app/components/element/UiTabs.vue +245 -0
- package/app/components/element/UiTogglePill.vue +86 -0
- package/app/components/element/UiTooltip.vue +390 -0
- package/app/components/element/UiTooltipGrid.vue +457 -0
- package/app/components/element/UiTrend.vue +184 -0
- package/app/components/element/UiTrialBenefits.vue +71 -0
- package/app/components/element/UiUrlLabel.vue +75 -0
- package/app/components/element/UiValenceDot.vue +37 -0
- package/app/components/element/UiWizardModal.vue +284 -0
- package/app/components/element/UiWizardNav.vue +59 -0
- package/app/components/element/UiWizardProgress.vue +78 -0
- package/app/components/element/UiWizardShell.vue +43 -0
- package/app/components/element/eject-menu.ts +359 -0
- package/app/components/element/eject-tracker.ts +34 -0
- package/app/components/element/onboarding-answers.ts +68 -0
- package/app/components/element/onboarding-intents.ts +19 -0
- package/app/composables/dataVizColors.ts +350 -0
- package/app/composables/formatting.ts +252 -0
- package/app/composables/semanticColors.ts +80 -0
- package/app/composables/useActivityOverlay.ts +55 -0
- package/app/composables/useAppShellNav.ts +21 -0
- package/app/composables/useChartBrush.ts +221 -0
- package/app/composables/useChartHover.ts +153 -0
- package/app/composables/useChartTickPlan.ts +186 -0
- package/app/composables/useCloseOnNavigation.ts +7 -0
- package/app/composables/useConfirm.ts +38 -0
- package/app/composables/useEdgeFade.ts +37 -0
- package/app/composables/useEject.ts +69 -0
- package/app/composables/useEntranceMotion.ts +22 -0
- package/app/composables/useFaviconBacking.ts +164 -0
- package/app/composables/useGridCols.ts +39 -0
- package/app/composables/useIssueOverlay.ts +43 -0
- package/app/composables/useOnboardingService.ts +106 -0
- package/app/composables/useOpenedOnce.ts +19 -0
- package/app/composables/useScrollSpy.ts +178 -0
- package/app/composables/useSquircleFallback.ts +21 -0
- package/app/composables/useWizardRoute.ts +123 -0
- package/app/plugins/favicon.ts +29 -0
- package/app/plugins/lazy-motion.ts +22 -0
- package/app/plugins/selection-highlight.client.ts +176 -0
- package/app/shared/cli.ts +10 -0
- package/app/shared/error-state.ts +150 -0
- package/app/shared/icons.ts +1 -0
- package/app/shared/motion.ts +1 -0
- package/app/shared/nav.ts +38 -0
- package/app/shared/navigation.ts +5 -0
- package/app/shared/static-terminal.ts +14 -0
- package/app/shared/table-export.ts +36 -0
- package/app/shared/table.ts +105 -0
- package/app/shared/ui-icons.ts +1 -0
- package/app/shared/view-state.ts +85 -0
- package/app/shared/widget-failure.ts +126 -0
- package/app/utils/chartAnnotations.ts +326 -0
- package/app/utils/countries.ts +337 -0
- package/app/utils/export-download.ts +100 -0
- package/app/utils/favicon-fallback.ts +12 -0
- package/app/utils/icon-color.ts +105 -0
- package/app/utils/number.ts +74 -0
- package/app/utils/render-link.ts +14 -0
- package/app/utils/slot-text.ts +18 -0
- package/app/utils/squircle.ts +154 -0
- package/app/utils/topEntityStack.ts +197 -0
- package/app/utils/vue.ts +57 -0
- package/build/design-system.ts +47 -0
- package/build/icon-server-bundle.ts +276 -0
- package/build/runtime/icon-server-bundle.ts +29 -0
- package/css/global.css +1892 -0
- package/nuxt.config.ts +132 -0
- package/package.json +86 -3
- package/public/brand-icon-dark.png +0 -0
- package/public/brand-icon-light.png +0 -0
- package/public/favicon-dark.png +0 -0
- package/public/favicon-light.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/fonts/README.md +31 -0
- package/public/fonts/fira-code-OFL.txt +93 -0
- package/public/fonts/fira-code-var-latin.woff2 +0 -0
- package/public/fonts/hubot-sans-OFL.txt +93 -0
- package/public/fonts/hubot-sans-var-latin.woff2 +0 -0
- package/public/icon-dark.png +0 -0
- package/public/icon.png +0 -0
- package/public/touch-icon-dark.png +0 -0
- package/public/touch-icon.png +0 -0
- package/server/api/favicon.get.ts +122 -0
- package/server/utils/favicon-source.ts +51 -0
- package/shared/contracts/feedback.ts +42 -0
- package/shared/contracts/index.ts +1 -0
- package/shared/delivery-channels.ts +53 -0
- package/shared/icons/index.ts +143 -0
- package/shared/icons/registry.ts +347 -0
- package/shared/icons/ui-slots.ts +58 -0
- package/shared/markdown.ts +44 -0
- package/shared/motion.ts +120 -0
- package/shared/rangi.ts +60 -0
- package/shared/rich-text.ts +34 -0
- package/shared/ui-icons.ts +6 -0
- package/shared/utils/urls.ts +64 -0
- package/types/index.ts +1 -0
- package/types/ui.ts +339 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { VNodeChild } from 'vue'
|
|
3
|
+
import type { EjectChat, EjectLink, EjectMcp } from '../element/eject-menu'
|
|
4
|
+
import { useElementSize } from '@vueuse/core'
|
|
5
|
+
import { computed, useTemplateRef } from 'vue'
|
|
6
|
+
import { UiEjectMenu } from '#components'
|
|
7
|
+
import { useSquircleFallback } from '../../composables/useSquircleFallback'
|
|
8
|
+
import { squircleSvgPath } from '../../utils/squircle'
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
size = 'md',
|
|
12
|
+
emphasis = false,
|
|
13
|
+
flush = false,
|
|
14
|
+
titleTag = 'h3',
|
|
15
|
+
chat,
|
|
16
|
+
mcp,
|
|
17
|
+
schema,
|
|
18
|
+
} = defineProps<{
|
|
19
|
+
title?: string
|
|
20
|
+
/** Match the card title to its page section depth. */
|
|
21
|
+
titleTag?: 'h2' | 'h3' | 'h4'
|
|
22
|
+
description?: string
|
|
23
|
+
divided?: boolean
|
|
24
|
+
/** 'default' = solid bg-elevated (forms, settings). 'subtle' = translucent bg (data displays, lists). */
|
|
25
|
+
variant?: 'default' | 'subtle'
|
|
26
|
+
/** 'xs' = tight (inline/nested). 'sm' = compact (lists, dense tables). 'md' = default. 'lg' = spacious (hero/feature cards). */
|
|
27
|
+
size?: 'xs' | 'sm' | 'md' | 'lg'
|
|
28
|
+
/**
|
|
29
|
+
* The single sanctioned in-page contrast move: lift + primary-tinted top
|
|
30
|
+
* bevel (--elevation-emphasis) that survives the dashboard's raised→flat
|
|
31
|
+
* remap. Budget: at most ONE per page — two emphasis cards means neither is.
|
|
32
|
+
*/
|
|
33
|
+
emphasis?: boolean
|
|
34
|
+
/**
|
|
35
|
+
* Drop the body padding so rows sit flush to the card edges and their
|
|
36
|
+
* dividers span the full width. The rows own their inset instead — match the
|
|
37
|
+
* header with `px-4 sm:px-(--density-card-padding)` so the two line up.
|
|
38
|
+
*/
|
|
39
|
+
flush?: boolean
|
|
40
|
+
/**
|
|
41
|
+
* Card-grain eject affordance: renders a `<UiEjectMenu>` in the header so the
|
|
42
|
+
* card's own data can be opened in AI chat (or copied as code) without a
|
|
43
|
+
* floating page-level button. The card title supplies the context that a
|
|
44
|
+
* generic "Ask AI" button lacked, so the seed reads as scoped to this card.
|
|
45
|
+
* Pass `chat` with `entity` + `filter` (siteId) + `autoSend: true` to open the
|
|
46
|
+
* in-place ProChatPanel and fire the seed immediately. Only renders in the
|
|
47
|
+
* auto-header (title/actions driven); cards using a custom `#header` slot place
|
|
48
|
+
* the menu themselves.
|
|
49
|
+
*/
|
|
50
|
+
chat?: EjectChat
|
|
51
|
+
mcp?: EjectMcp
|
|
52
|
+
schema?: EjectLink
|
|
53
|
+
}>()
|
|
54
|
+
|
|
55
|
+
const slots = defineSlots<{
|
|
56
|
+
default?: () => VNodeChild
|
|
57
|
+
header?: () => VNodeChild
|
|
58
|
+
actions?: () => VNodeChild
|
|
59
|
+
}>()
|
|
60
|
+
|
|
61
|
+
const hasEject = computed(() => !!(chat || mcp || schema))
|
|
62
|
+
|
|
63
|
+
// The default (md) size reads the --density-card-padding knob at sm+ so card
|
|
64
|
+
// density tunes centrally per context (:root / .dashboard-theme). The explicit
|
|
65
|
+
// sizes (xs/sm/lg) are local intent and stay fixed.
|
|
66
|
+
const headerClass = {
|
|
67
|
+
xs: 'px-2.5 py-2',
|
|
68
|
+
sm: 'px-3 sm:px-4 py-3',
|
|
69
|
+
md: 'px-4 sm:px-(--density-card-padding) py-4',
|
|
70
|
+
lg: 'px-6 sm:px-8 py-5',
|
|
71
|
+
}[size]
|
|
72
|
+
|
|
73
|
+
const bodyClass = {
|
|
74
|
+
xs: 'p-2.5',
|
|
75
|
+
sm: 'p-3 sm:p-4',
|
|
76
|
+
md: 'p-4 sm:p-(--density-card-padding)',
|
|
77
|
+
lg: 'p-6 sm:p-8',
|
|
78
|
+
}[size]
|
|
79
|
+
|
|
80
|
+
const bodyDividedClass = {
|
|
81
|
+
xs: 'divide-y divide-[var(--ui-border)] [&>*]:p-2.5',
|
|
82
|
+
sm: 'divide-y divide-[var(--ui-border)] [&>*]:p-3 [&>*]:sm:p-4',
|
|
83
|
+
md: 'divide-y divide-[var(--ui-border)] [&>*]:p-4 [&>*]:sm:p-(--density-card-padding)',
|
|
84
|
+
lg: 'divide-y divide-[var(--ui-border)] [&>*]:p-6 [&>*]:sm:p-8',
|
|
85
|
+
}[size]
|
|
86
|
+
|
|
87
|
+
// Size stays driven by the card's `size` prop (not the theme-resizing role
|
|
88
|
+
// tokens); only the weight routes through the .font-strong token so it tunes
|
|
89
|
+
// centrally with the rest of the type system.
|
|
90
|
+
const titleClass = {
|
|
91
|
+
xs: 'text-xs font-strong text-default',
|
|
92
|
+
sm: 'text-sm font-strong text-default',
|
|
93
|
+
md: 'font-strong text-default',
|
|
94
|
+
lg: 'text-lg font-strong text-default',
|
|
95
|
+
}[size]
|
|
96
|
+
|
|
97
|
+
// Figma-squircle corners. Chrome gets the native `corner-shape: squircle`
|
|
98
|
+
// (global.css), so `rounded-xl` (0.75rem ≈ 12px) is both the SSR shape and the
|
|
99
|
+
// final shape — no JS, no morph. Safari/Firefox fall back to a generated
|
|
100
|
+
// clip-path (clips bg + content to the squircle) plus an SVG hairline overlay
|
|
101
|
+
// whose stroke is `var(--ui-border)`, so it follows the curve and tracks the
|
|
102
|
+
// theme for free. The CSS border is hidden in that case so the clip can't
|
|
103
|
+
// slice it.
|
|
104
|
+
const RADIUS = 12
|
|
105
|
+
const cardRef = useTemplateRef<HTMLElement>('card')
|
|
106
|
+
const { width, height } = useElementSize(cardRef)
|
|
107
|
+
const { enabled: squircle } = useSquircleFallback()
|
|
108
|
+
|
|
109
|
+
const path = computed(() => squircle.value
|
|
110
|
+
? squircleSvgPath(width.value, height.value, { radius: RADIUS, smoothing: 0.6 })
|
|
111
|
+
: '')
|
|
112
|
+
// Emphasis shadow rides the inner card. In squircle-fallback browsers the
|
|
113
|
+
// clip-path swallows the ambient drop shadow (inset bevel survives the clip) —
|
|
114
|
+
// acceptable degradation; Chrome renders the full preset.
|
|
115
|
+
const cardStyle = computed(() => {
|
|
116
|
+
const style: Record<string, string> = {}
|
|
117
|
+
if (path.value) {
|
|
118
|
+
style.clipPath = `path("${path.value}")`
|
|
119
|
+
style.borderColor = 'transparent'
|
|
120
|
+
}
|
|
121
|
+
if (emphasis)
|
|
122
|
+
style.boxShadow = 'var(--elevation-emphasis)'
|
|
123
|
+
return Object.keys(style).length ? style : undefined
|
|
124
|
+
})
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<template>
|
|
128
|
+
<div class="relative flex flex-col">
|
|
129
|
+
<div
|
|
130
|
+
ref="card"
|
|
131
|
+
data-ui-card
|
|
132
|
+
class="group/card relative overflow-hidden rounded-xl border flex flex-col flex-1"
|
|
133
|
+
:class="[
|
|
134
|
+
emphasis ? 'border-primary-400/25' : 'border-default',
|
|
135
|
+
emphasis
|
|
136
|
+
? 'bg-[var(--ui-bg-elevated)]/50'
|
|
137
|
+
: variant === 'subtle' ? 'bg-[var(--ui-bg-elevated)]/5' : 'bg-[var(--ui-bg-elevated)]/35',
|
|
138
|
+
]"
|
|
139
|
+
:style="cardStyle"
|
|
140
|
+
>
|
|
141
|
+
<!-- Header slot -->
|
|
142
|
+
<div v-if="slots.header" class="relative border-b border-default shrink-0" :class="headerClass">
|
|
143
|
+
<slot name="header" />
|
|
144
|
+
</div>
|
|
145
|
+
<!-- Auto header from title/description -->
|
|
146
|
+
<div v-else-if="title || slots.actions || hasEject" class="relative border-b border-default shrink-0 flex flex-col items-stretch gap-3 sm:flex-row sm:items-start sm:justify-between" :class="headerClass">
|
|
147
|
+
<div class="min-w-0">
|
|
148
|
+
<component :is="titleTag" v-if="title" class="break-words" :class="[titleClass]">
|
|
149
|
+
{{ title }}
|
|
150
|
+
</component>
|
|
151
|
+
<p v-if="description" class="text-sm text-muted mt-1">
|
|
152
|
+
{{ description }}
|
|
153
|
+
</p>
|
|
154
|
+
</div>
|
|
155
|
+
<div v-if="slots.actions || hasEject" class="flex flex-wrap items-center justify-end gap-1 sm:shrink-0">
|
|
156
|
+
<slot name="actions" />
|
|
157
|
+
<UiEjectMenu
|
|
158
|
+
v-if="hasEject"
|
|
159
|
+
:chat="chat"
|
|
160
|
+
:mcp="mcp"
|
|
161
|
+
:schema="schema"
|
|
162
|
+
class="-mr-1"
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<!-- A custom #header owns its own actions row, so the eject can't sit in
|
|
168
|
+
it. When chat/eject props are set alongside #header, surface a
|
|
169
|
+
hover-revealed ⋯ in the card's top-right corner instead (same affordance
|
|
170
|
+
as UiEjectOverlay). Always-visible on touch / for keyboard users. -->
|
|
171
|
+
<div
|
|
172
|
+
v-if="hasEject && slots.header"
|
|
173
|
+
class="absolute top-2.5 right-2.5 z-10 opacity-0 transition-opacity group-hover/card:opacity-100 focus-within:opacity-100 [@media(hover:none)]:opacity-100"
|
|
174
|
+
>
|
|
175
|
+
<UiEjectMenu :chat="chat" :mcp="mcp" :schema="schema" />
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div
|
|
179
|
+
data-card-body class="relative flex-1 flex flex-col"
|
|
180
|
+
:class="[flush ? '' : divided ? bodyDividedClass : bodyClass]"
|
|
181
|
+
>
|
|
182
|
+
<slot />
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<!-- Squircle hairline border (fallback browsers only). Stroke reads the
|
|
187
|
+
live token so it tracks the theme; non-scaling so it stays 1px. -->
|
|
188
|
+
<svg
|
|
189
|
+
v-if="path"
|
|
190
|
+
class="pointer-events-none absolute inset-0 size-full"
|
|
191
|
+
:viewBox="`0 0 ${width} ${height}`"
|
|
192
|
+
preserveAspectRatio="none"
|
|
193
|
+
aria-hidden="true"
|
|
194
|
+
>
|
|
195
|
+
<path
|
|
196
|
+
:d="path"
|
|
197
|
+
fill="none"
|
|
198
|
+
stroke-width="1"
|
|
199
|
+
vector-effect="non-scaling-stroke"
|
|
200
|
+
style="stroke: var(--ui-border)"
|
|
201
|
+
/>
|
|
202
|
+
</svg>
|
|
203
|
+
</div>
|
|
204
|
+
</template>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { UiChip, UiIcon } from '#components'
|
|
3
|
+
// The sanctioned overflow destination for tertiary content (DESIGN.md
|
|
4
|
+
// Composition rules): a quiet native <details> row that keeps detail off the
|
|
5
|
+
// page until asked for. Native element = free keyboard/AT semantics and
|
|
6
|
+
// explicit body visibility keeps modal focus inside its visible controls.
|
|
7
|
+
//
|
|
8
|
+
// Expand/collapse animates via `.ui-disclosure::details-content` +
|
|
9
|
+
// interpolate-size in global.css (Chromium); Firefox/Safari toggle instantly.
|
|
10
|
+
// Reduced-motion disables the transition in the global block.
|
|
11
|
+
//
|
|
12
|
+
// Deliberately icon-light: the chevron is the only glyph. No leading icon
|
|
13
|
+
// container, no eyebrow — the label is a noun + count, not a CTA.
|
|
14
|
+
|
|
15
|
+
const { label, count } = defineProps<{
|
|
16
|
+
/** Quiet row label — a noun beats a verb ("Sitemaps", not "Show sitemaps"). */
|
|
17
|
+
label: string
|
|
18
|
+
/** Optional count so the collapsed row still reports scale. */
|
|
19
|
+
count?: number | string
|
|
20
|
+
}>()
|
|
21
|
+
|
|
22
|
+
const open = defineModel<boolean>('open', { default: false })
|
|
23
|
+
|
|
24
|
+
function onToggle(e: Event) {
|
|
25
|
+
open.value = (e as ToggleEvent).newState === 'open'
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<details class="ui-disclosure group" :open="open" @toggle="onToggle">
|
|
31
|
+
<summary class="flex items-center gap-2 cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden min-h-11 sm:min-h-0 py-1 -mx-1 px-1 rounded-md focus-visible:ring-2 focus-visible:ring-primary outline-none">
|
|
32
|
+
<UiIcon name="chevron-right" class="size-3.5 text-dimmed transition-transform duration-150 group-open:rotate-90" aria-hidden="true" />
|
|
33
|
+
<span class="text-sm font-strong text-muted group-hover:text-default transition-colors">{{ label }}</span>
|
|
34
|
+
<UiChip v-if="count !== undefined" purpose="count">
|
|
35
|
+
{{ count }}
|
|
36
|
+
</UiChip>
|
|
37
|
+
<slot name="actions" />
|
|
38
|
+
</summary>
|
|
39
|
+
<div v-show="open" class="pt-3">
|
|
40
|
+
<slot />
|
|
41
|
+
</div>
|
|
42
|
+
</details>
|
|
43
|
+
</template>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { UiIcon as UiIconName } from '../../shared/ui-icons'
|
|
3
|
+
import { m } from 'motion-v'
|
|
4
|
+
import { computed } from 'vue'
|
|
5
|
+
import { UiAtmosphere, UiIcon } from '#components'
|
|
6
|
+
import { useEntranceMotion } from '../../composables/useEntranceMotion'
|
|
7
|
+
import { entrancePresets } from '../../shared/motion'
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
compact,
|
|
11
|
+
wide,
|
|
12
|
+
headingTag,
|
|
13
|
+
animated = true,
|
|
14
|
+
} = defineProps<{
|
|
15
|
+
icon: UiIconName
|
|
16
|
+
title: string
|
|
17
|
+
description?: string
|
|
18
|
+
/**
|
|
19
|
+
* Where the data comes from once the feature runs (e.g. "From Search Console,
|
|
20
|
+
* refreshed daily."). Full variant only; ignored when `compact`. Renders as a
|
|
21
|
+
* small dimmed line under the description. First-use anatomy: title sells the
|
|
22
|
+
* value, description says why or how, source names the data, one CTA in the slot.
|
|
23
|
+
*/
|
|
24
|
+
source?: string
|
|
25
|
+
compact?: boolean
|
|
26
|
+
/**
|
|
27
|
+
* Widen the default slot for a short setup form. The slot spans the full
|
|
28
|
+
* width up to `max-w-2xl` and its text is left-aligned, so fields do not
|
|
29
|
+
* inherit the centering. The submit CTA stays left-aligned under the last
|
|
30
|
+
* field, where the eye ends after filling the form. Full variant only.
|
|
31
|
+
*/
|
|
32
|
+
wide?: boolean
|
|
33
|
+
/**
|
|
34
|
+
* Title element. Full variant defaults to `h3`. Compact defaults to `p`,
|
|
35
|
+
* because most compact states sit inside a card that already has a heading;
|
|
36
|
+
* pass a level when the compact state stands in for a section's content.
|
|
37
|
+
*/
|
|
38
|
+
headingTag?: 'h1' | 'h2' | 'h3'
|
|
39
|
+
/** Disable JS entrance motion on SSR-sensitive public routes. */
|
|
40
|
+
animated?: boolean
|
|
41
|
+
/**
|
|
42
|
+
* Opt-in greydient atmosphere for genuine pre-data / front-door empty states
|
|
43
|
+
* (a preset name). Full variant only — leave unset for in-dashboard empties so
|
|
44
|
+
* data surfaces stay silent. Renders behind the content via <UiAtmosphere>.
|
|
45
|
+
*/
|
|
46
|
+
atmosphere?: 'front-door' | 'error' | 'ai' | 'preview' | 'share'
|
|
47
|
+
}>()
|
|
48
|
+
|
|
49
|
+
const rootComponent = computed(() => animated ? m.div : 'div')
|
|
50
|
+
const titleTag = computed(() => headingTag ?? (compact ? 'p' : 'h3'))
|
|
51
|
+
const rootMotion = useEntranceMotion(entrancePresets.fadeUp, () => animated)
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<component
|
|
56
|
+
:is="rootComponent"
|
|
57
|
+
data-testid="empty-state"
|
|
58
|
+
:class="compact
|
|
59
|
+
? 'flex flex-col items-center justify-center h-[220px] rounded-lg border border-dashed border-default bg-muted'
|
|
60
|
+
: ['text-center px-4 py-10 sm:py-16 sm:min-h-[400px] flex flex-col items-center justify-center', atmosphere ? 'relative isolate overflow-hidden' : '']"
|
|
61
|
+
v-bind="rootMotion"
|
|
62
|
+
>
|
|
63
|
+
<!-- Compact: inline empty state for cards/lists -->
|
|
64
|
+
<template v-if="compact">
|
|
65
|
+
<UiIcon :name="icon" class="size-8 text-dimmed mb-2" aria-hidden="true" />
|
|
66
|
+
<component :is="titleTag" class="text-sm font-medium text-default">
|
|
67
|
+
{{ title }}
|
|
68
|
+
</component>
|
|
69
|
+
<p v-if="description" class="text-xs text-muted mt-1 max-w-sm text-center px-4">
|
|
70
|
+
{{ description }}
|
|
71
|
+
</p>
|
|
72
|
+
<div v-if="$slots.default" class="mt-3 [&_button]:min-h-11 [&_a]:min-h-11 sm:[&_button]:min-h-0 sm:[&_a]:min-h-0">
|
|
73
|
+
<slot />
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<!-- Full: centered page-level empty state -->
|
|
78
|
+
<template v-else>
|
|
79
|
+
<!-- z-index:-1 + isolate keeps it behind content without wrapping anything. -->
|
|
80
|
+
<UiAtmosphere v-if="atmosphere" :preset="atmosphere" style="z-index: -1" />
|
|
81
|
+
<div class="inline-flex items-center justify-center size-14 rounded-2xl bg-elevated border border-default mb-4">
|
|
82
|
+
<UiIcon :name="icon" class="size-7 text-muted" aria-hidden="true" />
|
|
83
|
+
</div>
|
|
84
|
+
<component :is="titleTag" class="text-lg font-medium text-default mb-1 max-w-md text-balance">
|
|
85
|
+
{{ title }}
|
|
86
|
+
</component>
|
|
87
|
+
<p v-if="description" class="text-sm text-muted max-w-md mx-auto leading-relaxed text-pretty">
|
|
88
|
+
{{ description }}
|
|
89
|
+
</p>
|
|
90
|
+
<p v-if="source" class="text-xs text-dimmed mt-2 max-w-md mx-auto text-pretty">
|
|
91
|
+
{{ source }}
|
|
92
|
+
</p>
|
|
93
|
+
<div
|
|
94
|
+
class="mt-6 [&_button]:min-h-11 [&_a]:min-h-11 sm:[&_button]:min-h-0 sm:[&_a]:min-h-0"
|
|
95
|
+
:class="wide && 'w-full max-w-2xl text-left'"
|
|
96
|
+
>
|
|
97
|
+
<slot />
|
|
98
|
+
</div>
|
|
99
|
+
<slot name="footer" />
|
|
100
|
+
</template>
|
|
101
|
+
</component>
|
|
102
|
+
</template>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ErrorStateSource } from '../../shared/error-state'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { UiAlert, UiButton } from '#components'
|
|
5
|
+
import { resolveErrorState } from '../../shared/error-state'
|
|
6
|
+
|
|
7
|
+
const { error } = defineProps<{
|
|
8
|
+
/**
|
|
9
|
+
* Why the region failed, and where the words come from.
|
|
10
|
+
*
|
|
11
|
+
* Build it with `wireError(caught)` for a caught value, `errorCopy(...)` for
|
|
12
|
+
* a sentence a person wrote, or `rateLimited(...)` for a parsed quota
|
|
13
|
+
* refusal. A caught error has no renderable string here: `wireError` types it
|
|
14
|
+
* `unknown`, so the template reads only its HTTP status code and states copy
|
|
15
|
+
* this component owns. The old `string | { message: string }` prop rendered
|
|
16
|
+
* the caught message, which quotes the request URL.
|
|
17
|
+
*/
|
|
18
|
+
error: ErrorStateSource | null
|
|
19
|
+
/** Optional retry handler. When set (and no #action slot), renders a "Try again" button. */
|
|
20
|
+
onRetry?: () => void
|
|
21
|
+
}>()
|
|
22
|
+
|
|
23
|
+
defineSlots<{
|
|
24
|
+
/** Replaces the retry button and the generic next-step line. */
|
|
25
|
+
action?: () => unknown
|
|
26
|
+
}>()
|
|
27
|
+
|
|
28
|
+
// `Date.now()` is read once per resolve, the same as the old inline reset
|
|
29
|
+
// maths. The reset hint counts down only when something else re-renders.
|
|
30
|
+
const view = computed(() => error ? resolveErrorState(error, Date.now()) : null)
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<UiAlert
|
|
35
|
+
v-if="view"
|
|
36
|
+
:status="view.tone"
|
|
37
|
+
:icon="view.icon"
|
|
38
|
+
:title="view.title"
|
|
39
|
+
class="mb-8"
|
|
40
|
+
>
|
|
41
|
+
<p v-if="view.detail" class="mt-1">
|
|
42
|
+
{{ view.detail }}
|
|
43
|
+
</p>
|
|
44
|
+
<p
|
|
45
|
+
v-if="view.nextStep && (view.nextStep._tag === 'timing' || !$slots.action)"
|
|
46
|
+
class="text-muted/80 mt-1"
|
|
47
|
+
>
|
|
48
|
+
{{ view.nextStep.text }}
|
|
49
|
+
</p>
|
|
50
|
+
<slot name="action" />
|
|
51
|
+
<UiButton
|
|
52
|
+
v-if="onRetry && view.retryable && !$slots.action"
|
|
53
|
+
purpose="secondary"
|
|
54
|
+
size="xs"
|
|
55
|
+
icon="refresh"
|
|
56
|
+
label="Try again"
|
|
57
|
+
class="mt-2"
|
|
58
|
+
@click="onRetry()"
|
|
59
|
+
/>
|
|
60
|
+
</UiAlert>
|
|
61
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { UiSkeleton } from '#components'
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
rows?: number
|
|
6
|
+
}>()
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<div class="space-y-3" role="status" aria-live="polite" aria-busy="true">
|
|
11
|
+
<span class="sr-only">Loading…</span>
|
|
12
|
+
<div v-for="i in rows ?? 3" :key="i" class="flex items-center gap-3">
|
|
13
|
+
<UiSkeleton type="circle" :base="32" :index="i" />
|
|
14
|
+
<div class="flex-1 space-y-1.5">
|
|
15
|
+
<UiSkeleton :base="100" :range="60" :index="i" />
|
|
16
|
+
<UiSkeleton :base="140" :range="80" :index="i + 10" />
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { UiIcon as UiIconName } from '../../shared/ui-icons'
|
|
3
|
+
import { NuxtLink, UiIcon } from '#components'
|
|
4
|
+
|
|
5
|
+
// Page header: context crumb + title + trailing actions row, with optional
|
|
6
|
+
// breadcrumb links above (admin detail pages). Extracted after six sibling
|
|
7
|
+
// pages converged on the same shape.
|
|
8
|
+
|
|
9
|
+
interface Crumb {
|
|
10
|
+
label: string
|
|
11
|
+
to?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// `description` is intentionally accepted-but-unrendered: page headers no longer
|
|
15
|
+
// show a subtitle. Kept in the type so existing callers don't break.
|
|
16
|
+
const { crumbs = [], title, icon, flush = false, border = true, stackActionsOnMobile = false } = defineProps<{
|
|
17
|
+
crumbs?: Crumb[]
|
|
18
|
+
title: string
|
|
19
|
+
icon?: UiIconName
|
|
20
|
+
description?: string
|
|
21
|
+
/** Skip pro-container (max-width + horizontal padding). Use in non-pro shells where padding is owned by the layout. */
|
|
22
|
+
flush?: boolean
|
|
23
|
+
/** Bottom hairline. Turn off for headers with a sub-nav tab strip beneath (the tabs own the separator). */
|
|
24
|
+
border?: boolean
|
|
25
|
+
/** Give dense detail-page actions their own row below the title on mobile. */
|
|
26
|
+
stackActionsOnMobile?: boolean
|
|
27
|
+
}>()
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<header :class="[flush ? '' : 'pro-container', border ? 'border-b border-default' : '']">
|
|
32
|
+
<div class="flex items-start gap-4 pt-2">
|
|
33
|
+
<div
|
|
34
|
+
class="flex items-start justify-between min-w-0 w-full gap-3"
|
|
35
|
+
:class="stackActionsOnMobile ? 'flex-col sm:flex-row' : 'flex-row'"
|
|
36
|
+
>
|
|
37
|
+
<div class="min-w-0 flex-1">
|
|
38
|
+
<nav v-if="crumbs.length" aria-label="Breadcrumb" class="flex items-center gap-2 text-xs text-dimmed mb-1">
|
|
39
|
+
<template v-for="(crumb, i) in crumbs" :key="crumb.to ?? crumb.label">
|
|
40
|
+
<NuxtLink v-if="crumb.to" :to="crumb.to" class="hover:text-default">
|
|
41
|
+
{{ crumb.label }}
|
|
42
|
+
</NuxtLink>
|
|
43
|
+
<span
|
|
44
|
+
v-else
|
|
45
|
+
class="text-muted truncate"
|
|
46
|
+
:aria-current="i === crumbs.length - 1 ? 'page' : undefined"
|
|
47
|
+
>
|
|
48
|
+
{{ crumb.label }}
|
|
49
|
+
</span>
|
|
50
|
+
<UiIcon
|
|
51
|
+
v-if="i < crumbs.length - 1"
|
|
52
|
+
name="chevron-right"
|
|
53
|
+
class="size-3"
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
/>
|
|
56
|
+
</template>
|
|
57
|
+
</nav>
|
|
58
|
+
<!-- Title row. `#leading` (the mobile nav button) sits before both
|
|
59
|
+
lines. `#crumb` is the context segment (site/group switcher):
|
|
60
|
+
from `sm` it shares the title's row at title scale ("Nuxt SEO /
|
|
61
|
+
Overview"); below `sm` the header stacks, with the crumb as a
|
|
62
|
+
small muted line above the title. The crumb component owns its
|
|
63
|
+
trailing slash (it may render nothing, e.g. while the site list
|
|
64
|
+
resolves, and a dangling separator reads broken). Kept outside
|
|
65
|
+
the h1 so the heading text stays clean. When the row overflows,
|
|
66
|
+
the crumb truncates first: the title barely shrinks until the
|
|
67
|
+
crumb reaches its minimum width. -->
|
|
68
|
+
<div class="flex min-w-0 items-center gap-2">
|
|
69
|
+
<slot name="leading" />
|
|
70
|
+
<div class="flex min-w-0 flex-1 flex-col sm:flex-row sm:items-center gap-x-2">
|
|
71
|
+
<div v-if="$slots.crumb" class="flex min-w-0 items-center gap-x-2 sm:shrink">
|
|
72
|
+
<slot name="crumb" />
|
|
73
|
+
</div>
|
|
74
|
+
<div class="flex min-w-0 items-center gap-2 sm:shrink-[0.001]">
|
|
75
|
+
<h1 class="text-title text-default flex items-center gap-2 min-w-0">
|
|
76
|
+
<slot name="icon">
|
|
77
|
+
<UiIcon v-if="icon" :name="icon" class="size-4 text-dimmed shrink-0" aria-hidden="true" />
|
|
78
|
+
</slot>
|
|
79
|
+
<span class="truncate">{{ title }}</span>
|
|
80
|
+
</h1>
|
|
81
|
+
<slot name="title-trailing" />
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<div
|
|
87
|
+
class="flex items-center gap-3 flex-wrap shrink-0"
|
|
88
|
+
:class="stackActionsOnMobile ? 'w-full justify-start sm:w-auto sm:justify-end' : 'justify-end'"
|
|
89
|
+
>
|
|
90
|
+
<slot name="actions" />
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</header>
|
|
95
|
+
</template>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { PopoverProps } from '@nuxt/ui'
|
|
3
|
+
import type { VNode } from 'vue'
|
|
4
|
+
import { computed, useSlots, watch } from 'vue'
|
|
5
|
+
import { UPopover } from '#components'
|
|
6
|
+
|
|
7
|
+
type UiPopoverProps = Omit<PopoverProps, 'open' | 'defaultOpen' | 'portal' | 'ui'> & {
|
|
8
|
+
/** Prevent opening while retaining the trigger in the page flow. */
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
/** The content remains portalled; callers may target a specific portal host. */
|
|
11
|
+
portal?: true | string | HTMLElement
|
|
12
|
+
/** Optional semantic role applied to the owned panel wrapper. */
|
|
13
|
+
role?: string
|
|
14
|
+
/** Stable id for associating the panel with its trigger. */
|
|
15
|
+
panelId?: string
|
|
16
|
+
ui?: PopoverProps['ui']
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const props = withDefaults(defineProps<UiPopoverProps>(), {
|
|
20
|
+
dismissible: true,
|
|
21
|
+
portal: true,
|
|
22
|
+
})
|
|
23
|
+
const emit = defineEmits<{
|
|
24
|
+
'close:prevent': []
|
|
25
|
+
}>()
|
|
26
|
+
defineSlots<{
|
|
27
|
+
default?: (props: { disabled: boolean, open: boolean }) => VNode[]
|
|
28
|
+
panel?: (props: { close?: () => void }) => VNode[]
|
|
29
|
+
anchor?: (props: { close?: () => void }) => VNode[]
|
|
30
|
+
}>()
|
|
31
|
+
|
|
32
|
+
const slots = useSlots()
|
|
33
|
+
const open = defineModel<boolean>('open', { default: false })
|
|
34
|
+
const guardedOpen = computed({
|
|
35
|
+
get: () => props.disabled ? false : open.value,
|
|
36
|
+
set: (nextOpen: boolean) => {
|
|
37
|
+
if (props.disabled && nextOpen)
|
|
38
|
+
return
|
|
39
|
+
open.value = nextOpen
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
watch(() => props.disabled, (disabled) => {
|
|
44
|
+
if (disabled)
|
|
45
|
+
open.value = false
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// Keep the application popover contract resilient by default. UPopover remains
|
|
49
|
+
// the accessible implementation detail; callers cannot disable its portal and
|
|
50
|
+
// every panel gets collision padding plus the shared overlay chrome.
|
|
51
|
+
const forwardedProps = computed(() => {
|
|
52
|
+
const { disabled: _disabled, panelId: _panelId, role: _role, ui: _ui, ...forwarded } = props
|
|
53
|
+
return {
|
|
54
|
+
...forwarded,
|
|
55
|
+
mode: props.mode ?? 'click',
|
|
56
|
+
openDelay: props.openDelay ?? 0,
|
|
57
|
+
closeDelay: props.closeDelay ?? 0,
|
|
58
|
+
dismissible: props.dismissible ?? true,
|
|
59
|
+
portal: props.portal ?? true,
|
|
60
|
+
content: {
|
|
61
|
+
side: 'bottom' as const,
|
|
62
|
+
sideOffset: 8,
|
|
63
|
+
collisionPadding: 8,
|
|
64
|
+
...props.content,
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const mergedUi = computed<PopoverProps['ui']>(() => {
|
|
70
|
+
const userContent = props.ui?.content
|
|
71
|
+
return {
|
|
72
|
+
...props.ui,
|
|
73
|
+
content: typeof userContent === 'function'
|
|
74
|
+
? (defaults: string) => ['ui-popover-content', userContent(defaults)]
|
|
75
|
+
: ['ui-popover-content', userContent],
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<template>
|
|
81
|
+
<UPopover
|
|
82
|
+
v-bind="forwardedProps"
|
|
83
|
+
v-model:open="guardedOpen"
|
|
84
|
+
:ui="mergedUi"
|
|
85
|
+
@close:prevent="emit('close:prevent')"
|
|
86
|
+
>
|
|
87
|
+
<template v-if="slots.default" #default="slotProps">
|
|
88
|
+
<slot v-bind="slotProps" :disabled="props.disabled ?? false" />
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<template v-if="slots.anchor" #anchor="slotProps">
|
|
92
|
+
<slot name="anchor" v-bind="slotProps" />
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<template #content="slotProps">
|
|
96
|
+
<div v-if="props.role || props.panelId" :id="props.panelId" data-ui="UiPopover" :role="props.role">
|
|
97
|
+
<slot name="panel" v-bind="slotProps" />
|
|
98
|
+
</div>
|
|
99
|
+
<slot v-else name="panel" v-bind="slotProps" />
|
|
100
|
+
</template>
|
|
101
|
+
</UPopover>
|
|
102
|
+
</template>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { UiAtmosphere, UiButton, UiIcon } from '#components'
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
message?: string
|
|
6
|
+
description?: string
|
|
7
|
+
ctaLabel?: string
|
|
8
|
+
ctaTo?: string
|
|
9
|
+
}>()
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<div class="relative">
|
|
14
|
+
<!-- Faded sample content. `inert` removes it from focus order AND the
|
|
15
|
+
accessibility tree together — `aria-hidden` alone leaves any focusable
|
|
16
|
+
children tabbable while hidden (a WCAG 4.1.2 violation). -->
|
|
17
|
+
<div class="opacity-25 select-none" inert>
|
|
18
|
+
<slot />
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<!-- Overlay -->
|
|
22
|
+
<div class="absolute inset-0 flex flex-col items-center justify-center bg-gradient-to-b from-[var(--ui-bg)]/70 via-[var(--ui-bg)]/95 to-[var(--ui-bg)]/75 backdrop-blur-[1px]">
|
|
23
|
+
<!-- The pre-data preview read as data glimpsed through fog. -->
|
|
24
|
+
<UiAtmosphere preset="preview" />
|
|
25
|
+
<div class="relative w-full max-w-md rounded-lg border border-default bg-default/90 px-5 py-4 text-center shadow-lg ring-1 ring-black/5 dark:ring-white/5">
|
|
26
|
+
<div class="mb-3 inline-flex size-10 items-center justify-center rounded-lg border border-default bg-elevated">
|
|
27
|
+
<UiIcon name="google" class="size-5 text-muted" aria-hidden="true" />
|
|
28
|
+
</div>
|
|
29
|
+
<p class="mb-1 text-sm font-semibold text-highlighted">
|
|
30
|
+
{{ message || 'Preview' }}
|
|
31
|
+
</p>
|
|
32
|
+
<p class="mx-auto mb-4 max-w-sm text-sm leading-relaxed text-default">
|
|
33
|
+
{{ description || 'Connect Google Search Console to see your real data.' }}
|
|
34
|
+
</p>
|
|
35
|
+
<UiButton
|
|
36
|
+
v-if="ctaTo"
|
|
37
|
+
:to="ctaTo"
|
|
38
|
+
size="sm"
|
|
39
|
+
purpose="secondary"
|
|
40
|
+
icon="plug"
|
|
41
|
+
>
|
|
42
|
+
{{ ctaLabel || 'Connect GSC' }}
|
|
43
|
+
</UiButton>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|