@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.
Files changed (225) hide show
  1. package/README.md +101 -2
  2. package/app/app.config.ts +596 -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 +232 -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 +125 -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 +55 -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 +68 -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 +29 -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/chartAnnotations.ts +326 -0
  180. package/app/utils/countries.ts +337 -0
  181. package/app/utils/export-download.ts +100 -0
  182. package/app/utils/favicon-fallback.ts +12 -0
  183. package/app/utils/icon-color.ts +105 -0
  184. package/app/utils/number.ts +74 -0
  185. package/app/utils/render-link.ts +14 -0
  186. package/app/utils/slot-text.ts +18 -0
  187. package/app/utils/squircle.ts +154 -0
  188. package/app/utils/topEntityStack.ts +197 -0
  189. package/app/utils/vue.ts +57 -0
  190. package/build/design-system.ts +47 -0
  191. package/build/icon-server-bundle.ts +276 -0
  192. package/build/runtime/icon-server-bundle.ts +29 -0
  193. package/css/global.css +1892 -0
  194. package/nuxt.config.ts +132 -0
  195. package/package.json +86 -3
  196. package/public/brand-icon-dark.png +0 -0
  197. package/public/brand-icon-light.png +0 -0
  198. package/public/favicon-dark.png +0 -0
  199. package/public/favicon-light.png +0 -0
  200. package/public/favicon.ico +0 -0
  201. package/public/fonts/README.md +31 -0
  202. package/public/fonts/fira-code-OFL.txt +93 -0
  203. package/public/fonts/fira-code-var-latin.woff2 +0 -0
  204. package/public/fonts/hubot-sans-OFL.txt +93 -0
  205. package/public/fonts/hubot-sans-var-latin.woff2 +0 -0
  206. package/public/icon-dark.png +0 -0
  207. package/public/icon.png +0 -0
  208. package/public/touch-icon-dark.png +0 -0
  209. package/public/touch-icon.png +0 -0
  210. package/server/api/favicon.get.ts +122 -0
  211. package/server/utils/favicon-source.ts +51 -0
  212. package/shared/contracts/feedback.ts +42 -0
  213. package/shared/contracts/index.ts +1 -0
  214. package/shared/delivery-channels.ts +53 -0
  215. package/shared/icons/index.ts +143 -0
  216. package/shared/icons/registry.ts +347 -0
  217. package/shared/icons/ui-slots.ts +58 -0
  218. package/shared/markdown.ts +44 -0
  219. package/shared/motion.ts +120 -0
  220. package/shared/rangi.ts +60 -0
  221. package/shared/rich-text.ts +34 -0
  222. package/shared/ui-icons.ts +6 -0
  223. package/shared/utils/urls.ts +64 -0
  224. package/types/index.ts +1 -0
  225. package/types/ui.ts +339 -0
@@ -0,0 +1,596 @@
1
+ import type { IconColorClasses } from './utils/icon-color'
2
+ import { UI_ICON_SLOTS } from '../shared/icons/ui-slots'
3
+
4
+ // Nuxt UI's internal icon slots reference semantic role names directly; @nuxt/icon's
5
+ // global aliases (registered by build/design-system.ts) resolve them to the
6
+ // chosen set's id at build time. So the framework primitives (selects, alerts,
7
+ // pagination, accordions…) follow the active icon set without this file pulling the
8
+ // role registry into the client runtime. Edit roles in shared/icons/registry.ts.
9
+ export default {
10
+ // Brand seams for a site that extends this layer. `iconColors` adds or remaps
11
+ // an icon colour role that getIconColor() resolves, e.g.
12
+ // `lime: { bg: 'bg-lime-500/10', text: 'text-lime-500' }`.
13
+ designSystem: {
14
+ iconColors: {} as Record<string, IconColorClasses>,
15
+ },
16
+ // Theme-aware browser and installed-app brand marks. touch-icon(-dark).png are
17
+ // deliberately not named apple-touch-icon*.png: nuxt-seo-utils auto-generates a
18
+ // <link rel="apple-touch-icon"> for any public file matching that convention,
19
+ // which duplicated the reactive one this plugin already emits.
20
+ favicon: {
21
+ light: '/favicon-light.png',
22
+ dark: '/favicon-dark.png',
23
+ appleTouchLight: '/touch-icon.png',
24
+ appleTouchDark: '/touch-icon-dark.png',
25
+ },
26
+ toaster: {
27
+ position: 'bottom-right' as const,
28
+ // Collapsed peek-deck that fans out on hover (Sonner/Linear). Read by UiToaster.
29
+ max: 3,
30
+ duration: 5000,
31
+ },
32
+ ui: {
33
+ colors: {
34
+ primary: 'violet',
35
+ neutral: 'slate',
36
+ pro: 'violet',
37
+ },
38
+ icons: UI_ICON_SLOTS,
39
+
40
+ // Button — primary is violet solid, secondary actions use semantic neutral tokens
41
+ // so light/dark modes both resolve correctly via --ui-* vars.
42
+ button: {
43
+ slots: {
44
+ base: 'cursor-pointer disabled:cursor-not-allowed aria-disabled:cursor-not-allowed',
45
+ item: '!py-0',
46
+ link: 'flex flex-col items-center justify-start w-full gap-0.5 w-[90px]',
47
+ linkLabel: 'text-xs text-toned',
48
+ linkTrailing: 'absolute right-2 top-2',
49
+ linkTrailingIcon: 'size-4 text-dimmed',
50
+ // inner menu
51
+ childList: '!grid-cols-5',
52
+ childLink: 'flex flex-col items-center justify-start w-full gap-0.5',
53
+ childLinkLabel: 'text-[10px] text-center text-muted',
54
+ childLinkIcon: 'size-6 !text-primary-400',
55
+ },
56
+ variants: {
57
+ variant: {
58
+ solid: '',
59
+ outline: 'ring-default hover:bg-elevated',
60
+ ghost: 'hover:bg-muted',
61
+ link: '',
62
+ },
63
+ },
64
+ compoundVariants: [
65
+ // Primary CTA — Vercel/PostHog-style inversion. bg-inverted/text-inverted
66
+ // auto-flip per mode (dark slate on light, white on dark), so contrast
67
+ // stays balanced without saturated brand colour fighting the surface.
68
+ // Mode-specific shadows: light mode gets a top inner sheen ("lit from
69
+ // above"); dark mode swaps to a hairline ring so the white button feels
70
+ // carved instead of floating. Hover lifts 1px, press returns and scales
71
+ // 0.99 — the 1000-small-details polish.
72
+ // Saturated violet is reserved for `color="pro"` (purchase/upgrade only).
73
+ {
74
+ color: 'primary',
75
+ variant: 'solid',
76
+ class: 'bg-inverted text-inverted shadow-[inset_0_1px_0_0_rgba(255,255,255,0.08),0_1px_2px_0_rgba(0,0,0,0.06)] dark:shadow-[0_1px_2px_0_rgba(0,0,0,0.3),0_0_0_1px_rgba(0,0,0,0.04)] hover:bg-inverted/90 hover:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.1),0_2px_4px_-1px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_2px_6px_-1px_rgba(0,0,0,0.4),0_0_0_1px_rgba(0,0,0,0.04)] disabled:bg-inverted/40 disabled:text-inverted/60 disabled:shadow-none aria-disabled:bg-inverted/40 aria-disabled:text-inverted/60 aria-disabled:shadow-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 transition-[background-color,box-shadow] duration-200 ease-[var(--ease-standard)]',
77
+ },
78
+ // Pro CTA — refined violet for purchase/upgrade moments. Same hover
79
+ // lift + press scale as primary so the gesture vocabulary is
80
+ // consistent; a top inner sheen and soft drop shadow give it that
81
+ // "lit from above" Stripe/Linear quality.
82
+ {
83
+ color: 'pro',
84
+ variant: 'solid',
85
+ class: 'bg-primary-600 hover:bg-primary-500 text-white shadow-[inset_0_1px_0_0_rgba(255,255,255,0.12),0_1px_2px_0_rgba(0,0,0,0.12)] hover:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.16),0_3px_8px_-1px_rgba(0,0,0,0.18)] disabled:bg-inverted/40 disabled:text-inverted/60 disabled:shadow-none aria-disabled:bg-inverted/40 aria-disabled:text-inverted/60 aria-disabled:shadow-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 transition-[background-color,box-shadow] duration-200 ease-[var(--ease-standard)]',
86
+ },
87
+ {
88
+ color: 'neutral',
89
+ variant: 'outline',
90
+ class: 'ring-default text-toned hover:bg-elevated focus-visible:ring-accented',
91
+ },
92
+ {
93
+ color: 'neutral',
94
+ variant: 'ghost',
95
+ class: 'text-muted hover:text-highlighted hover:bg-muted',
96
+ },
97
+ // Primary subtle/soft — keep monochrome. Nuxt UI's defaults pull
98
+ // bg-primary-50 + text-primary-700 (violet); we redirect to the
99
+ // elevated/muted semantic tokens so the surface stays neutral and
100
+ // colour is reserved for `color="pro"`.
101
+ {
102
+ color: 'primary',
103
+ variant: 'subtle',
104
+ class: 'bg-elevated text-default ring-default hover:bg-accented disabled:bg-elevated/60 disabled:text-muted aria-disabled:bg-elevated/60 aria-disabled:text-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
105
+ },
106
+ {
107
+ color: 'primary',
108
+ variant: 'soft',
109
+ class: 'bg-muted text-default hover:bg-accented disabled:bg-muted/60 disabled:text-muted aria-disabled:bg-muted/60 aria-disabled:text-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
110
+ },
111
+ {
112
+ color: 'primary',
113
+ variant: 'outline',
114
+ class: 'ring-default text-default hover:bg-elevated disabled:text-muted aria-disabled:text-muted focus-visible:ring-accented',
115
+ },
116
+ {
117
+ color: 'primary',
118
+ variant: 'ghost',
119
+ class: 'text-default hover:text-highlighted hover:bg-muted disabled:text-muted aria-disabled:text-muted',
120
+ },
121
+ ],
122
+ },
123
+
124
+ // Badge — semantic neutral tokens so soft/subtle variants flip correctly in light mode
125
+ badge: {
126
+ slots: {
127
+ base: 'rounded-md',
128
+ },
129
+ variants: {
130
+ size: {
131
+ xs: 'text-[10px] px-1 py-px leading-tight',
132
+ },
133
+ },
134
+ compoundVariants: [
135
+ {
136
+ color: 'neutral',
137
+ variant: 'soft',
138
+ class: 'bg-elevated text-muted',
139
+ },
140
+ {
141
+ color: 'neutral',
142
+ variant: 'subtle',
143
+ class: 'bg-elevated/50 ring-default text-muted',
144
+ },
145
+ {
146
+ color: 'neutral',
147
+ variant: 'outline',
148
+ class: 'ring-default text-dimmed',
149
+ },
150
+ {
151
+ color: 'primary',
152
+ variant: 'soft',
153
+ class: 'bg-primary-500/10 text-primary-500',
154
+ },
155
+ ],
156
+ },
157
+
158
+ // Card — cards sit at the "lg" radius tier per the radius-by-elevation rule.
159
+ // Arbitrary box-shadow syntax (rather than a `shadow-*` utility) keeps the
160
+ // CSS variable live at runtime so .dashboard-theme can swap it for the
161
+ // hairline ring. A regular shadow utility would inline the value at build.
162
+ card: {
163
+ slots: {
164
+ root: 'rounded-lg [box-shadow:var(--elevation-raised)]',
165
+ },
166
+ variants: {
167
+ variant: {
168
+ outline: {
169
+ root: 'bg-muted ring-default',
170
+ },
171
+ soft: {
172
+ root: 'bg-muted/50',
173
+ },
174
+ subtle: {
175
+ root: 'bg-muted ring-default',
176
+ },
177
+ },
178
+ },
179
+ },
180
+
181
+ // PageCard — same structural treatment as Card
182
+ pageCard: {
183
+ slots: {
184
+ root: 'rounded-lg [box-shadow:var(--elevation-raised)]',
185
+ title: 'text-highlighted',
186
+ description: 'text-muted',
187
+ leadingIcon: 'text-primary-500',
188
+ },
189
+ variants: {
190
+ variant: {
191
+ outline: {
192
+ root: 'bg-muted ring-default hover:ring-accented',
193
+ },
194
+ subtle: {
195
+ root: 'bg-muted ring-default',
196
+ },
197
+ soft: {
198
+ root: 'bg-muted/50',
199
+ },
200
+ },
201
+ },
202
+ },
203
+
204
+ // Input — rounded-md per DESIGN.md radius-by-elevation (inputs tier); full-width
205
+ // by default (the root wrapper is inline-flex otherwise, so inputs collapse to
206
+ // content width in form grids). Semantic tokens for background and ring.
207
+ input: {
208
+ slots: {
209
+ root: 'w-full',
210
+ base: 'w-full rounded-md bg-muted placeholder:text-dimmed',
211
+ },
212
+ variants: {
213
+ variant: {
214
+ outline: 'ring-default focus-visible:ring-accented',
215
+ subtle: 'bg-muted ring-default',
216
+ },
217
+ },
218
+ compoundVariants: [
219
+ {
220
+ color: 'primary',
221
+ variant: 'outline',
222
+ class: 'focus-visible:ring-primary-500/50',
223
+ },
224
+ ],
225
+ },
226
+
227
+ // Textarea - Match input styling (full-width, rounded-md). Auto-grow
228
+ // (field-sizing) is applied in global.css so it survives Tailwind's
229
+ // app.config class-scanning gap.
230
+ textarea: {
231
+ slots: {
232
+ root: 'w-full',
233
+ base: 'w-full rounded-md bg-muted placeholder:text-dimmed',
234
+ },
235
+ variants: {
236
+ variant: {
237
+ outline: 'ring-default focus-visible:ring-accented',
238
+ },
239
+ },
240
+ },
241
+
242
+ // Select — match input: full-width trigger, rounded-md, semantic ring so the
243
+ // native-style select doesn't read narrower/lighter than its sibling inputs.
244
+ select: {
245
+ slots: {
246
+ base: 'w-full rounded-md',
247
+ },
248
+ variants: {
249
+ variant: {
250
+ outline: 'ring-default focus-visible:ring-accented',
251
+ },
252
+ },
253
+ },
254
+
255
+ // Separator - Subtle divider
256
+ // A full pager wraps on a narrow screen instead of widening the page.
257
+ pagination: {
258
+ slots: {
259
+ list: 'flex-wrap justify-center sm:justify-start',
260
+ },
261
+ },
262
+ separator: {
263
+ slots: {
264
+ border: 'border-default',
265
+ },
266
+ variants: {
267
+ color: {
268
+ neutral: {
269
+ border: 'border-default',
270
+ },
271
+ },
272
+ },
273
+ },
274
+
275
+ // UiPageHeader - Documentation page headers
276
+ pageHeader: {
277
+ slots: {
278
+ root: '',
279
+ container: '',
280
+ wrapper: '',
281
+ headline: 'text-muted font-medium text-sm',
282
+ title: 'text-3xl md:text-4xl font-semibold tracking-tight text-highlighted leading-normal',
283
+ description: 'text-lg text-muted mt-2',
284
+ links: 'mt-4',
285
+ },
286
+ },
287
+
288
+ // PageBody - Documentation page content
289
+ pageBody: {
290
+ slots: {
291
+ root: '',
292
+ },
293
+ },
294
+
295
+ // ContentSurround - Prev/next navigation
296
+ contentSurround: {
297
+ slots: {
298
+ root: 'grid grid-cols-1 sm:grid-cols-2 gap-4',
299
+ link: 'grid grid-cols-[auto_minmax(0,1fr)] items-center gap-x-3 gap-y-0.5 p-4 rounded-lg border border-default hover:border-accented hover:bg-muted/50 transition-colors group',
300
+ linkLeading: 'row-span-2 self-center shrink-0',
301
+ linkLeadingIcon: 'size-5 text-dimmed group-hover:text-primary-500 transition-colors',
302
+ linkTitle: 'col-start-2 font-medium text-highlighted truncate group-hover:text-primary-400 transition-colors',
303
+ linkDescription: 'col-start-2 text-sm text-dimmed line-clamp-1',
304
+ },
305
+ },
306
+
307
+ // Link — modern inline link convention. Underline is always rendered but
308
+ // transparent by default; on hover the decoration color animates to the
309
+ // primary tint and text lifts to highlighted. Saturated violet stays
310
+ // reserved for CTAs/active nav (60-30-10). Apply to <ULink> usages.
311
+ link: {
312
+ base: 'underline underline-offset-4 decoration-1 decoration-transparent hover:decoration-[var(--ui-color-primary-500)] focus-visible:outline-primary transition-[color,text-decoration-color] duration-200',
313
+ variants: {
314
+ active: {
315
+ true: 'text-primary decoration-[var(--ui-color-primary-500)]',
316
+ false: 'text-default',
317
+ },
318
+ disabled: {
319
+ true: 'cursor-not-allowed opacity-75',
320
+ },
321
+ },
322
+ compoundVariants: [
323
+ {
324
+ active: false,
325
+ disabled: false,
326
+ class: 'hover:text-highlighted',
327
+ },
328
+ ],
329
+ },
330
+
331
+ // Tooltip - Subtle border, adapts to light/dark
332
+ tooltip: {
333
+ slots: {
334
+ content: 'bg-muted text-highlighted ring-default rounded-lg',
335
+ arrow: 'fill-[var(--ui-bg-muted)]',
336
+ },
337
+ },
338
+
339
+ // DropdownMenu — tooltip-grade chrome (5-layer shadow + inset edges) plus
340
+ // outline-ring hover on items and a left accent bar on the active item.
341
+ // Style only; behaviour stays default Nuxt UI v4 / Reka.
342
+ dropdownMenu: {
343
+ slots: {
344
+ content: 'ui-popover-content',
345
+ item: 'ui-dropdown-item rounded-md transition-colors items-center',
346
+ itemLeadingIcon: 'ui-dropdown-lead-icon !size-3 text-dimmed group-data-highlighted:!text-default',
347
+ itemLabel: 'tracking-[-0.005em]',
348
+ itemTrailingKbds: 'gap-0.5',
349
+ itemTrailingKbd: 'font-mono text-[10px] bg-muted rounded-sm px-1 py-px',
350
+ separator: 'ui-dropdown-separator mx-2 border-default',
351
+ },
352
+ },
353
+
354
+ // Popover — same chrome treatment as dropdown/tooltip.
355
+ popover: {
356
+ slots: {
357
+ content: 'ui-popover-content',
358
+ },
359
+ },
360
+
361
+ // ContextMenu — match dropdown chrome.
362
+ contextMenu: {
363
+ slots: {
364
+ content: 'ui-popover-content',
365
+ item: 'ui-dropdown-item rounded-md transition-colors items-center',
366
+ itemLeadingIcon: 'ui-dropdown-lead-icon !size-3 text-dimmed',
367
+ },
368
+ },
369
+
370
+ // Avatar - Subtle border
371
+ avatar: {
372
+ slots: {
373
+ root: 'ring-default',
374
+ },
375
+ },
376
+
377
+ // NavigationMenu - Semantic tokens so light/dark both work
378
+ navigationMenu: {
379
+ slots: {
380
+ item: '!py-0',
381
+ link: 'flex flex-col items-center justify-start w-full gap-0.5 w-[90px]',
382
+ linkLabel: 'text-xs text-muted',
383
+ linkTrailing: 'absolute right-2 top-2',
384
+ linkTrailingIcon: 'size-4 text-dimmed',
385
+ childList: '!grid-cols-5',
386
+ childLink: 'flex flex-col items-center justify-start w-full gap-0.5',
387
+ childLinkLabel: 'text-[10px] text-center text-dimmed',
388
+ childLinkIcon: 'size-6 !text-primary-400',
389
+ },
390
+ variants: {
391
+ active: {
392
+ false: {
393
+ linkLeadingIcon: 'text-muted',
394
+ },
395
+ true: {
396
+ link: 'text-primary-500',
397
+ },
398
+ },
399
+ },
400
+ },
401
+
402
+ // Accordion - Semantic tokens for light/dark parity
403
+ accordion: {
404
+ slots: {
405
+ root: 'border-default',
406
+ item: 'border-default',
407
+ trigger: 'text-highlighted hover:bg-muted',
408
+ content: 'text-muted',
409
+ },
410
+ },
411
+
412
+ // Drawer - Semantic tokens for light/dark parity
413
+ drawer: {
414
+ slots: {
415
+ content: 'bg-default ring-default',
416
+ handle: 'bg-accented',
417
+ },
418
+ },
419
+
420
+ // Tabs - Neutral white active to sit quietly below the sidebar's primary nav.
421
+ // Primary violet is reserved for CTAs; tabs are sub-navigation and should read
422
+ // as text-level emphasis with a thin white underline.
423
+ tabs: {
424
+ slots: {
425
+ trigger: 'text-muted',
426
+ indicator: 'rounded-full',
427
+ },
428
+ compoundVariants: [
429
+ {
430
+ variant: 'link',
431
+ class: {
432
+ trigger: 'data-[state=active]:text-highlighted',
433
+ indicator: 'bg-inverted',
434
+ },
435
+ },
436
+ ],
437
+ },
438
+
439
+ // Form elements
440
+ formField: {
441
+ slots: {
442
+ label: 'text-toned',
443
+ description: 'text-dimmed',
444
+ error: 'text-error',
445
+ },
446
+ },
447
+
448
+ // Checkbox - Primary color when checked
449
+ checkbox: {
450
+ slots: {
451
+ base: 'border-accented data-[state=checked]:bg-primary-500 data-[state=checked]:border-primary-500',
452
+ },
453
+ },
454
+
455
+ // SelectMenu - Dropdown with semantic tokens + overlay chrome
456
+ selectMenu: {
457
+ slots: {
458
+ content: 'bg-muted ring-default ui-popover-content',
459
+ item: 'ui-dropdown-item rounded-md text-toned data-[highlighted]:text-highlighted',
460
+ itemLeadingIcon: 'ui-dropdown-lead-icon !size-3 text-dimmed',
461
+ },
462
+ },
463
+ contentNavigation: {
464
+ slots: {
465
+ list: 'space-y-2',
466
+ listWithChildren: 'border-none transform mb-5',
467
+ },
468
+ variants: {
469
+ active: {
470
+ true: {
471
+ link: 'text-toned after:-left-[1px] font-semibold rounded-[10px] after:rounded-[10px] after:w-full after:h-full after:absolute after:bottom-0 after:block after:bg-elevated/50 after:shadow-xs',
472
+ },
473
+ false: {
474
+ link: 'text-toned',
475
+ linkLeadingIcon: 'text-dimmed',
476
+ },
477
+ },
478
+ },
479
+ compoundVariants: [
480
+ {
481
+ color: 'primary',
482
+ variant: 'pill',
483
+ active: true,
484
+ class: {
485
+ link: 'text-highlighted ',
486
+ },
487
+ },
488
+ ],
489
+ },
490
+ // Markdown rendering. The `prose-*` class names below carry no utilities;
491
+ // they are the stable hooks the "Docs prose" block in css/global.css keys
492
+ // on, so a defect in upstream module Markdown is fixed once, here, for
493
+ // docs + learn-seo + recipes.
494
+ prose: {
495
+ // The body-copy and code-pill treatment lives on `prose-callout` in
496
+ // css/global.css. A `text-default` variant override here only reached
497
+ // light mode: Nuxt UI tints with `text-{color}-600 dark:text-{color}-300`
498
+ // and tailwind-merge treats the `dark:` step as a separate key, so the
499
+ // override dropped the light class and left the dark one standing.
500
+ callout: {
501
+ slots: {
502
+ base: 'prose-callout',
503
+ icon: 'prose-callout-icon',
504
+ },
505
+ },
506
+ // Heading ladder: the h1 dominates, h2/h3 stay modest and the gap above a
507
+ // section is ~40px, not ~70px, so a new h2 reads as a section and not as
508
+ // a new page.
509
+ h2: {
510
+ slots: {
511
+ base: 'prose-h2 text-xl font-semibold mt-10 mb-3',
512
+ },
513
+ },
514
+ h3: {
515
+ slots: {
516
+ base: 'prose-h3 text-base font-semibold mt-8 mb-2',
517
+ },
518
+ },
519
+ h4: {
520
+ slots: {
521
+ base: 'prose-h4 text-sm font-semibold mt-6 mb-2',
522
+ },
523
+ },
524
+ ul: {
525
+ base: 'prose-ul',
526
+ },
527
+ // `prose-table-scroll` is the hook for the edge-shadow rule in
528
+ // css/global.css: a table wider than the article scrolls here, and
529
+ // nothing on the border said so.
530
+ table: {
531
+ slots: {
532
+ root: 'prose-table-scroll my-6 overflow-x-auto overscroll-x-contain rounded-lg border border-default bg-default',
533
+ base: 'prose-table w-full border-collapse rounded-none',
534
+ },
535
+ },
536
+ thead: {
537
+ base: 'bg-muted',
538
+ },
539
+ th: {
540
+ base: 'border-0 border-b border-default px-4 py-3 text-sm font-semibold text-highlighted text-left whitespace-nowrap first:border-s-0',
541
+ },
542
+ td: {
543
+ base: 'border-0 border-b border-default px-4 py-3 text-sm align-top',
544
+ },
545
+ tr: {
546
+ base: 'transition-colors [&:last-child>td]:border-b-0',
547
+ },
548
+ // Long type signatures (`urls: MaybeFunction<MaybePromise<...>>`) set the
549
+ // page's min-content width and scrolled the whole document sideways on a
550
+ // phone. The wrap rule lives in css/global.css on this hook.
551
+ code: {
552
+ base: 'prose-code',
553
+ },
554
+ // The filename header and the code body are one surface; the header was
555
+ // `bg-default` against a `bg-muted` body, which read as two cards.
556
+ pre: {
557
+ slots: {
558
+ header: 'bg-muted',
559
+ filename: 'text-muted',
560
+ // `prose-pre` is the hook for the scroll rule in css/global.css: a
561
+ // fenced block keeps its lines intact and scrolls sideways instead of
562
+ // wrapping a URL or a type signature mid-token.
563
+ base: 'prose-pre',
564
+ // The copy button renders at 28px. Extend the hit area to the 44px
565
+ // touch floor with a pseudo-element instead of growing the glyph.
566
+ copy: 'after:absolute after:-inset-2 after:content-[\'\']',
567
+ },
568
+ },
569
+ codeGroup: {
570
+ slots: {
571
+ // The strip still scrolls, but at 390px a five-tab install group ran
572
+ // 7px past the viewport and cut the last icon in half. Tighter
573
+ // padding below `sm` fits it.
574
+ list: 'bg-muted max-sm:gap-0.5 max-sm:p-1.5',
575
+ // `prose-code-tab`/`-icon` are the hooks for the brand-mark rule in
576
+ // css/global.css: five full-colour logos read as a wash on a
577
+ // near-monochrome page, so only the selected tab spends colour.
578
+ trigger: 'prose-code-tab max-sm:px-1.5 text-muted data-[state=active]:text-highlighted',
579
+ triggerIcon: 'prose-code-tab-icon size-3.5',
580
+ },
581
+ },
582
+ codeIcon: {
583
+ 'robots.txt': 'vscode-icons:file-type-robots',
584
+ 'txt': 'vscode-icons:file-type-text',
585
+ },
586
+ a: {
587
+ base: [
588
+ 'prose-link relative border-none underline underline-offset-4 text-default decoration-transition-all decoration-opacity-70 decoration-[0.1rem] decoration-[var(--ui-text-dimmed)]',
589
+ // set a :after border under the link
590
+ 'transition-[color,text-decoration-color,text-underline-offset,text-decoration-thickness]',
591
+ 'hover:text-dimmed decoration-opacity-100 hover:underline-offset-1 hover:decoration-[0.1rem]',
592
+ ],
593
+ },
594
+ },
595
+ },
596
+ }
@@ -0,0 +1,8 @@
1
+ import type { AriaAttributes, HTMLAttributes } from 'vue'
2
+
3
+ declare module 'vue' {
4
+ // Components pass these standard attributes and listeners through $attrs.
5
+ interface ComponentCustomProps extends AriaAttributes, Pick<HTMLAttributes, 'autocapitalize' | 'autocorrect' | 'spellcheck' | 'onBlur' | 'onFocus' | 'onPointermove' | 'inputmode' | 'onLoad' | 'role' | 'tabindex' | 'onInput' | 'onKeydown' | 'onKeyup' | 'onPointerdown' | 'onPaste'> {
6
+ onClick?: ((event: MouseEvent) => void) | Array<(event: MouseEvent) => void>
7
+ }
8
+ }
@@ -0,0 +1,38 @@
1
+ # Design system components
2
+
3
+ Canonical UI primitives. Every component here must render with prop fixtures alone: no `useFetch`, no [Drizzle](https://orm.drizzle.team), no Caller, no auth/billing state. Domain data is passed in via props or slots, never resolved internally.
4
+
5
+ See [ADR-0029](../../../docs/adr/0029-component-placement-contract.md) for the placement contract that decides what lives here vs in `layers/saas/`, `apps/pro/`, or a feature layer.
6
+
7
+ ## Taxonomy
8
+
9
+ Three subdirs by composition shape. Pick by what the component is for, not what it looks like.
10
+
11
+ ### `container/`
12
+
13
+ Wraps other content and manages its surface, layout, or portal. Has at least one `<slot />`. Renaming "Card" without children is a leaf; renaming with children is a container.
14
+
15
+ Current: `Alert`, `Card`, `DateRangePicker`, `EmptyState`, `LoadingCard`, `LockedCard`, `PageHeader`, `SampleDataOverlay`, `SectionHeader`, `UiNoData`, `UiPopover`, `UiWidgetState`.
16
+
17
+ ### `data/`
18
+
19
+ Renders structured data: tables, rows of metrics, structured cells. Takes a row/value/series and decides how to present it. Often slotted into a `container/`.
20
+
21
+ Current: `DataList`, `FacetStrip`, `MetricCard`, `MetricLabel`, `MetricsRow`, `MetricToggle`, `SparklineLoader`, `UiBarChart`, `UiDonutChart`, `UiLineChart`, `UiSparkline`, `UiStat`, `UiStats`, `UiTable*` family, `UiDataTableSection`, `cells/`.
22
+
23
+ ### `element/`
24
+
25
+ Leaf primitives. No nested slots beyond a single optional `<slot />`. Includes dots, badges, chips, icons, buttons, and single-purpose displays.
26
+
27
+ Current: `Chip`, `DotLabel`, `EjectMenu`, `Favicon`, `HealthDot`, `ProgressPercent`, `SeverityDot`, `StatusBadge`, `SyncDot`, `TogglePill`, `UiHelpLabel`, `UiButton`, `UiPillSelect`, `UiProgressCircle`, `UiSkeleton`, `UiTooltip`, `UiTooltipGrid`, `UiTrend`.
28
+
29
+ ## Naming rules
30
+
31
+ - **No scope prefixes.** Not `Pro*`, not `Admin*`, not `Tool*`, not `Docs*`. The layer name is the scope. Promotion from `pro-saas` → here drops the prefix in the same PR ([ADR-0029](../../../docs/adr/0029-component-placement-contract.md)).
32
+ - **`Ui*` is grandfathered**, not preferred. New primitives don't add the prefix; existing `Ui*` files keep it until a rename batch is convenient.
33
+ - **PascalCase**, single basename word where possible (`Card`, not `CardSurface`; `Chip`, not `BadgeChip`).
34
+ - **Auto-import registers them under the bare name** (see `nuxt.config.ts` here: `pathPrefix: false`).
35
+
36
+ ## Priority
37
+
38
+ The design-system layer registers its components dir with `priority: 10` (see `nuxt.config.ts`). This wins shadow conflicts against same-basename files in other layers (e.g. `apps/site/app/components/content/Alert.vue`, owned by the content team). When you add a new primitive, check `ripast components --dups` to confirm it doesn't introduce a new shadow that's load-bearing somewhere.