@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,120 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Component } from 'vue'
|
|
3
|
+
import type { SourceLogo } from '../element/UiSourceLogos.vue'
|
|
4
|
+
import { computed } from 'vue'
|
|
5
|
+
import { UiIcon, UiSourceLogos } from '#components'
|
|
6
|
+
|
|
7
|
+
// One external service in a list of them. The row states what the service does
|
|
8
|
+
// for you before it states that it is connected, so an unconnected list reads
|
|
9
|
+
// as a menu of capabilities rather than a checklist of chores.
|
|
10
|
+
//
|
|
11
|
+
// Presentational only: every value arrives as a prop. The caller owns fetching,
|
|
12
|
+
// connect flows, and per-site link state.
|
|
13
|
+
//
|
|
14
|
+
// Native <details> for the same reason UiDisclosure uses it — free keyboard and
|
|
15
|
+
// AT semantics, find-in-page auto-expand. The expanded half is where credential
|
|
16
|
+
// forms and per-site coverage live, so it must stay in the DOM-on-demand path
|
|
17
|
+
// rather than being a v-if the caller manages.
|
|
18
|
+
|
|
19
|
+
const { name, status, logo, logoComponent, connected = false, sources = [] } = defineProps<{
|
|
20
|
+
/** Service name as the vendor writes it. */
|
|
21
|
+
name: string
|
|
22
|
+
/**
|
|
23
|
+
* Coverage, not a bare "Connected". "4 of 6 sites linked" answers the
|
|
24
|
+
* question the row exists to answer; a checkmark does not.
|
|
25
|
+
*/
|
|
26
|
+
status: string
|
|
27
|
+
/** Brand mark as an Iconify `logos` id. */
|
|
28
|
+
logo?: string
|
|
29
|
+
/** Inline brand-mark component, for vendors no coloured icon set carries. */
|
|
30
|
+
logoComponent?: Component
|
|
31
|
+
/** Drives the quiet connected marker, not the status text. */
|
|
32
|
+
connected?: boolean
|
|
33
|
+
/**
|
|
34
|
+
* The Sources this Integration lets Pro read through, in Source order. The
|
|
35
|
+
* row wears their marks, and names them from `sm` up. Empty for a service
|
|
36
|
+
* that only sends, such as a delivery channel.
|
|
37
|
+
*/
|
|
38
|
+
sources?: readonly SourceLogo[]
|
|
39
|
+
}>()
|
|
40
|
+
|
|
41
|
+
const sourceNames = computed(() => sources.map(source => source.label).join(', '))
|
|
42
|
+
|
|
43
|
+
const open = defineModel<boolean>('open', { default: false })
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Keep an action click from toggling the row. The toggle is the summary's
|
|
47
|
+
* DEFAULT action, so `stopPropagation` alone never prevented it: a button
|
|
48
|
+
* whose label span took the click collapsed an open row. Links keep their
|
|
49
|
+
* default, which is the navigation, and leave the page anyway.
|
|
50
|
+
*/
|
|
51
|
+
function onActionClick(e: MouseEvent) {
|
|
52
|
+
e.stopPropagation()
|
|
53
|
+
if (!(e.target as Element | null)?.closest('a[href]'))
|
|
54
|
+
e.preventDefault()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function onToggle(e: Event) {
|
|
58
|
+
open.value = (e as ToggleEvent).newState === 'open'
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<template>
|
|
63
|
+
<details class="ui-disclosure group" :open="open" @toggle="onToggle">
|
|
64
|
+
<summary
|
|
65
|
+
class="grid grid-cols-[14px_32px_minmax(0,1fr)] items-center gap-x-3 gap-y-2 px-3.5 py-2.5 cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden min-h-11 outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-md sm:flex sm:gap-3"
|
|
66
|
+
>
|
|
67
|
+
<UiIcon
|
|
68
|
+
name="chevron-right"
|
|
69
|
+
class="size-3.5 shrink-0 text-dimmed transition-transform duration-150 group-open:rotate-90"
|
|
70
|
+
aria-hidden="true"
|
|
71
|
+
/>
|
|
72
|
+
<span class="inline-flex size-8 shrink-0 items-center justify-center rounded-lg border border-default bg-elevated">
|
|
73
|
+
<component :is="logoComponent" v-if="logoComponent" class="size-4" aria-hidden="true" />
|
|
74
|
+
<UiIcon v-else-if="logo" :name="logo" class="size-4" aria-hidden="true" />
|
|
75
|
+
</span>
|
|
76
|
+
<div class="min-w-0 flex-1">
|
|
77
|
+
<p class="break-words text-sm font-medium text-default sm:line-clamp-1">
|
|
78
|
+
{{ name }}
|
|
79
|
+
</p>
|
|
80
|
+
<!-- Clamped, not truncated. `truncate` half-applied here: the utility's
|
|
81
|
+
`white-space: nowrap` loses inside `<summary>`, so the line wrapped
|
|
82
|
+
to four lines at 375px while still claiming to ellipse. Two lines
|
|
83
|
+
keeps rows near-uniform without cutting a provider's description to
|
|
84
|
+
four words on mobile. -->
|
|
85
|
+
<p class="line-clamp-2 text-sm text-muted">
|
|
86
|
+
{{ status }}
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
89
|
+
<!-- From `sm` up the Sources sit in the row. Below it the service name
|
|
90
|
+
already truncates, so they move into the expanded half. -->
|
|
91
|
+
<span v-if="sources.length" class="hidden shrink-0 items-center gap-1.5 text-sm text-muted sm:inline-flex">
|
|
92
|
+
<UiSourceLogos :sources="sources" size="xs" />
|
|
93
|
+
{{ sourceNames }}
|
|
94
|
+
</span>
|
|
95
|
+
<!-- Below `sm` the status line already states coverage, and the marker
|
|
96
|
+
beside an action squeezed the service name to two letters. -->
|
|
97
|
+
<span v-if="connected" class="hidden shrink-0 items-center gap-1 text-sm text-muted sm:inline-flex">
|
|
98
|
+
<UiIcon name="check" class="size-3.5" aria-hidden="true" />
|
|
99
|
+
Connected
|
|
100
|
+
</span>
|
|
101
|
+
<!-- The action sits inside <summary>, where any click would also toggle the
|
|
102
|
+
disclosure. Handle it here rather than on each caller's button: whether a
|
|
103
|
+
button component forwards a native listener is its own business, and a
|
|
104
|
+
row whose button collapses the row it just acted on is the exact bug
|
|
105
|
+
this wrapper makes impossible.
|
|
106
|
+
Below `sm` the action takes a second row under the name, so a wide
|
|
107
|
+
button never squeezes the service name. -->
|
|
108
|
+
<span class="col-start-3 row-start-2 justify-self-start empty:hidden sm:shrink-0" @click="onActionClick">
|
|
109
|
+
<slot name="action" />
|
|
110
|
+
</span>
|
|
111
|
+
</summary>
|
|
112
|
+
<div class="px-3.5 pb-4 pt-1 pl-[4.25rem]">
|
|
113
|
+
<p v-if="sources.length" class="mb-2 flex items-center gap-1.5 text-sm text-muted sm:hidden">
|
|
114
|
+
<UiSourceLogos :sources="sources" size="xs" />
|
|
115
|
+
{{ sources.length === 1 ? 'Source' : 'Sources' }}: {{ sourceNames }}
|
|
116
|
+
</p>
|
|
117
|
+
<slot />
|
|
118
|
+
</div>
|
|
119
|
+
</details>
|
|
120
|
+
</template>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const { ariaLabel = 'Activity and next actions' } = defineProps<{
|
|
3
|
+
ariaLabel?: string
|
|
4
|
+
}>()
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<section class="know-do-rails min-w-0" :aria-label="ariaLabel">
|
|
9
|
+
<div class="know-do-rails__grid">
|
|
10
|
+
<slot />
|
|
11
|
+
</div>
|
|
12
|
+
</section>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<style scoped>
|
|
16
|
+
.know-do-rails {
|
|
17
|
+
container-type: inline-size;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.know-do-rails__grid {
|
|
21
|
+
position: relative;
|
|
22
|
+
display: grid;
|
|
23
|
+
align-items: start;
|
|
24
|
+
gap: 1rem;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
:global(.know-do-rails__grid > *) {
|
|
28
|
+
min-width: 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
:global(.know-do-rails__grid > :only-child) {
|
|
32
|
+
grid-column: 1 / -1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@container (min-width: 44rem) {
|
|
36
|
+
.know-do-rails__grid {
|
|
37
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
</style>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { UiChip, UiIcon } from '#components'
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
title,
|
|
7
|
+
description,
|
|
8
|
+
icon,
|
|
9
|
+
count = null,
|
|
10
|
+
status = 'success',
|
|
11
|
+
empty = false,
|
|
12
|
+
emptyLabel,
|
|
13
|
+
loadingLabel = 'Loading...',
|
|
14
|
+
errorLabel = 'Could not load this panel.',
|
|
15
|
+
hideWhenEmpty = true,
|
|
16
|
+
} = defineProps<{
|
|
17
|
+
title: string
|
|
18
|
+
description?: string
|
|
19
|
+
icon?: string
|
|
20
|
+
count?: string | number | null
|
|
21
|
+
status?: 'pending' | 'success' | 'error'
|
|
22
|
+
empty?: boolean
|
|
23
|
+
emptyLabel?: string
|
|
24
|
+
loadingLabel?: string
|
|
25
|
+
errorLabel?: string
|
|
26
|
+
hideWhenEmpty?: boolean
|
|
27
|
+
}>()
|
|
28
|
+
|
|
29
|
+
const shouldRender = computed(() =>
|
|
30
|
+
status !== 'success' || !empty || !!emptyLabel || !hideWhenEmpty,
|
|
31
|
+
)
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<section
|
|
36
|
+
v-if="shouldRender"
|
|
37
|
+
class="group/rail min-w-0 overflow-hidden rounded-lg border border-default bg-elevated/45 transition-colors hover:bg-elevated/60"
|
|
38
|
+
:aria-label="title"
|
|
39
|
+
:aria-busy="status === 'pending' ? 'true' : undefined"
|
|
40
|
+
>
|
|
41
|
+
<header class="flex min-h-12 flex-wrap items-center justify-between gap-x-3 gap-y-2 border-b border-default/80 px-3.5 py-2.5">
|
|
42
|
+
<div class="flex min-w-0 items-center gap-2">
|
|
43
|
+
<span
|
|
44
|
+
v-if="icon"
|
|
45
|
+
class="flex size-6 shrink-0 items-center justify-center rounded-md border border-default bg-default text-dimmed"
|
|
46
|
+
aria-hidden="true"
|
|
47
|
+
>
|
|
48
|
+
<UiIcon :name="icon" class="size-3.5" />
|
|
49
|
+
</span>
|
|
50
|
+
<span class="min-w-0">
|
|
51
|
+
<h3 class="min-w-0 truncate text-sm font-medium text-highlighted">
|
|
52
|
+
{{ title }}
|
|
53
|
+
</h3>
|
|
54
|
+
<p v-if="description" class="mt-0.5 truncate text-mini text-dimmed">
|
|
55
|
+
{{ description }}
|
|
56
|
+
</p>
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="flex max-w-full shrink-0 flex-wrap items-center justify-end gap-2">
|
|
61
|
+
<slot name="meta" />
|
|
62
|
+
<UiChip
|
|
63
|
+
v-if="count != null && count !== 0"
|
|
64
|
+
purpose="count"
|
|
65
|
+
size="xs"
|
|
66
|
+
tabular
|
|
67
|
+
>
|
|
68
|
+
{{ count }}
|
|
69
|
+
</UiChip>
|
|
70
|
+
</div>
|
|
71
|
+
</header>
|
|
72
|
+
|
|
73
|
+
<slot v-if="status === 'pending'" name="loading">
|
|
74
|
+
<div class="px-4 py-6 text-sm text-dimmed">
|
|
75
|
+
{{ loadingLabel }}
|
|
76
|
+
</div>
|
|
77
|
+
</slot>
|
|
78
|
+
|
|
79
|
+
<slot v-else-if="status === 'error'" name="error">
|
|
80
|
+
<div class="px-4 py-6 text-sm text-dimmed">
|
|
81
|
+
{{ errorLabel }}
|
|
82
|
+
</div>
|
|
83
|
+
</slot>
|
|
84
|
+
|
|
85
|
+
<slot v-else-if="empty" name="empty">
|
|
86
|
+
<div v-if="emptyLabel" class="px-4 py-5 text-sm text-dimmed">
|
|
87
|
+
{{ emptyLabel }}
|
|
88
|
+
</div>
|
|
89
|
+
</slot>
|
|
90
|
+
|
|
91
|
+
<slot v-else />
|
|
92
|
+
</section>
|
|
93
|
+
</template>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { SemanticStatus } from '../../composables/semanticColors'
|
|
3
|
+
import type { UiIcon as UiIconName } from '../../shared/ui-icons'
|
|
4
|
+
import { m } from 'motion-v'
|
|
5
|
+
import { computed } from 'vue'
|
|
6
|
+
import { UiButton, UiIcon } from '#components'
|
|
7
|
+
import { semanticColors } from '../../composables/semanticColors'
|
|
8
|
+
import { useEntranceMotion } from '../../composables/useEntranceMotion'
|
|
9
|
+
import { entrancePresets } from '../../shared/motion'
|
|
10
|
+
|
|
11
|
+
const { status = 'info', icon, title } = defineProps<{
|
|
12
|
+
/** Semantic status — drives the accent icon + bar + corner bloom (surface
|
|
13
|
+
* stays neutral for every status). Shared name across the status family
|
|
14
|
+
* (UiChip, UiStatusBadge). */
|
|
15
|
+
status?: SemanticStatus
|
|
16
|
+
/** Icon name (Lucide/Carbon) */
|
|
17
|
+
icon?: UiIconName
|
|
18
|
+
/** Alert title */
|
|
19
|
+
title?: string
|
|
20
|
+
/** Alert description text */
|
|
21
|
+
description?: string
|
|
22
|
+
/** Show close/dismiss button */
|
|
23
|
+
dismissible?: boolean
|
|
24
|
+
}>()
|
|
25
|
+
|
|
26
|
+
const emit = defineEmits<{
|
|
27
|
+
dismiss: []
|
|
28
|
+
}>()
|
|
29
|
+
|
|
30
|
+
// All alerts use a neutral surface; the semantic accent rides the icon + the
|
|
31
|
+
// top bar + corner bloom only. A red/amber/green fill dominates the page —
|
|
32
|
+
// especially when alerts stack — and spends color on the surface instead of
|
|
33
|
+
// the signal. Keep the colour where it carries meaning (the icon), not the
|
|
34
|
+
// whole banner. (Color-budget ladder — see brand-kit /status.)
|
|
35
|
+
const colors = computed(() => ({
|
|
36
|
+
...semanticColors[status],
|
|
37
|
+
bg: 'bg-elevated/50',
|
|
38
|
+
border: 'border-default',
|
|
39
|
+
}))
|
|
40
|
+
|
|
41
|
+
const defaultIcons: Record<SemanticStatus, string> = {
|
|
42
|
+
error: 'caution',
|
|
43
|
+
warning: 'warning',
|
|
44
|
+
info: 'note',
|
|
45
|
+
success: 'success',
|
|
46
|
+
neutral: 'circle-dash',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const resolvedIcon = computed(() => icon || defaultIcons[status])
|
|
50
|
+
|
|
51
|
+
const isUrgent = computed(() => status === 'error' || status === 'warning')
|
|
52
|
+
const dismissLabel = computed(() => title ? `Dismiss alert: ${title}` : 'Dismiss alert')
|
|
53
|
+
|
|
54
|
+
const entrance = useEntranceMotion(entrancePresets.fadeUp)
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<template>
|
|
58
|
+
<!-- motion-v fadeUp entrance — animates in when the client mounts it; server-rendered alerts stay visible. -->
|
|
59
|
+
<m.div
|
|
60
|
+
class="ui-alert group relative overflow-hidden rounded-xl border"
|
|
61
|
+
:class="[colors.bg, colors.border]"
|
|
62
|
+
:role="isUrgent ? 'alert' : 'status'"
|
|
63
|
+
:aria-live="isUrgent ? 'assertive' : 'polite'"
|
|
64
|
+
v-bind="entrance"
|
|
65
|
+
>
|
|
66
|
+
<!-- Top-left corner bloom — faint radial light source, kept whisper-quiet
|
|
67
|
+
so the semantic accent reads as a hint, not a wash. -->
|
|
68
|
+
<div
|
|
69
|
+
class="pointer-events-none absolute left-0 top-0 size-14 rounded-full opacity-[0.15] blur-xl"
|
|
70
|
+
:class="colors.dot"
|
|
71
|
+
style="transform: translate(-55%, -55%);"
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
<!-- Gradient fade accent bar — top edge, crisp at left, fades out.
|
|
76
|
+
Runs along the top so the semantic accent matches the top-lit
|
|
77
|
+
direction of the --surface-bevel fill rather than fighting it. -->
|
|
78
|
+
<div
|
|
79
|
+
class="pointer-events-none absolute inset-x-0 top-0 h-px opacity-[0.35]"
|
|
80
|
+
:class="colors.dot"
|
|
81
|
+
style="mask-image: linear-gradient(to right, black 0%, black 20%, transparent 80%); -webkit-mask-image: linear-gradient(to right, black 0%, black 20%, transparent 80%);"
|
|
82
|
+
aria-hidden="true"
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
<div class="flex items-start gap-3 px-4 py-3">
|
|
86
|
+
<UiIcon :name="resolvedIcon" class="mt-0.5 size-4 shrink-0" :class="colors.text" aria-hidden="true" />
|
|
87
|
+
|
|
88
|
+
<!-- Content -->
|
|
89
|
+
<div class="min-w-0 flex-1 flex flex-col items-start gap-3 sm:flex-row sm:items-start sm:gap-4">
|
|
90
|
+
<div class="min-w-0 flex-1 text-sm">
|
|
91
|
+
<p v-if="title" class="font-medium text-default">
|
|
92
|
+
{{ title }}
|
|
93
|
+
</p>
|
|
94
|
+
<p v-if="description" class="text-muted" :class="title ? 'mt-0.5' : ''">
|
|
95
|
+
{{ description }}
|
|
96
|
+
</p>
|
|
97
|
+
<slot />
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<!-- Inline action -->
|
|
101
|
+
<slot name="action" />
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<!-- Dismiss -->
|
|
105
|
+
<UiButton
|
|
106
|
+
v-if="dismissible"
|
|
107
|
+
class="min-h-11 min-w-11 cursor-pointer"
|
|
108
|
+
:aria-label="dismissLabel"
|
|
109
|
+
purpose="quiet"
|
|
110
|
+
size="xs"
|
|
111
|
+
@click="emit('dismiss')"
|
|
112
|
+
>
|
|
113
|
+
<UiIcon name="close" class="size-3" aria-hidden="true" />
|
|
114
|
+
</UiButton>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<!-- Progress bar (optional) -->
|
|
118
|
+
<slot name="progress" />
|
|
119
|
+
</m.div>
|
|
120
|
+
</template>
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Generic dashboard shell: fixed sidebar, content slot, mobile drawer.
|
|
3
|
+
// Pure layout — no auth, no banners, no app-specific chrome.
|
|
4
|
+
// Used by admin (AdminLayoutShell), pro (ProDashboardShell), and the
|
|
5
|
+
// design-system sandbox (brand-kit `kit` layout).
|
|
6
|
+
import type { VNodeChild } from 'vue'
|
|
7
|
+
import type { RouteLocationRaw } from 'vue-router'
|
|
8
|
+
import { useMediaQuery } from '@vueuse/core'
|
|
9
|
+
import { useRoute, useRouter } from 'nuxt/app'
|
|
10
|
+
import { provide, ref, watch } from 'vue'
|
|
11
|
+
import { UDrawer, UiButton, UiIcon } from '#components'
|
|
12
|
+
import { provideUiAppShellNav } from '../../composables/useAppShellNav'
|
|
13
|
+
import { navDrawerCloseDecider, uiNavDrawerKey } from './ui-nav-drawer'
|
|
14
|
+
|
|
15
|
+
const { sidebarWidth = 64, flushContent = false, contentClass, inlineMobileNav = false } = defineProps<{
|
|
16
|
+
sidebarWidth?: 56 | 64
|
|
17
|
+
flushContent?: boolean
|
|
18
|
+
/** Override the content padding (non-flush only). */
|
|
19
|
+
contentClass?: string
|
|
20
|
+
/** The page header draws the hamburger (+ switcher) inline with the page
|
|
21
|
+
* title below `lg` (via `useUiAppShellNav`), so the shell draws no separate
|
|
22
|
+
* mobile bar above the content. */
|
|
23
|
+
inlineMobileNav?: boolean
|
|
24
|
+
}>()
|
|
25
|
+
|
|
26
|
+
const slots = defineSlots<{
|
|
27
|
+
brand?: () => VNodeChild
|
|
28
|
+
sidebar?: () => VNodeChild
|
|
29
|
+
footer?: () => VNodeChild
|
|
30
|
+
topBanners?: () => VNodeChild
|
|
31
|
+
mobile?: (props: { closeNav: () => void }) => VNodeChild
|
|
32
|
+
/** Inline content next to the mobile hamburger (e.g. the site/group switcher). */
|
|
33
|
+
mobileNav?: () => VNodeChild
|
|
34
|
+
default?: () => VNodeChild
|
|
35
|
+
bottom?: () => VNodeChild
|
|
36
|
+
extras?: () => VNodeChild
|
|
37
|
+
}>()
|
|
38
|
+
|
|
39
|
+
const navOpen = ref(false)
|
|
40
|
+
const route = useRoute()
|
|
41
|
+
const router = useRouter()
|
|
42
|
+
const isDesktop = useMediaQuery('(min-width: 1024px)')
|
|
43
|
+
|
|
44
|
+
// An in-drawer pane switch (the sidebar mode tabs) IS a route change, but it
|
|
45
|
+
// must not close the drawer the way a page navigation does: the tab arms its
|
|
46
|
+
// destination here, and the watcher keeps the drawer open when that exact
|
|
47
|
+
// navigation lands. The watcher keys on fullPath, so every route change
|
|
48
|
+
// consumes the arm: a query-only change (the tabs flip a chatNonce) must not
|
|
49
|
+
// leave it armed for a later navigation. Only a path change closes.
|
|
50
|
+
const drawerNav = navDrawerCloseDecider()
|
|
51
|
+
function keepOpenFor(target: RouteLocationRaw) {
|
|
52
|
+
drawerNav.arm(router.resolve(target).fullPath)
|
|
53
|
+
}
|
|
54
|
+
provide(uiNavDrawerKey, { keepOpenFor })
|
|
55
|
+
|
|
56
|
+
let watchedPath = route.path
|
|
57
|
+
watch(() => route.fullPath, () => {
|
|
58
|
+
const pathChanged = route.path !== watchedPath
|
|
59
|
+
watchedPath = route.path
|
|
60
|
+
const close = drawerNav.shouldCloseOnRouteChange(route.fullPath)
|
|
61
|
+
if (pathChanged && close)
|
|
62
|
+
navOpen.value = false
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (import.meta.client) {
|
|
66
|
+
watch(isDesktop, (desktop) => {
|
|
67
|
+
if (desktop)
|
|
68
|
+
navOpen.value = false
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function openNav() {
|
|
73
|
+
navOpen.value = true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function closeNav() {
|
|
77
|
+
navOpen.value = false
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
provideUiAppShellNav({ openNav })
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<template>
|
|
84
|
+
<div class="flex min-h-screen" data-allow-mismatch="children">
|
|
85
|
+
<a
|
|
86
|
+
href="#main-content"
|
|
87
|
+
class="sr-only focus-visible:not-sr-only focus-visible:fixed focus-visible:top-3 focus-visible:left-3 focus-visible:z-50 focus-visible:rounded-md focus-visible:bg-default focus-visible:px-3 focus-visible:py-2 focus-visible:text-sm focus-visible:font-medium focus-visible:ring-2 focus-visible:ring-primary"
|
|
88
|
+
>Skip to content</a>
|
|
89
|
+
<aside
|
|
90
|
+
class="hidden lg:flex flex-col shrink-0 fixed top-0 bottom-0 left-0 border-r border-default bg-default"
|
|
91
|
+
:class="sidebarWidth === 56 ? 'w-56' : 'w-64'"
|
|
92
|
+
>
|
|
93
|
+
<div v-if="slots.brand" class="shrink-0 px-3 pt-4 pb-2">
|
|
94
|
+
<slot name="brand" />
|
|
95
|
+
</div>
|
|
96
|
+
<div class="flex-1 min-h-0 overflow-y-auto scroll-overlay px-3 py-2 space-y-5">
|
|
97
|
+
<slot name="sidebar" />
|
|
98
|
+
</div>
|
|
99
|
+
<div v-if="slots.footer" class="shrink-0 border-t border-default px-3 py-3">
|
|
100
|
+
<slot name="footer" />
|
|
101
|
+
</div>
|
|
102
|
+
</aside>
|
|
103
|
+
|
|
104
|
+
<div
|
|
105
|
+
class="flex-1 min-w-0"
|
|
106
|
+
:class="[
|
|
107
|
+
sidebarWidth === 56 ? 'lg:ml-56' : 'lg:ml-64',
|
|
108
|
+
flushContent ? 'h-screen flex flex-col' : '',
|
|
109
|
+
]"
|
|
110
|
+
>
|
|
111
|
+
<slot name="topBanners" />
|
|
112
|
+
|
|
113
|
+
<div v-if="!inlineMobileNav" class="lg:hidden flex items-center gap-1 px-4 pt-3">
|
|
114
|
+
<UiButton purpose="quiet" class="-ml-2 min-h-11 min-w-11 shrink-0" aria-label="Open navigation menu" @click="openNav()">
|
|
115
|
+
<UiIcon name="menu" class="size-5" aria-hidden="true" />
|
|
116
|
+
</UiButton>
|
|
117
|
+
<div v-if="slots.mobileNav" class="min-w-0 flex-1">
|
|
118
|
+
<slot name="mobileNav" />
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div class="flex flex-col" :class="flushContent ? 'flex-1 min-h-0' : 'min-h-screen'">
|
|
123
|
+
<main id="main-content" class="flex-1" :class="flushContent ? 'min-h-0' : (contentClass ?? 'p-4 sm:p-6 lg:p-8')">
|
|
124
|
+
<slot />
|
|
125
|
+
</main>
|
|
126
|
+
<slot name="bottom" />
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<!-- A left drawer sizes to its content by default, which squeezed the nav
|
|
131
|
+
to ~250px on a phone. Fixed width, capped so the scrim stays tappable.
|
|
132
|
+
No handle: it is a swipe affordance for bottom sheets and overlapped
|
|
133
|
+
the nav rows here. -->
|
|
134
|
+
<UDrawer
|
|
135
|
+
v-model:open="navOpen"
|
|
136
|
+
title="Navigation menu"
|
|
137
|
+
direction="left"
|
|
138
|
+
:handle="false"
|
|
139
|
+
:ui="{ content: 'w-80 max-w-[calc(100vw-3rem)]' }"
|
|
140
|
+
>
|
|
141
|
+
<template #content>
|
|
142
|
+
<div class="flex flex-1 flex-col h-full min-w-0 overflow-x-hidden">
|
|
143
|
+
<template v-if="slots.mobile">
|
|
144
|
+
<div class="flex-1 min-h-0 overflow-y-auto px-5 pt-5 pb-3">
|
|
145
|
+
<slot name="mobile" :close-nav="closeNav" />
|
|
146
|
+
</div>
|
|
147
|
+
</template>
|
|
148
|
+
<template v-else>
|
|
149
|
+
<div v-if="slots.brand" class="shrink-0 px-5 pt-5 pb-2">
|
|
150
|
+
<slot name="brand" />
|
|
151
|
+
</div>
|
|
152
|
+
<div class="flex-1 min-h-0 overflow-y-auto scroll-overlay px-5 py-2 space-y-5">
|
|
153
|
+
<slot name="sidebar" />
|
|
154
|
+
</div>
|
|
155
|
+
<div v-if="slots.footer" class="shrink-0 border-t border-default px-5 py-4">
|
|
156
|
+
<slot name="footer" />
|
|
157
|
+
</div>
|
|
158
|
+
</template>
|
|
159
|
+
</div>
|
|
160
|
+
</template>
|
|
161
|
+
</UDrawer>
|
|
162
|
+
|
|
163
|
+
<slot name="extras" />
|
|
164
|
+
</div>
|
|
165
|
+
</template>
|