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