@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.
Files changed (219) hide show
  1. package/README.md +181 -2
  2. package/app/app.config.ts +592 -0
  3. package/app/component-attributes.d.ts +8 -0
  4. package/app/components/README.md +38 -0
  5. package/app/components/brand/UiBrandMark.vue +63 -0
  6. package/app/components/brand/UiModuleIcon.vue +45 -0
  7. package/app/components/brand/UiProLettering.vue +26 -0
  8. package/app/components/brand/UiRangeTerrain.vue +311 -0
  9. package/app/components/brand/range-glyphs.ts +18 -0
  10. package/app/components/container/BrowserTab.vue +145 -0
  11. package/app/components/container/IntegrationRow.vue +120 -0
  12. package/app/components/container/KnowDoRails.vue +40 -0
  13. package/app/components/container/RailPanel.vue +93 -0
  14. package/app/components/container/UiAlert.vue +120 -0
  15. package/app/components/container/UiAppShell.vue +165 -0
  16. package/app/components/container/UiCard.vue +204 -0
  17. package/app/components/container/UiDisclosure.vue +43 -0
  18. package/app/components/container/UiEmptyState.vue +102 -0
  19. package/app/components/container/UiErrorState.vue +61 -0
  20. package/app/components/container/UiLoadingState.vue +20 -0
  21. package/app/components/container/UiPageHeader.vue +95 -0
  22. package/app/components/container/UiPopover.vue +102 -0
  23. package/app/components/container/UiSampleDataOverlay.vue +47 -0
  24. package/app/components/container/UiSectionHeader.vue +83 -0
  25. package/app/components/container/UiSummaryStrip.vue +40 -0
  26. package/app/components/container/UiToast.vue +230 -0
  27. package/app/components/container/UiToaster.vue +271 -0
  28. package/app/components/container/UiWidgetState.vue +143 -0
  29. package/app/components/container/ui-nav-drawer.ts +48 -0
  30. package/app/components/data/ActionRail.vue +152 -0
  31. package/app/components/data/FacetStrip.vue +93 -0
  32. package/app/components/data/UiBarCell.vue +37 -0
  33. package/app/components/data/UiBarChart.vue +281 -0
  34. package/app/components/data/UiChartAnnotationControls.vue +74 -0
  35. package/app/components/data/UiChartAnnotations.vue +163 -0
  36. package/app/components/data/UiChartBrushOverlay.vue +96 -0
  37. package/app/components/data/UiChartDataReveal.vue +81 -0
  38. package/app/components/data/UiChartFrame.vue +123 -0
  39. package/app/components/data/UiChartTooltip.vue +81 -0
  40. package/app/components/data/UiCoverageNote.vue +90 -0
  41. package/app/components/data/UiCwvSparkline.vue +188 -0
  42. package/app/components/data/UiDataList.vue +276 -0
  43. package/app/components/data/UiDataTableSection.vue +233 -0
  44. package/app/components/data/UiDistributionBars.vue +164 -0
  45. package/app/components/data/UiDonutChart.vue +132 -0
  46. package/app/components/data/UiDoorwayColumn.vue +108 -0
  47. package/app/components/data/UiDoorwayGrid.vue +58 -0
  48. package/app/components/data/UiDoorwayRow.vue +83 -0
  49. package/app/components/data/UiEntitySummary.vue +261 -0
  50. package/app/components/data/UiFactsGrid.vue +107 -0
  51. package/app/components/data/UiImpactMeter.vue +33 -0
  52. package/app/components/data/UiIssueRow.vue +149 -0
  53. package/app/components/data/UiLineChart.vue +431 -0
  54. package/app/components/data/UiMetricBarPopover.vue +121 -0
  55. package/app/components/data/UiMetricLabel.vue +30 -0
  56. package/app/components/data/UiMetricStat.vue +73 -0
  57. package/app/components/data/UiMetricToggle.vue +156 -0
  58. package/app/components/data/UiMetricsRow.vue +64 -0
  59. package/app/components/data/UiPositionMetric.vue +53 -0
  60. package/app/components/data/UiRelatedFixes.vue +113 -0
  61. package/app/components/data/UiRichText.vue +325 -0
  62. package/app/components/data/UiScatterPlot.vue +144 -0
  63. package/app/components/data/UiSeverityMarker.vue +30 -0
  64. package/app/components/data/UiSparkColumns.vue +160 -0
  65. package/app/components/data/UiSparkline.vue +681 -0
  66. package/app/components/data/UiStat.vue +445 -0
  67. package/app/components/data/UiStats.vue +76 -0
  68. package/app/components/data/UiTable.vue +517 -0
  69. package/app/components/data/UiTableExport.vue +40 -0
  70. package/app/components/data/UiTableFrame.vue +149 -0
  71. package/app/components/data/UiTablePagination.vue +37 -0
  72. package/app/components/data/UiTableShell.vue +48 -0
  73. package/app/components/data/UiTableTd.vue +39 -0
  74. package/app/components/data/UiTableTh.vue +61 -0
  75. package/app/components/data/UiTopEntityStackChart.vue +393 -0
  76. package/app/components/data/cells/UiTableDash.vue +3 -0
  77. package/app/components/data/cells/UiTableHeaderCell.vue +72 -0
  78. package/app/components/data/cells/UiTableMetricCell.vue +72 -0
  79. package/app/components/data/cells/UiTablePathCell.vue +76 -0
  80. package/app/components/data/cells/UiTableScoreTile.vue +49 -0
  81. package/app/components/data/cells/UiTableTrendCell.vue +41 -0
  82. package/app/components/element/UiAtmosphere.vue +64 -0
  83. package/app/components/element/UiAuthHeading.vue +24 -0
  84. package/app/components/element/UiAuthProviders.vue +46 -0
  85. package/app/components/element/UiBrandIcon.vue +39 -0
  86. package/app/components/element/UiButton.vue +549 -0
  87. package/app/components/element/UiCelebration.vue +65 -0
  88. package/app/components/element/UiChannelChips.vue +62 -0
  89. package/app/components/element/UiChatQuickLink.vue +319 -0
  90. package/app/components/element/UiChip.vue +101 -0
  91. package/app/components/element/UiConfirmDialog.vue +84 -0
  92. package/app/components/element/UiCopyButton.vue +90 -0
  93. package/app/components/element/UiDitherAvatar.vue +177 -0
  94. package/app/components/element/UiDotLabel.vue +19 -0
  95. package/app/components/element/UiEjectMenu.vue +181 -0
  96. package/app/components/element/UiEjectOverlay.vue +29 -0
  97. package/app/components/element/UiFavicon.vue +235 -0
  98. package/app/components/element/UiFaviconStack.vue +84 -0
  99. package/app/components/element/UiFilterMenu.vue +324 -0
  100. package/app/components/element/UiFocusedSurface.vue +129 -0
  101. package/app/components/element/UiHealthDot.vue +32 -0
  102. package/app/components/element/UiHelpLabel.vue +28 -0
  103. package/app/components/element/UiIcon.vue +22 -0
  104. package/app/components/element/UiInput.vue +35 -0
  105. package/app/components/element/UiLogoPlausible.vue +12 -0
  106. package/app/components/element/UiLogoPro.vue +25 -0
  107. package/app/components/element/UiLogoUmami.vue +12 -0
  108. package/app/components/element/UiNavIcon.vue +107 -0
  109. package/app/components/element/UiNavList.vue +156 -0
  110. package/app/components/element/UiOnboardingAnswers.vue +127 -0
  111. package/app/components/element/UiOverflowCount.vue +31 -0
  112. package/app/components/element/UiProBadge.vue +19 -0
  113. package/app/components/element/UiProgressCircle.vue +79 -0
  114. package/app/components/element/UiProgressPercent.vue +35 -0
  115. package/app/components/element/UiRelativeTime.vue +58 -0
  116. package/app/components/element/UiScrollFade.vue +16 -0
  117. package/app/components/element/UiSelect.vue +68 -0
  118. package/app/components/element/UiSeverityDot.vue +27 -0
  119. package/app/components/element/UiSiteFavicon.vue +24 -0
  120. package/app/components/element/UiSkeleton.vue +148 -0
  121. package/app/components/element/UiSlotText.vue +65 -0
  122. package/app/components/element/UiSourceLogos.vue +113 -0
  123. package/app/components/element/UiSourceRail.vue +71 -0
  124. package/app/components/element/UiStaticTerminal.vue +107 -0
  125. package/app/components/element/UiStatusBadge.vue +62 -0
  126. package/app/components/element/UiSyncDot.vue +46 -0
  127. package/app/components/element/UiTabs.vue +245 -0
  128. package/app/components/element/UiTogglePill.vue +86 -0
  129. package/app/components/element/UiTooltip.vue +390 -0
  130. package/app/components/element/UiTooltipGrid.vue +457 -0
  131. package/app/components/element/UiTrend.vue +184 -0
  132. package/app/components/element/UiTrialBenefits.vue +71 -0
  133. package/app/components/element/UiUrlLabel.vue +75 -0
  134. package/app/components/element/UiValenceDot.vue +37 -0
  135. package/app/components/element/UiWizardModal.vue +284 -0
  136. package/app/components/element/UiWizardNav.vue +59 -0
  137. package/app/components/element/UiWizardProgress.vue +78 -0
  138. package/app/components/element/UiWizardShell.vue +43 -0
  139. package/app/components/element/eject-menu.ts +359 -0
  140. package/app/components/element/eject-tracker.ts +34 -0
  141. package/app/components/element/onboarding-answers.ts +93 -0
  142. package/app/components/element/onboarding-intents.ts +19 -0
  143. package/app/composables/dataVizColors.ts +350 -0
  144. package/app/composables/formatting.ts +252 -0
  145. package/app/composables/semanticColors.ts +80 -0
  146. package/app/composables/useActivityOverlay.ts +55 -0
  147. package/app/composables/useAppShellNav.ts +21 -0
  148. package/app/composables/useChartBrush.ts +221 -0
  149. package/app/composables/useChartHover.ts +153 -0
  150. package/app/composables/useChartTickPlan.ts +186 -0
  151. package/app/composables/useCloseOnNavigation.ts +7 -0
  152. package/app/composables/useConfirm.ts +38 -0
  153. package/app/composables/useEdgeFade.ts +37 -0
  154. package/app/composables/useEject.ts +69 -0
  155. package/app/composables/useEntranceMotion.ts +22 -0
  156. package/app/composables/useFaviconBacking.ts +164 -0
  157. package/app/composables/useGridCols.ts +39 -0
  158. package/app/composables/useIssueOverlay.ts +43 -0
  159. package/app/composables/useOnboardingService.ts +106 -0
  160. package/app/composables/useOpenedOnce.ts +19 -0
  161. package/app/composables/useScrollSpy.ts +178 -0
  162. package/app/composables/useSquircleFallback.ts +21 -0
  163. package/app/composables/useWizardRoute.ts +123 -0
  164. package/app/plugins/favicon.ts +15 -0
  165. package/app/plugins/lazy-motion.ts +22 -0
  166. package/app/plugins/selection-highlight.client.ts +176 -0
  167. package/app/shared/cli.ts +10 -0
  168. package/app/shared/error-state.ts +150 -0
  169. package/app/shared/icons.ts +1 -0
  170. package/app/shared/motion.ts +1 -0
  171. package/app/shared/nav.ts +38 -0
  172. package/app/shared/navigation.ts +5 -0
  173. package/app/shared/static-terminal.ts +14 -0
  174. package/app/shared/table-export.ts +36 -0
  175. package/app/shared/table.ts +105 -0
  176. package/app/shared/ui-icons.ts +1 -0
  177. package/app/shared/view-state.ts +85 -0
  178. package/app/shared/widget-failure.ts +126 -0
  179. package/app/utils/app-favicon.ts +64 -0
  180. package/app/utils/chartAnnotations.ts +326 -0
  181. package/app/utils/countries.ts +337 -0
  182. package/app/utils/export-download.ts +100 -0
  183. package/app/utils/favicon-fallback.ts +12 -0
  184. package/app/utils/icon-color.ts +105 -0
  185. package/app/utils/number.ts +74 -0
  186. package/app/utils/render-link.ts +14 -0
  187. package/app/utils/slot-text.ts +18 -0
  188. package/app/utils/squircle.ts +154 -0
  189. package/app/utils/topEntityStack.ts +197 -0
  190. package/app/utils/vue.ts +57 -0
  191. package/build/design-system.ts +69 -0
  192. package/build/external-links.ts +38 -0
  193. package/build/fonts.ts +39 -0
  194. package/build/icon-server-bundle.ts +276 -0
  195. package/build/runtime/icon-server-bundle.ts +29 -0
  196. package/css/global.css +1896 -0
  197. package/nuxt.config.ts +143 -0
  198. package/package.json +93 -3
  199. package/public/fonts/README.md +33 -0
  200. package/public/fonts/fira-code-OFL.txt +93 -0
  201. package/public/fonts/fira-code-var-latin.woff2 +0 -0
  202. package/public/fonts/hubot-sans-OFL.txt +93 -0
  203. package/public/fonts/hubot-sans-var-latin.woff2 +0 -0
  204. package/server/api/favicon.get.ts +122 -0
  205. package/server/utils/favicon-source.ts +51 -0
  206. package/shared/contracts/feedback.ts +42 -0
  207. package/shared/contracts/index.ts +1 -0
  208. package/shared/delivery-channels.ts +53 -0
  209. package/shared/icons/index.ts +143 -0
  210. package/shared/icons/registry.ts +347 -0
  211. package/shared/icons/ui-slots.ts +58 -0
  212. package/shared/markdown.ts +44 -0
  213. package/shared/motion.ts +120 -0
  214. package/shared/rangi.ts +60 -0
  215. package/shared/rich-text.ts +34 -0
  216. package/shared/ui-icons.ts +6 -0
  217. package/shared/utils/urls.ts +64 -0
  218. package/types/index.ts +1 -0
  219. 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>