@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,132 @@
|
|
|
1
|
+
<script lang="ts" setup generic="T extends Record<string, unknown>">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { categoricalVizColors } from '../../composables/dataVizColors'
|
|
4
|
+
import { formatGroupedNumber } from '../../composables/formatting'
|
|
5
|
+
import { roundedShareLabel, roundedShares } from '../../utils/number'
|
|
6
|
+
|
|
7
|
+
// Donut/pie chart for composition — share of a whole (device split, brand vs non-brand,
|
|
8
|
+
// country share). SVG arc ring + a legend with value and percentage per slice. Generic +
|
|
9
|
+
// dependency-free, so any row array drives it.
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
data,
|
|
13
|
+
labelKey,
|
|
14
|
+
valueKey,
|
|
15
|
+
size = 132,
|
|
16
|
+
colors = categoricalVizColors,
|
|
17
|
+
colorMap,
|
|
18
|
+
centerValue,
|
|
19
|
+
centerLabel,
|
|
20
|
+
label = 'Composition',
|
|
21
|
+
format,
|
|
22
|
+
} = defineProps<{
|
|
23
|
+
data: T[]
|
|
24
|
+
/** Row key for the slice label. */
|
|
25
|
+
labelKey: keyof T & string
|
|
26
|
+
/** Row key for the numeric value. */
|
|
27
|
+
valueKey: keyof T & string
|
|
28
|
+
/** Diameter in px. Default 132. */
|
|
29
|
+
size?: number
|
|
30
|
+
/** Ordered design-system chart colours. */
|
|
31
|
+
colors?: readonly string[]
|
|
32
|
+
/** Stable category identity colours keyed by the rendered label. */
|
|
33
|
+
colorMap?: Readonly<Record<string, string>>
|
|
34
|
+
/** Optional dominant takeaway rendered in the ring centre. */
|
|
35
|
+
centerValue?: string | number
|
|
36
|
+
/** Short label beneath the centre value. */
|
|
37
|
+
centerLabel?: string
|
|
38
|
+
/** Accessible caption for the exact semantic legend. */
|
|
39
|
+
label?: string
|
|
40
|
+
/** Format the value in the legend. */
|
|
41
|
+
format?: (value: number) => string
|
|
42
|
+
}>()
|
|
43
|
+
|
|
44
|
+
function num(v: unknown) {
|
|
45
|
+
const n = typeof v === 'number' ? v : Number(String(v).replace(/^"+|"+$/g, ''))
|
|
46
|
+
return Number.isFinite(n) ? n : 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Arc maths against a unit-100 circumference so stroke-dasharray reads as percentages.
|
|
50
|
+
const R = 15.9155 // circumference ≈ 100
|
|
51
|
+
const C = 2 * Math.PI * R
|
|
52
|
+
const palette = computed(() => colors.length ? colors : categoricalVizColors)
|
|
53
|
+
const categoryColors = computed(() => {
|
|
54
|
+
const labels = [...new Set(data.map(r => String(r[labelKey] ?? '')))].sort((a, b) => a.localeCompare(b))
|
|
55
|
+
return new Map(labels.map((label, index) => [
|
|
56
|
+
label,
|
|
57
|
+
colorMap?.[label] ?? palette.value[index % palette.value.length]!,
|
|
58
|
+
]))
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const segments = computed(() => {
|
|
62
|
+
const items = data
|
|
63
|
+
.map(r => ({ label: String(r[labelKey] ?? ''), value: num(r[valueKey]) }))
|
|
64
|
+
.filter(s => s.value > 0)
|
|
65
|
+
.sort((a, b) => b.value - a.value)
|
|
66
|
+
const total = items.reduce((sum, s) => sum + s.value, 0) || 1
|
|
67
|
+
// The legend column sums to 100 (largest remainder); the arcs keep exact shares.
|
|
68
|
+
const rounded = roundedShares(items.map(s => s.value))
|
|
69
|
+
let acc = 0
|
|
70
|
+
return items.map((s, i) => {
|
|
71
|
+
const pct = (s.value / total) * 100
|
|
72
|
+
const pctLabel = roundedShareLabel(rounded[i]!, s.value, items.length > 1)
|
|
73
|
+
const seg = { ...s, color: categoryColors.value.get(s.label) ?? palette.value[i % palette.value.length]!, pct, pctLabel, offset: acc }
|
|
74
|
+
acc += pct
|
|
75
|
+
return seg
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
function fmt(value: number): string {
|
|
80
|
+
return format ? format(value) : formatGroupedNumber(value)
|
|
81
|
+
}
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<template>
|
|
85
|
+
<figure data-ui="UiDonutChart" class="flex items-center gap-4">
|
|
86
|
+
<div class="relative shrink-0" :style="{ width: `${size}px`, height: `${size}px` }">
|
|
87
|
+
<svg
|
|
88
|
+
:width="size"
|
|
89
|
+
:height="size"
|
|
90
|
+
viewBox="0 0 36 36"
|
|
91
|
+
class="-rotate-90"
|
|
92
|
+
aria-hidden="true"
|
|
93
|
+
focusable="false"
|
|
94
|
+
>
|
|
95
|
+
<circle cx="18" cy="18" :r="R" fill="none" stroke="var(--ui-bg-muted)" stroke-width="4" />
|
|
96
|
+
<circle
|
|
97
|
+
v-for="(s, i) in segments"
|
|
98
|
+
:key="i"
|
|
99
|
+
data-donut-segment
|
|
100
|
+
cx="18"
|
|
101
|
+
cy="18"
|
|
102
|
+
:r="R"
|
|
103
|
+
fill="none"
|
|
104
|
+
:stroke="s.color"
|
|
105
|
+
stroke-width="4"
|
|
106
|
+
:stroke-dasharray="`${(s.pct / 100) * C} ${C}`"
|
|
107
|
+
:stroke-dashoffset="`${-(s.offset / 100) * C}`"
|
|
108
|
+
/>
|
|
109
|
+
</svg>
|
|
110
|
+
<div
|
|
111
|
+
v-if="centerValue != null || centerLabel"
|
|
112
|
+
data-donut-center
|
|
113
|
+
class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center px-4 text-center"
|
|
114
|
+
>
|
|
115
|
+
<span v-if="centerValue != null" class="numerals-display text-heading text-highlighted">{{ centerValue }}</span>
|
|
116
|
+
<span v-if="centerLabel" class="max-w-full truncate text-sm text-muted">{{ centerLabel }}</span>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<ul class="min-w-0 flex-1 flex flex-col gap-1 text-sm">
|
|
121
|
+
<li v-for="(s, i) in segments" :key="i" class="flex items-center gap-2">
|
|
122
|
+
<span class="size-2 shrink-0 rounded-full" :style="{ background: s.color }" aria-hidden="true" />
|
|
123
|
+
<span data-donut-label class="flex-1 truncate text-muted" :title="s.label">{{ s.label }}</span>
|
|
124
|
+
<span class="tabular-nums text-highlighted">{{ fmt(s.value) }}</span>
|
|
125
|
+
<span class="w-9 text-right tabular-nums text-muted">{{ s.pctLabel }}</span>
|
|
126
|
+
</li>
|
|
127
|
+
</ul>
|
|
128
|
+
<figcaption class="sr-only">
|
|
129
|
+
{{ label }}
|
|
130
|
+
</figcaption>
|
|
131
|
+
</figure>
|
|
132
|
+
</template>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { NavigationDestination as RouteLocationRaw } from '../../shared/navigation'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { NuxtLink, UiButton, UiIcon, UiSkeleton } from '#components'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* UiDoorwayColumn — the card shell of a doorway column, extracted from the site
|
|
8
|
+
* overview's Next Actions / Recent Activity pair so a feature page can render
|
|
9
|
+
* the SAME column instead of a look-alike.
|
|
10
|
+
*
|
|
11
|
+
* A doorway names its destination once and stops: the header carries the label
|
|
12
|
+
* and one "View all" link, and the body carries one-line rows. The chrome that
|
|
13
|
+
* Columns sit inside a `UiDoorwayGrid`, which owns the single-column collapse
|
|
14
|
+
* and the hide-the-row-when-every-column-is-empty rule off the
|
|
15
|
+
* `data-doorway-empty` stamp below. Never hand-roll the `md:grid-cols-2` around
|
|
16
|
+
* a pair of columns — that grid is what leaves a gated-off column's half of the
|
|
17
|
+
* page blank.
|
|
18
|
+
*
|
|
19
|
+
* The chrome that
|
|
20
|
+
* does NOT belong here (and is what this shell exists to keep out) is the rail
|
|
21
|
+
* grammar it replaces on feature pages — count chips, context chips, group
|
|
22
|
+
* headings, severity bars, per-row lead-fact lines, age tails, and an "N more"
|
|
23
|
+
* footer. Depth is the owning route's job, which is what the header link is for.
|
|
24
|
+
*/
|
|
25
|
+
const {
|
|
26
|
+
title,
|
|
27
|
+
viewAllTo = null,
|
|
28
|
+
viewAllLabel = 'View all',
|
|
29
|
+
status = 'success',
|
|
30
|
+
empty = false,
|
|
31
|
+
emptyLabel = null,
|
|
32
|
+
errorLabel = 'Could not load this column.',
|
|
33
|
+
skeletonRows = 4,
|
|
34
|
+
} = defineProps<{
|
|
35
|
+
title: string
|
|
36
|
+
/** The owning route this column is a doorway to. Omit to render no link. */
|
|
37
|
+
viewAllTo?: RouteLocationRaw | null
|
|
38
|
+
viewAllLabel?: string
|
|
39
|
+
status?: 'pending' | 'success' | 'error'
|
|
40
|
+
empty?: boolean
|
|
41
|
+
/** Honest empty copy. Rendered only when `empty` and loaded. */
|
|
42
|
+
emptyLabel?: string | null
|
|
43
|
+
errorLabel?: string
|
|
44
|
+
skeletonRows?: number
|
|
45
|
+
}>()
|
|
46
|
+
|
|
47
|
+
const emit = defineEmits<{ retry: [] }>()
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The stamp `UiDoorwayGrid` reads to decide whether the whole row has anything
|
|
51
|
+
* to say. It means "loaded, and there is nothing here" — never "still loading"
|
|
52
|
+
* and never "failed", both of which are content a reader needs.
|
|
53
|
+
*/
|
|
54
|
+
const doorwayEmpty = computed(() => (status === 'success' && empty ? '' : undefined))
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<template>
|
|
58
|
+
<section
|
|
59
|
+
class="overflow-hidden rounded-xl border border-default bg-elevated/20"
|
|
60
|
+
:aria-label="title"
|
|
61
|
+
:aria-busy="status === 'pending' ? 'true' : undefined"
|
|
62
|
+
:data-doorway-empty="doorwayEmpty"
|
|
63
|
+
>
|
|
64
|
+
<div class="flex min-h-11 items-center gap-2 border-b border-default/60 px-4">
|
|
65
|
+
<span class="text-label">{{ title }}</span>
|
|
66
|
+
<slot name="header-meta" />
|
|
67
|
+
<NuxtLink
|
|
68
|
+
v-if="viewAllTo"
|
|
69
|
+
:to="viewAllTo"
|
|
70
|
+
class="ml-auto flex min-h-11 items-center text-xs text-muted outline-none transition-colors hover:text-default focus-visible:ring-2 focus-visible:ring-primary sm:min-h-0"
|
|
71
|
+
>
|
|
72
|
+
{{ viewAllLabel }}
|
|
73
|
+
</NuxtLink>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<template v-if="status === 'pending'">
|
|
77
|
+
<span class="sr-only" role="status" aria-live="polite">Loading {{ title }}</span>
|
|
78
|
+
<ul aria-hidden="true" class="divide-y divide-default/60">
|
|
79
|
+
<li v-for="n in skeletonRows" :key="n" class="flex min-w-0 items-center gap-2.5 px-4 py-2">
|
|
80
|
+
<UiSkeleton type="circle" :base="16" :index="n" class="shrink-0" />
|
|
81
|
+
<UiSkeleton type="text" :base="130" :range="90" :index="n" class="!h-3" />
|
|
82
|
+
</li>
|
|
83
|
+
</ul>
|
|
84
|
+
</template>
|
|
85
|
+
|
|
86
|
+
<div v-else-if="status === 'error'" class="flex min-h-11 items-center gap-2.5 px-4 py-2" role="alert">
|
|
87
|
+
<UiIcon name="caution" class="size-4 shrink-0 text-error" aria-hidden="true" />
|
|
88
|
+
<p class="min-w-0 flex-1 text-sm text-muted">
|
|
89
|
+
{{ errorLabel }}
|
|
90
|
+
</p>
|
|
91
|
+
<UiButton purpose="secondary" size="xs" icon="refresh" @click="emit('retry')">
|
|
92
|
+
Retry
|
|
93
|
+
</UiButton>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<template v-else-if="empty">
|
|
97
|
+
<slot name="empty">
|
|
98
|
+
<p v-if="emptyLabel" class="px-4 py-3 text-sm text-muted">
|
|
99
|
+
{{ emptyLabel }}
|
|
100
|
+
</p>
|
|
101
|
+
</slot>
|
|
102
|
+
</template>
|
|
103
|
+
|
|
104
|
+
<ul v-else class="divide-y divide-default/60">
|
|
105
|
+
<slot />
|
|
106
|
+
</ul>
|
|
107
|
+
</section>
|
|
108
|
+
</template>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* UiDoorwayGrid — the row that holds `UiDoorwayColumn`s and owns the two rules
|
|
4
|
+
* a page must not have to restate: how many columns actually rendered, and what
|
|
5
|
+
* to do when none of them has anything to say.
|
|
6
|
+
*
|
|
7
|
+
* Both rules are CSS, deliberately. A page gates a column with `v-if`
|
|
8
|
+
* (`allowLiveQueries`, `fullMonitoringAvailable`), and a column only learns it
|
|
9
|
+
* is empty after its query settles — so a JS count would have to be a
|
|
10
|
+
* registration protocol that resolves AFTER the parent's first render, which
|
|
11
|
+
* means a wrong class on the server and a hydration patch on the client.
|
|
12
|
+
* Counting slots is worse still: a slot is "present" even when everything
|
|
13
|
+
* inside it is `v-if`'d away. The selectors below read the DOM, which is the
|
|
14
|
+
* only thing that knows the truth, at the moment it is true.
|
|
15
|
+
*
|
|
16
|
+
* - `:only-child` — a gated-off column leaves a comment placeholder, not an
|
|
17
|
+
* element, so the surviving column is a genuine only-child and spans the full
|
|
18
|
+
* width instead of sitting in the left half of a 2-col grid. (Requires each
|
|
19
|
+
* column to be a single-root component; `UiDoorwayColumn` is.)
|
|
20
|
+
* - `:has(> :not([data-doorway-empty]))` — `UiDoorwayColumn` stamps
|
|
21
|
+
* `data-doorway-empty` on itself once it has LOADED and has no rows. If no
|
|
22
|
+
* child lacks that stamp, every column is empty and the whole row hides: a
|
|
23
|
+
* column stays rendered while its sibling has content (the pair teaches where
|
|
24
|
+
* activity will appear), but a page with nothing to say does not show two
|
|
25
|
+
* boxes saying nothing. Pending and error columns are never stamped, so the
|
|
26
|
+
* row never flickers away while loading.
|
|
27
|
+
*/
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<div data-doorway-grid class="grid items-start gap-5 md:grid-cols-2">
|
|
32
|
+
<slot />
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<style>
|
|
37
|
+
/* Deliberately unscoped: these rules are the component's behaviour, and the
|
|
38
|
+
last one has to reach the WRAPPER, which a scope attribute wouldn't match. */
|
|
39
|
+
|
|
40
|
+
/* The only column left (its sibling gated off) takes the whole width. */
|
|
41
|
+
[data-doorway-grid] > *:only-child {
|
|
42
|
+
grid-column: 1 / -1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* No column has content: the row says nothing, so it isn't drawn. */
|
|
46
|
+
[data-doorway-grid]:not(:has(> *:not([data-doorway-empty]))) {
|
|
47
|
+
display: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* …and neither is the page zone that exists only to hold it — a hidden row
|
|
51
|
+
would otherwise still spend a section gap on nothing. `:only-child` keeps
|
|
52
|
+
this off a zone that holds other content beside the row. Written as two
|
|
53
|
+
sibling conditions rather than one nested test because `:has()` inside
|
|
54
|
+
`:has()` is invalid and silently drops the whole rule. */
|
|
55
|
+
:where(div):has(> [data-doorway-grid]:only-child):not(:has([data-doorway-grid] > *:not([data-doorway-empty]))) {
|
|
56
|
+
display: none;
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { RichSegment } from '../../../shared/rich-text'
|
|
3
|
+
import type { NavigationDestination as RouteLocationRaw } from '../../shared/navigation'
|
|
4
|
+
import { computed } from 'vue'
|
|
5
|
+
import { NuxtLink, UiFavicon, UiIcon, UiRichText } from '#components'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* UiDoorwayRow — one line inside a `UiDoorwayColumn`: source identity, the
|
|
9
|
+
* typed headline, an optional trailing fact, a chevron. Nothing else. The row
|
|
10
|
+
* states what is next; the surface it opens owns the evidence.
|
|
11
|
+
*
|
|
12
|
+
* A row that opens a modal is a `<button aria-haspopup="dialog">`, never a
|
|
13
|
+
* `NuxtLink` carrying an `@click`. Vue's `mergeProps` appends a parent's
|
|
14
|
+
* fallthrough click AFTER `RouterLink`'s own `navigate` handler, so the router
|
|
15
|
+
* pushes first and the modal opens onto a page that is already leaving. The
|
|
16
|
+
* click listener here is bound ONLY on the button branch, so a link row can
|
|
17
|
+
* never grow that bug by accident.
|
|
18
|
+
*/
|
|
19
|
+
const {
|
|
20
|
+
label,
|
|
21
|
+
segments = null,
|
|
22
|
+
glyph = null,
|
|
23
|
+
faviconDomain = null,
|
|
24
|
+
to = null,
|
|
25
|
+
interactive = true,
|
|
26
|
+
} = defineProps<{
|
|
27
|
+
/** Plain-text row title. Always required — it is the accessible name. */
|
|
28
|
+
label: string
|
|
29
|
+
/** Typed headline; falls back to `label` when the producer has no segments. */
|
|
30
|
+
segments?: RichSegment[] | null
|
|
31
|
+
glyph?: string | null
|
|
32
|
+
/**
|
|
33
|
+
* Lead with this host's favicon instead of the glyph. Use it where the rows
|
|
34
|
+
* name external entities (a citing site, a platform) — a column of identical
|
|
35
|
+
* kind glyphs carries no signal, the favicons do.
|
|
36
|
+
*/
|
|
37
|
+
faviconDomain?: string | null
|
|
38
|
+
/** Navigation destination. Omit for a modal trigger. */
|
|
39
|
+
to?: RouteLocationRaw | null
|
|
40
|
+
/**
|
|
41
|
+
* A read-only log line (no destination, no modal) renders as a plain row: no
|
|
42
|
+
* hover cue, no chevron, no focus ring. Advertising a click a row cannot
|
|
43
|
+
* honour is the defect this flag exists to prevent.
|
|
44
|
+
*/
|
|
45
|
+
interactive?: boolean
|
|
46
|
+
}>()
|
|
47
|
+
|
|
48
|
+
const emit = defineEmits<{ select: [] }>()
|
|
49
|
+
|
|
50
|
+
const kind = computed<'link' | 'button' | 'static'>(() => {
|
|
51
|
+
if (!interactive)
|
|
52
|
+
return 'static'
|
|
53
|
+
return to ? 'link' : 'button'
|
|
54
|
+
})
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<template>
|
|
58
|
+
<li>
|
|
59
|
+
<component
|
|
60
|
+
:is="kind === 'link' ? NuxtLink : kind === 'button' ? 'button' : 'div'"
|
|
61
|
+
:to="kind === 'link' ? to! : undefined"
|
|
62
|
+
:type="kind === 'button' ? 'button' : undefined"
|
|
63
|
+
:aria-haspopup="kind === 'button' ? 'dialog' : undefined"
|
|
64
|
+
:aria-label="kind === 'static' ? undefined : label"
|
|
65
|
+
class="flex min-h-11 w-full min-w-0 items-center gap-2.5 px-4 py-1.5 text-left"
|
|
66
|
+
:class="kind === 'static'
|
|
67
|
+
? ''
|
|
68
|
+
: 'cursor-pointer outline-none transition-colors hover:bg-elevated/40 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary sm:min-h-0'"
|
|
69
|
+
v-on="kind === 'button' ? { click: () => emit('select') } : {}"
|
|
70
|
+
>
|
|
71
|
+
<slot name="leading">
|
|
72
|
+
<UiFavicon v-if="faviconDomain" :domain="faviconDomain" :size="14" decorative class="shrink-0 rounded-sm" />
|
|
73
|
+
<UiIcon v-else-if="glyph" :name="glyph" class="size-3.5 shrink-0 text-dimmed" aria-hidden="true" />
|
|
74
|
+
</slot>
|
|
75
|
+
<span class="min-w-0 flex-1 truncate text-sm leading-5 text-default">
|
|
76
|
+
<UiRichText v-if="segments?.length" :segments="segments" size="sm" truncate />
|
|
77
|
+
<template v-else>{{ label }}</template>
|
|
78
|
+
</span>
|
|
79
|
+
<slot name="trailing" />
|
|
80
|
+
<UiIcon v-if="kind !== 'static'" name="chevron-right" class="size-3 shrink-0 text-dimmed" aria-hidden="true" />
|
|
81
|
+
</component>
|
|
82
|
+
</li>
|
|
83
|
+
</template>
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { VNodeChild } from 'vue'
|
|
3
|
+
import type { FactItem } from './UiFactsGrid.vue'
|
|
4
|
+
import { useElementHover } from '@vueuse/core'
|
|
5
|
+
import { useTemplateRef } from 'vue'
|
|
6
|
+
import { ClientOnly, UiCard, UiFactsGrid, UiSparkline, UiStat } from '#components'
|
|
7
|
+
/**
|
|
8
|
+
* UiEntitySummary — the identity header for a machine-object DETAIL page
|
|
9
|
+
* (a competitor, a Lighthouse scan, an alert delivery). One hero metric +
|
|
10
|
+
* sparkline on the left, a quiet facts grid on the right, in a single card.
|
|
11
|
+
*
|
|
12
|
+
* NOT a data-page hero moment — the "UiMetricsRow OR UiStats cards, never both"
|
|
13
|
+
* gate is untouched. This is an *entity header*; it lives on detail pages that
|
|
14
|
+
* aren't in the page-hero blueprint at all.
|
|
15
|
+
*
|
|
16
|
+
* Squint test: the hero column dominates by SIZE + WEIGHT (lg value); facts
|
|
17
|
+
* recede (text-label / text-sm). The column divider is a NEUTRAL rule
|
|
18
|
+
* (--ui-border) — never a semantic / accent stripe (Avoid: left-border cliché).
|
|
19
|
+
*
|
|
20
|
+
* It's a thin composition of UiStat (hero) + UiSparkline + UiFactsGrid — keep it
|
|
21
|
+
* that way. Domain-shaped props stay at the call site; this only knows
|
|
22
|
+
* {heading, metric, facts}.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
heading,
|
|
27
|
+
title,
|
|
28
|
+
tooltip,
|
|
29
|
+
tooltipDescription,
|
|
30
|
+
value,
|
|
31
|
+
suffix,
|
|
32
|
+
caption,
|
|
33
|
+
trend,
|
|
34
|
+
trendSuffix,
|
|
35
|
+
invertTrend,
|
|
36
|
+
sparkline,
|
|
37
|
+
sparklineColor,
|
|
38
|
+
facts = [],
|
|
39
|
+
factColumns = 3,
|
|
40
|
+
status,
|
|
41
|
+
emphasis = true,
|
|
42
|
+
divider = 'bleed',
|
|
43
|
+
stackHeaderOnMobile = false,
|
|
44
|
+
} = defineProps<{
|
|
45
|
+
/** Entity identity line (e.g. a hostname). Overridable via #heading. */
|
|
46
|
+
heading?: string
|
|
47
|
+
/** Hero metric label (e.g. "Organic traffic"). */
|
|
48
|
+
title?: string
|
|
49
|
+
/** Contextual help — renders a (?) on the hero metric label. */
|
|
50
|
+
tooltip?: string
|
|
51
|
+
tooltipDescription?: string
|
|
52
|
+
value?: string | number | null
|
|
53
|
+
suffix?: string
|
|
54
|
+
/** Period / context line under the metric (e.g. "Last 12 months"). */
|
|
55
|
+
caption?: string
|
|
56
|
+
/** Full caption shown on hover when the visible caption is shortened. */
|
|
57
|
+
captionTitle?: string
|
|
58
|
+
trend?: number | null
|
|
59
|
+
trendSuffix?: string
|
|
60
|
+
invertTrend?: boolean
|
|
61
|
+
/** Hero sparkline series (metric identity color via sparklineColor). */
|
|
62
|
+
sparkline?: number[] | Record<string, number | string>[]
|
|
63
|
+
sparklineColor?: string
|
|
64
|
+
facts?: FactItem[]
|
|
65
|
+
factColumns?: 2 | 3
|
|
66
|
+
/** Threshold state — surfaces UiStat's status chip beside the metric label. */
|
|
67
|
+
status?: 'crisis' | 'warning' | 'good'
|
|
68
|
+
/**
|
|
69
|
+
* Separator treatment between hero / facts and under the header:
|
|
70
|
+
* - `bleed` crisp hairline bled to the card edges — structural ⊤ (default)
|
|
71
|
+
* - `inset` same full-bleed geometry, carved (line + highlight offset)
|
|
72
|
+
* - `fade` muted hairline that fades before the padding edges
|
|
73
|
+
* - `none` no rule — spacing carries the split (Linear-clean)
|
|
74
|
+
* `bleed` / `inset` run the header rule full card width and the column rule
|
|
75
|
+
* from that rule down to the card's bottom edge.
|
|
76
|
+
*/
|
|
77
|
+
divider?: 'fade' | 'none' | 'inset' | 'bleed'
|
|
78
|
+
/**
|
|
79
|
+
* The one sanctioned card lift (--elevation-emphasis + primary-tinted top
|
|
80
|
+
* bevel). Defaults on: the entity header is a detail page's single hero
|
|
81
|
+
* surface, so it's the sanctioned "max one per page" emphasis. Turn off when
|
|
82
|
+
* the page already spends its emphasis elsewhere.
|
|
83
|
+
*/
|
|
84
|
+
emphasis?: boolean
|
|
85
|
+
/** Stack identity and actions on narrow screens when actions need full labels. */
|
|
86
|
+
stackHeaderOnMobile?: boolean
|
|
87
|
+
}>()
|
|
88
|
+
|
|
89
|
+
defineSlots<{
|
|
90
|
+
/** Override the identity line (e.g. to host a UiOverflowCount / favicon). */
|
|
91
|
+
heading?: () => VNodeChild
|
|
92
|
+
/** Top-right actions (eject menu, refresh, links). */
|
|
93
|
+
actions?: () => VNodeChild
|
|
94
|
+
/** Override the hero column entirely. */
|
|
95
|
+
hero?: () => VNodeChild
|
|
96
|
+
/** Override the facts column entirely. */
|
|
97
|
+
facts?: () => VNodeChild
|
|
98
|
+
}>()
|
|
99
|
+
// Card hover drives the sparkline's draw-in tracer — the sanctioned spark-joy
|
|
100
|
+
// micro-moment (DESIGN: hover tracer is the one exception to animate-once).
|
|
101
|
+
const cardEl = useTemplateRef<HTMLElement>('cardEl')
|
|
102
|
+
const cardHovered = useElementHover(cardEl)
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<template>
|
|
106
|
+
<UiCard variant="subtle" :emphasis="emphasis" :class="`divider-${divider}`" data-ui="UiEntitySummary">
|
|
107
|
+
<!-- Identity + actions header (optional). Separator is a faded hairline
|
|
108
|
+
(mask idiom) so it never hard-hits the card padding edges. -->
|
|
109
|
+
<div
|
|
110
|
+
v-if="heading || $slots.heading || $slots.actions"
|
|
111
|
+
class="entity-hrule relative mb-5 flex justify-between gap-3 pb-4"
|
|
112
|
+
:class="stackHeaderOnMobile ? 'flex-col items-stretch sm:flex-row sm:items-center' : 'items-center'"
|
|
113
|
+
>
|
|
114
|
+
<slot name="heading">
|
|
115
|
+
<span class="truncate text-sm font-medium text-highlighted">{{ heading }}</span>
|
|
116
|
+
</slot>
|
|
117
|
+
<div
|
|
118
|
+
v-if="$slots.actions"
|
|
119
|
+
class="flex shrink-0 items-center gap-2"
|
|
120
|
+
:class="stackHeaderOnMobile ? 'w-full justify-end sm:w-auto' : ''"
|
|
121
|
+
>
|
|
122
|
+
<slot name="actions" />
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<!-- Hero (left, dominant) · facts (right, recede). Neutral column rule.
|
|
127
|
+
items-stretch lets the sparkline flex-fill the hero column so it
|
|
128
|
+
anchors the metric instead of floating; the facts column top-aligns. -->
|
|
129
|
+
<div class="grid items-stretch gap-y-6 lg:grid-cols-2 lg:gap-x-0">
|
|
130
|
+
<slot name="hero">
|
|
131
|
+
<div ref="cardEl" class="flex min-w-0 flex-col gap-1 lg:pr-6">
|
|
132
|
+
<UiStat
|
|
133
|
+
size="lg"
|
|
134
|
+
:title="title"
|
|
135
|
+
:tooltip="tooltip"
|
|
136
|
+
:tooltip-description="tooltipDescription"
|
|
137
|
+
:value="value"
|
|
138
|
+
value-class="text-highlighted"
|
|
139
|
+
:suffix="suffix"
|
|
140
|
+
:trend="trend"
|
|
141
|
+
:trend-suffix="trendSuffix"
|
|
142
|
+
:invert-trend="invertTrend"
|
|
143
|
+
:status="status"
|
|
144
|
+
trend-colored
|
|
145
|
+
/>
|
|
146
|
+
<p v-if="caption" class="text-sm text-muted" :title="captionTitle">
|
|
147
|
+
{{ caption }}
|
|
148
|
+
</p>
|
|
149
|
+
<ClientOnly v-if="sparkline?.length">
|
|
150
|
+
<div class="mt-2 min-h-[76px] flex-1">
|
|
151
|
+
<UiSparkline
|
|
152
|
+
:data="sparkline"
|
|
153
|
+
:color="sparklineColor"
|
|
154
|
+
:inverted="invertTrend"
|
|
155
|
+
:stroke-width="2"
|
|
156
|
+
interactive
|
|
157
|
+
:hovered="cardHovered"
|
|
158
|
+
area
|
|
159
|
+
width="100%"
|
|
160
|
+
height="100%"
|
|
161
|
+
preserve-aspect-ratio="none"
|
|
162
|
+
class="block size-full"
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
</ClientOnly>
|
|
166
|
+
</div>
|
|
167
|
+
</slot>
|
|
168
|
+
|
|
169
|
+
<div class="entity-vrule relative min-w-0 lg:pl-6">
|
|
170
|
+
<slot name="facts">
|
|
171
|
+
<UiFactsGrid :facts="facts" :columns="factColumns" />
|
|
172
|
+
</slot>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</UiCard>
|
|
176
|
+
</template>
|
|
177
|
+
|
|
178
|
+
<style scoped>
|
|
179
|
+
/* Separator geometry (shared). Color / mask / depth is set per `divider` mode
|
|
180
|
+
below so the same positions read four ways. A hard, edge-to-edge, full-weight
|
|
181
|
+
1px line is the cheap tell — every mode here softens or structures that. */
|
|
182
|
+
.entity-hrule::after {
|
|
183
|
+
content: '';
|
|
184
|
+
position: absolute;
|
|
185
|
+
inset-inline: 0;
|
|
186
|
+
bottom: 0;
|
|
187
|
+
height: 1px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* Vertical column rule — desktop only; on mobile the columns stack (gap-y). */
|
|
191
|
+
@media (min-width: 1024px) {
|
|
192
|
+
.entity-vrule::before {
|
|
193
|
+
content: '';
|
|
194
|
+
position: absolute;
|
|
195
|
+
left: 0;
|
|
196
|
+
top: 0.25rem;
|
|
197
|
+
bottom: 0.25rem;
|
|
198
|
+
width: 1px;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* none — no rule; spacing carries the split. */
|
|
203
|
+
.divider-none .entity-hrule::after,
|
|
204
|
+
.divider-none .entity-vrule::before {
|
|
205
|
+
display: none;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* fade — muted hairline that fades before the padding edges (mask idiom). */
|
|
209
|
+
.divider-fade .entity-hrule::after {
|
|
210
|
+
background: var(--ui-border-muted);
|
|
211
|
+
-webkit-mask-image: linear-gradient(to right, transparent, #000 4%, #000 96%, transparent);
|
|
212
|
+
mask-image: linear-gradient(to right, transparent, #000 4%, #000 96%, transparent);
|
|
213
|
+
}
|
|
214
|
+
.divider-fade .entity-vrule::before {
|
|
215
|
+
background: var(--ui-border-muted);
|
|
216
|
+
-webkit-mask-image: linear-gradient(to bottom, transparent, #000 14%, #000 86%, transparent);
|
|
217
|
+
mask-image: linear-gradient(to bottom, transparent, #000 14%, #000 86%, transparent);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* bleed — crisp full-length hairline, edge to edge. Structured / Stripe-like. */
|
|
221
|
+
.divider-bleed .entity-hrule::after,
|
|
222
|
+
.divider-bleed .entity-vrule::before {
|
|
223
|
+
background: var(--ui-border);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* inset — carved line: a hairline with a 1px light highlight offset below / to
|
|
227
|
+
the right, so the seam reads recessed (tactile depth, not a flat stroke). */
|
|
228
|
+
.divider-inset .entity-hrule::after {
|
|
229
|
+
background: var(--ui-border-accented);
|
|
230
|
+
box-shadow: 0 1px 0 var(--ui-bg-elevated);
|
|
231
|
+
}
|
|
232
|
+
.divider-inset .entity-vrule::before {
|
|
233
|
+
background: var(--ui-border-accented);
|
|
234
|
+
box-shadow: 1px 0 0 var(--ui-bg-elevated);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* Full-bleed structural geometry (bleed + inset): the seams reach the card's
|
|
238
|
+
inner edges. --seam-pad tracks UiCard's body padding so the negative insets
|
|
239
|
+
land exactly on the border; the card's overflow-hidden clips any overshoot.
|
|
240
|
+
Header rule → full card width; column rule → from the header rule (mb-5) down
|
|
241
|
+
to the card's bottom edge (the Vercel ⊤). */
|
|
242
|
+
[data-ui='UiEntitySummary'] {
|
|
243
|
+
--seam-pad: 1rem;
|
|
244
|
+
}
|
|
245
|
+
@media (min-width: 640px) {
|
|
246
|
+
[data-ui='UiEntitySummary'] {
|
|
247
|
+
--seam-pad: var(--density-card-padding, 1.5rem);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
.divider-bleed .entity-hrule::after,
|
|
251
|
+
.divider-inset .entity-hrule::after {
|
|
252
|
+
inset-inline: calc(-1 * var(--seam-pad));
|
|
253
|
+
}
|
|
254
|
+
@media (min-width: 1024px) {
|
|
255
|
+
.divider-bleed .entity-vrule::before,
|
|
256
|
+
.divider-inset .entity-vrule::before {
|
|
257
|
+
top: -1.25rem;
|
|
258
|
+
bottom: calc(-1 * var(--seam-pad));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
</style>
|