@voltro/ui-shadcn 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 (72) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +57 -0
  3. package/README.md +26 -0
  4. package/SECURITY.md +56 -0
  5. package/THIRD-PARTY-NOTICES.md +3016 -0
  6. package/dist/brand.d.ts +73 -0
  7. package/dist/brand.js +177 -0
  8. package/dist/cn.d.ts +11 -0
  9. package/dist/cn.js +6 -0
  10. package/dist/index.d.ts +1183 -0
  11. package/dist/index.js +2636 -0
  12. package/dist/tokens.css +532 -0
  13. package/package.json +64 -0
  14. package/src/brand/README.md +60 -0
  15. package/src/brand/assets/voltro-favicon.svg +30 -0
  16. package/src/brand/assets/voltro-icon-dark.svg +26 -0
  17. package/src/brand/assets/voltro-icon.svg +14 -0
  18. package/src/brand/assets/voltro-mark-mono.svg +5 -0
  19. package/src/brand/assets/voltro-mark.svg +14 -0
  20. package/src/brand/voltroLogo.tsx +244 -0
  21. package/src/cn.ts +10 -0
  22. package/src/compositions/appShell.tsx +72 -0
  23. package/src/compositions/codeCompare.tsx +88 -0
  24. package/src/compositions/docShell.tsx +112 -0
  25. package/src/compositions/docsLayout.tsx +577 -0
  26. package/src/compositions/featureBento.tsx +103 -0
  27. package/src/compositions/featureGrid.tsx +41 -0
  28. package/src/compositions/heroSection.tsx +55 -0
  29. package/src/compositions/landingCta.tsx +85 -0
  30. package/src/compositions/landingHero.tsx +174 -0
  31. package/src/compositions/landingStats.tsx +99 -0
  32. package/src/compositions/loginCard.tsx +139 -0
  33. package/src/compositions/pageHeader.tsx +58 -0
  34. package/src/compositions/profileMenu.tsx +316 -0
  35. package/src/compositions/siteFooter.tsx +250 -0
  36. package/src/compositions/themeToggle.tsx +82 -0
  37. package/src/cookies.ts +109 -0
  38. package/src/index.ts +160 -0
  39. package/src/primitives/animatedNumber.tsx +73 -0
  40. package/src/primitives/avatar.tsx +39 -0
  41. package/src/primitives/badge.tsx +39 -0
  42. package/src/primitives/button.tsx +53 -0
  43. package/src/primitives/callout.tsx +97 -0
  44. package/src/primitives/card.tsx +68 -0
  45. package/src/primitives/checkbox.tsx +55 -0
  46. package/src/primitives/codeBlock.tsx +134 -0
  47. package/src/primitives/codeWindow.tsx +84 -0
  48. package/src/primitives/dialog.tsx +43 -0
  49. package/src/primitives/docCard.tsx +109 -0
  50. package/src/primitives/docIcons.tsx +268 -0
  51. package/src/primitives/dropdownMenu.tsx +162 -0
  52. package/src/primitives/gridOverlay.tsx +51 -0
  53. package/src/primitives/highlightedCode.tsx +112 -0
  54. package/src/primitives/input.tsx +25 -0
  55. package/src/primitives/label.tsx +19 -0
  56. package/src/primitives/localeSwitcher.tsx +90 -0
  57. package/src/primitives/meshBackdrop.tsx +62 -0
  58. package/src/primitives/scrollReveal.tsx +70 -0
  59. package/src/primitives/searchModal.tsx +304 -0
  60. package/src/primitives/select.tsx +24 -0
  61. package/src/primitives/separator.tsx +24 -0
  62. package/src/primitives/skeleton.tsx +12 -0
  63. package/src/primitives/sparkles.tsx +105 -0
  64. package/src/primitives/steps.tsx +55 -0
  65. package/src/primitives/tabs.tsx +102 -0
  66. package/src/primitives/textarea.tsx +24 -0
  67. package/src/primitives/toast.tsx +44 -0
  68. package/src/primitives/tocScrollSpy.tsx +110 -0
  69. package/src/primitives/toggle.tsx +50 -0
  70. package/src/primitives/toggleGroup.tsx +62 -0
  71. package/src/tokens.css +532 -0
  72. package/src/widgets.tsx +297 -0
package/src/cookies.ts ADDED
@@ -0,0 +1,109 @@
1
+ // User-preference cookies — the canonical names the framework's pre-paint
2
+ // theme script + auto-wired i18n resolver agree on (innovation/00). The kit's
3
+ // ProfileMenu / ThemeToggle write them; the server parses them at SSR time.
4
+ //
5
+ // Why cookies (and not localStorage)? Cookies travel on every HTTP request,
6
+ // so the SERVER can read them and bake the correct HTML (dark class,
7
+ // translated strings) BEFORE first paint — no theme/locale flash.
8
+ // localStorage is browser-only and would force a post-hydration flicker.
9
+ //
10
+ // Every helper is isomorphic: pass the request `Cookie` header on the server,
11
+ // omit it in the browser (falls back to `document.cookie`).
12
+
13
+ export const THEME_COOKIE = 'voltro:theme'
14
+ export const LANG_COOKIE = 'voltro:lang'
15
+
16
+ export type ThemePreference = 'system' | 'light' | 'dark'
17
+
18
+ const isThemePreference = (v: string): v is ThemePreference =>
19
+ v === 'system' || v === 'light' || v === 'dark'
20
+
21
+ // 400 days — Chrome caps cookie lifetime at 400d (RFC 6265bis). Picking the
22
+ // max so the preference survives long absences without being silently
23
+ // re-defaulted.
24
+ const DEFAULT_MAX_AGE_SEC = 60 * 60 * 24 * 400
25
+
26
+ /** Read a cookie by name from a header string (SSR: the request `Cookie`
27
+ * header) or, when omitted, `document.cookie` (browser). Returns undefined
28
+ * when absent or off-document. */
29
+ export const getCookie = (name: string, cookieHeader?: string): string | undefined => {
30
+ const header = cookieHeader ?? (typeof document !== 'undefined' ? document.cookie : '')
31
+ if (!header) return undefined
32
+ for (const part of header.split(';')) {
33
+ const eq = part.indexOf('=')
34
+ if (eq === -1) continue
35
+ if (part.slice(0, eq).trim() === name) {
36
+ try { return decodeURIComponent(part.slice(eq + 1).trim()) } catch { return part.slice(eq + 1).trim() }
37
+ }
38
+ }
39
+ return undefined
40
+ }
41
+
42
+ export interface SetCookieOptions {
43
+ /** Default: 400 days (Chrome's cap). Pass 0 to delete the cookie. */
44
+ readonly maxAgeSeconds?: number
45
+ /** Default: '/' — preference applies app-wide. */
46
+ readonly path?: string
47
+ /** Default: 'lax' — allows top-level navigation but blocks cross-site
48
+ * XHR, which is what preference cookies want. 'none' implies `Secure`
49
+ * (modern browsers reject a SameSite=None cookie without it). */
50
+ readonly sameSite?: 'lax' | 'strict' | 'none'
51
+ }
52
+
53
+ /** Build a `Set-Cookie`-style string AND, in the browser, write it to
54
+ * `document.cookie`. Returns the string so SSR callers can emit a header. */
55
+ export const setCookie = (name: string, value: string, options: SetCookieOptions = {}): string => {
56
+ const { maxAgeSeconds = DEFAULT_MAX_AGE_SEC, path = '/', sameSite = 'lax' } = options
57
+ const parts = [
58
+ `${name}=${encodeURIComponent(value)}`,
59
+ `Path=${path}`,
60
+ `Max-Age=${maxAgeSeconds}`,
61
+ `SameSite=${sameSite}`,
62
+ ]
63
+ if (sameSite === 'none') parts.push('Secure')
64
+ const cookie = parts.join('; ')
65
+ if (typeof document !== 'undefined') document.cookie = cookie
66
+ return cookie
67
+ }
68
+
69
+ /** Expire a cookie (Max-Age=0). Same isomorphic contract as `setCookie`:
70
+ * writes `document.cookie` in the browser, returns the header string. */
71
+ export const deleteCookie = (name: string, path = '/'): string =>
72
+ setCookie(name, '', { maxAgeSeconds: 0, path })
73
+
74
+ export interface PreferenceCookies {
75
+ /** undefined → server should fall back to its default (typically the
76
+ * app config's theme). 'system' means the client's pre-paint script
77
+ * decides against `prefers-color-scheme`. */
78
+ readonly theme?: ThemePreference
79
+ /** Language code as written by the client. Consumer decides which codes
80
+ * are valid + how to fall back if the value is unknown. */
81
+ readonly lang?: string
82
+ }
83
+
84
+ /** Extract both preference cookies (theme + lang) from a cookie header (or
85
+ * `document.cookie` when omitted). Tolerates `null`/`undefined` headers.
86
+ * An invalid theme value is dropped (not coerced); an empty lang is dropped. */
87
+ export const parsePreferenceCookies = (cookieHeader?: string | null): PreferenceCookies => {
88
+ const header = cookieHeader ?? undefined
89
+ const themeRaw = getCookie(THEME_COOKIE, header)
90
+ const lang = getCookie(LANG_COOKIE, header)
91
+ return {
92
+ ...(themeRaw !== undefined && isThemePreference(themeRaw) ? { theme: themeRaw } : {}),
93
+ ...(lang !== undefined && lang !== '' ? { lang } : {}),
94
+ }
95
+ }
96
+
97
+ /** Apply a theme preference to `<html>` (browser only) — adds/removes the
98
+ * `dark` class, resolving `system` against `prefers-color-scheme`. The kit's
99
+ * ProfileMenu / ThemeToggle call this after writing the cookie so the change
100
+ * is immediate (the pre-paint `themeBootScript` handles the first load). */
101
+ export const applyTheme = (theme: ThemePreference): void => {
102
+ if (typeof document === 'undefined') return
103
+ const dark =
104
+ theme === 'dark' ||
105
+ (theme === 'system' &&
106
+ typeof window !== 'undefined' &&
107
+ window.matchMedia?.('(prefers-color-scheme: dark)').matches === true)
108
+ document.documentElement.classList.toggle('dark', dark)
109
+ }
package/src/index.ts ADDED
@@ -0,0 +1,160 @@
1
+ // @voltro/ui-shadcn — Voltro's first-party shadcn/ui kit.
2
+ //
3
+ // Surfaces:
4
+ //
5
+ // Widgets — `shadcnWidgets`: Tailwind-styled implementations of the
6
+ // @voltro/ui widget seam. Drop into
7
+ // `<WidgetRegistryProvider>` to style every AutoForm.
8
+ //
9
+ // Primitives — Button/Card/Input/… wrap an HTML element with
10
+ // theme-aware styles. Copy into your project if you want
11
+ // to own them outright; importing here works when you
12
+ // don't want to diverge from the framework's defaults.
13
+ //
14
+ // Compositions — opinionated layouts (LoginCard, HeroSection, DocShell,
15
+ // AppShell, DocsLayout, ProfileMenu, ThemeToggle).
16
+ // Updated as the framework evolves. Apps adopting them
17
+ // get design refreshes for free.
18
+ //
19
+ // Cookies — the canonical `voltro:theme` / `voltro:lang`
20
+ // preference-cookie helpers the framework's SSR theme +
21
+ // i18n resolution agree on.
22
+ //
23
+ // Tokens live in `@voltro/ui-shadcn/tokens.css`. Import it once at the top
24
+ // of your app's globals.css; add the kit `@source` so Tailwind scans the
25
+ // kit's classes (see the tokens.css header / the styling docs).
26
+
27
+ export const UI_SHADCN_NAME = 'framework-ui-shadcn' as const
28
+
29
+ export { cn } from './cn'
30
+
31
+ // ---- Widget seam ----
32
+ export { shadcnWidgets } from './widgets'
33
+
34
+ // ---- User-preference cookies (theme + language) ----
35
+ export {
36
+ THEME_COOKIE, LANG_COOKIE,
37
+ getCookie, setCookie, deleteCookie,
38
+ parsePreferenceCookies, applyTheme,
39
+ } from './cookies'
40
+ export type {
41
+ ThemePreference, PreferenceCookies, SetCookieOptions,
42
+ } from './cookies'
43
+
44
+ // ---- Primitives ----
45
+ export { Button, buttonVariants } from './primitives/button'
46
+ export type { ButtonProps } from './primitives/button'
47
+
48
+ export {
49
+ Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter,
50
+ } from './primitives/card'
51
+
52
+ export { Input } from './primitives/input'
53
+ export { Textarea } from './primitives/textarea'
54
+ export { Checkbox, type CheckboxProps } from './primitives/checkbox'
55
+ export { Label } from './primitives/label'
56
+
57
+ export { Badge, badgeVariants } from './primitives/badge'
58
+ export type { BadgeProps } from './primitives/badge'
59
+
60
+ export { Separator } from './primitives/separator'
61
+
62
+ export { Avatar } from './primitives/avatar'
63
+ export { Skeleton } from './primitives/skeleton'
64
+ export { Select } from './primitives/select'
65
+
66
+ export {
67
+ Dialog, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
68
+ } from './primitives/dialog'
69
+
70
+ export {
71
+ Tabs, TabsList, TabsTrigger, TabsContent,
72
+ } from './primitives/tabs'
73
+
74
+ export { Toast, ToastTitle, ToastDescription } from './primitives/toast'
75
+
76
+ export {
77
+ DropdownMenu, DropdownMenuTrigger, DropdownMenuContent,
78
+ DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel,
79
+ DropdownMenuGroup, DropdownMenuPortal,
80
+ DropdownMenuRadioGroup, DropdownMenuRadioItem,
81
+ } from './primitives/dropdownMenu'
82
+
83
+ export { Toggle, toggleVariants } from './primitives/toggle'
84
+ export { ToggleGroup, ToggleGroupItem } from './primitives/toggleGroup'
85
+
86
+ export { LocaleSwitcher } from './primitives/localeSwitcher'
87
+ export type { LocaleSwitcherProps, LocaleOption } from './primitives/localeSwitcher'
88
+
89
+ // ---- Compositions ----
90
+ export { LoginCard } from './compositions/loginCard'
91
+ export type { LoginCardLabels } from './compositions/loginCard'
92
+ export { HeroSection } from './compositions/heroSection'
93
+ export { FeatureGrid } from './compositions/featureGrid'
94
+ export { DocShell } from './compositions/docShell'
95
+ export type { DocNavEntry, DocNavGroup, ShellLinkProps } from './compositions/docShell'
96
+ export { DocsLayout } from './compositions/docsLayout'
97
+ export type {
98
+ DocsNavItem, DocsNavGroup, DocsNavSubGroup, DocsTocEntry, DocsBreadcrumb,
99
+ DocsLayoutLabels,
100
+ } from './compositions/docsLayout'
101
+ export { AppShell } from './compositions/appShell'
102
+ export { PageHeader } from './compositions/pageHeader'
103
+ export { ProfileMenu } from './compositions/profileMenu'
104
+ export type {
105
+ ProfileMenuProps, ProfileMenuLink, ProfileMenuLanguage, ProfileMenuLabels,
106
+ } from './compositions/profileMenu'
107
+ export { ThemeToggle, themeBootScript } from './compositions/themeToggle'
108
+
109
+ // ---- Brand ----
110
+ export {
111
+ VoltroMark, VoltroIcon, VoltroWordmark,
112
+ VOLTRO_VIOLET, VOLTRO_BOLT_PATH,
113
+ } from './brand/voltroLogo'
114
+ export type {
115
+ VoltroMarkProps, VoltroMarkVariant,
116
+ VoltroIconProps, VoltroIconTone,
117
+ VoltroWordmarkProps,
118
+ } from './brand/voltroLogo'
119
+
120
+ // ---- Landing-page compositions ----
121
+ export { LandingHero } from './compositions/landingHero'
122
+ export { Bento, BentoCell } from './compositions/featureBento'
123
+ export type { BentoCellProps } from './compositions/featureBento'
124
+ export { CodeCompare } from './compositions/codeCompare'
125
+ export { LandingStats } from './compositions/landingStats'
126
+ export type { StatSpec } from './compositions/landingStats'
127
+ export { LandingCta } from './compositions/landingCta'
128
+ export { SiteFooter, StatusBadge } from './compositions/siteFooter'
129
+ export type {
130
+ SiteFooterColumn, SiteFooterLink, SiteFooterSocial,
131
+ } from './compositions/siteFooter'
132
+
133
+ // ---- Backdrop + animation primitives ----
134
+ export { MeshBackdrop } from './primitives/meshBackdrop'
135
+ export { GridOverlay } from './primitives/gridOverlay'
136
+ export { Sparkles } from './primitives/sparkles'
137
+ export { CodeWindow } from './primitives/codeWindow'
138
+ export { ScrollReveal } from './primitives/scrollReveal'
139
+ export { AnimatedNumber } from './primitives/animatedNumber'
140
+ export {
141
+ HighlightedCode, whenHighlighterReady, SHIKI_LANGS,
142
+ } from './primitives/highlightedCode'
143
+ export type { ShikiLang } from './primitives/highlightedCode'
144
+
145
+ // ---- Doc-content primitives ----
146
+ export { Callout } from './primitives/callout'
147
+ export { CodeBlock } from './primitives/codeBlock'
148
+ export { Steps, Step } from './primitives/steps'
149
+ export { DocCard, DocCards } from './primitives/docCard'
150
+ export { TocScrollSpy } from './primitives/tocScrollSpy'
151
+ export {
152
+ RocketIcon, CompassIcon, PackageIcon, FolderIcon, FileIcon,
153
+ BroadcastIcon, PencilIcon, BellIcon, CogIcon, BuildingIcon,
154
+ DatabaseIcon, LockIcon, BotIcon, TerminalIcon, LayersIcon,
155
+ PuzzleIcon, HookIcon, GlobeIcon, CloudIcon, HomeIcon,
156
+ ChevronLeftIcon, ChevronRightIcon, SearchIcon, ArrowReturnIcon,
157
+ GitHubIcon, XIcon, DiscordIcon, LinkedInIcon, RssIcon, ChartIcon,
158
+ } from './primitives/docIcons'
159
+ export { SearchModal, SearchTrigger } from './primitives/searchModal'
160
+ export type { SearchModalProps, SearchModalLabels } from './primitives/searchModal'
@@ -0,0 +1,73 @@
1
+ // AnimatedNumber — counts from 0 to the target value when scrolled
2
+ // into view. Mounts an IntersectionObserver, runs ONE rAF loop for
3
+ // ~700ms, then stops. No re-trigger.
4
+ //
5
+ // Use for the stats row on the landing — gives the impression of
6
+ // "live numbers" without actually polling anything.
7
+ //
8
+ // Respects prefers-reduced-motion (jumps straight to the final value).
9
+
10
+ import { useEffect, useRef, useState, type ReactNode } from 'react'
11
+
12
+ interface AnimatedNumberProps {
13
+ /** Target value. */
14
+ readonly value: number
15
+ /** Formatting — `'integer'` (default), `'percent'`, or a custom
16
+ * formatter. */
17
+ readonly format?: 'integer' | 'percent' | ((n: number) => string)
18
+ /** Animation duration in ms. Default 800. */
19
+ readonly duration?: number
20
+ }
21
+
22
+ const fmt = (n: number, format: AnimatedNumberProps['format']): string => {
23
+ if (typeof format === 'function') return format(n)
24
+ if (format === 'percent') return `${n.toFixed(0)}%`
25
+ return Math.round(n).toLocaleString('en-US')
26
+ }
27
+
28
+ export const AnimatedNumber = ({
29
+ value, format = 'integer', duration = 800,
30
+ }: AnimatedNumberProps): ReactNode => {
31
+ const ref = useRef<HTMLSpanElement>(null)
32
+ const [current, setCurrent] = useState(0)
33
+
34
+ useEffect(() => {
35
+ if (typeof IntersectionObserver === 'undefined') {
36
+ setCurrent(value)
37
+ return
38
+ }
39
+ const prefersReduced = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
40
+ if (prefersReduced) {
41
+ setCurrent(value)
42
+ return
43
+ }
44
+ const node = ref.current
45
+ if (!node) return
46
+ const observer = new IntersectionObserver((entries) => {
47
+ for (const entry of entries) {
48
+ if (entry.isIntersecting) {
49
+ observer.disconnect()
50
+ const start = performance.now()
51
+ const tick = (now: number): void => {
52
+ const t = Math.min((now - start) / duration, 1)
53
+ // ease-out cubic — fast at first, gentle stop
54
+ const eased = 1 - Math.pow(1 - t, 3)
55
+ setCurrent(value * eased)
56
+ if (t < 1) requestAnimationFrame(tick)
57
+ else setCurrent(value)
58
+ }
59
+ requestAnimationFrame(tick)
60
+ return
61
+ }
62
+ }
63
+ }, { threshold: 0.4 })
64
+ observer.observe(node)
65
+ return () => observer.disconnect()
66
+ }, [value, duration])
67
+
68
+ return (
69
+ <span ref={ref} className="font-tabular">
70
+ {fmt(current, format)}
71
+ </span>
72
+ )
73
+ }
@@ -0,0 +1,39 @@
1
+ // shadcn Avatar — circular image with text fallback.
2
+ // Minimal: no async image-state machine, plain <img> + <span> fallback.
3
+ // Upgrade to Radix Avatar when load-state UX matters.
4
+
5
+ import type { HTMLAttributes, ReactNode } from 'react'
6
+ import { cn } from '../cn'
7
+
8
+ interface AvatarProps extends HTMLAttributes<HTMLDivElement> {
9
+ readonly src?: string
10
+ readonly alt?: string
11
+ readonly fallback?: string
12
+ readonly size?: 'sm' | 'md' | 'lg'
13
+ }
14
+
15
+ const sizeClasses = {
16
+ sm: 'size-6 text-xs',
17
+ md: 'size-9 text-sm',
18
+ lg: 'size-12 text-base',
19
+ } as const
20
+
21
+ export const Avatar = ({ src, alt, fallback, size = 'md', className, ...props }: AvatarProps): ReactNode => (
22
+ <div
23
+ data-slot="avatar"
24
+ className={cn(
25
+ 'relative inline-flex items-center justify-center overflow-hidden rounded-full bg-muted',
26
+ sizeClasses[size],
27
+ className,
28
+ )}
29
+ {...props}
30
+ >
31
+ {src ? (
32
+ <img src={src} alt={alt ?? ''} className="size-full object-cover" />
33
+ ) : (
34
+ <span className="font-medium text-muted-foreground select-none">
35
+ {fallback ?? '?'}
36
+ </span>
37
+ )}
38
+ </div>
39
+ )
@@ -0,0 +1,39 @@
1
+ // shadcn Badge — small pill for status, tags, count indicators.
2
+
3
+ import type { HTMLAttributes, ReactNode } from 'react'
4
+ import { cva, type VariantProps } from 'class-variance-authority'
5
+ import { cn } from '../cn'
6
+
7
+ const badgeVariants = cva(
8
+ 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 ' +
9
+ '[&>svg]:size-3 [&>svg]:pointer-events-none gap-1 transition-[color,box-shadow] overflow-hidden ' +
10
+ 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] ' +
11
+ 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
12
+ {
13
+ variants: {
14
+ variant: {
15
+ default: 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
16
+ secondary: 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
17
+ destructive: 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90',
18
+ outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
19
+ },
20
+ },
21
+ defaultVariants: { variant: 'default' },
22
+ },
23
+ )
24
+
25
+ export interface BadgeProps
26
+ extends HTMLAttributes<HTMLSpanElement>,
27
+ VariantProps<typeof badgeVariants> {}
28
+
29
+ export const Badge = ({ className, variant, children, ...props }: BadgeProps): ReactNode => (
30
+ <span
31
+ data-slot="badge"
32
+ className={cn(badgeVariants({ variant }), className)}
33
+ {...props}
34
+ >
35
+ {children}
36
+ </span>
37
+ )
38
+
39
+ export { badgeVariants }
@@ -0,0 +1,53 @@
1
+ // shadcn Button — variants via class-variance-authority. Drops the
2
+ // `asChild` slot escape hatch in V1 to keep transitive dependencies
3
+ // minimal; reach for the dashboard's full component if you need it.
4
+
5
+ import type { ButtonHTMLAttributes, ReactNode } from 'react'
6
+ import { cva, type VariantProps } from 'class-variance-authority'
7
+ import { cn } from '../cn'
8
+
9
+ const buttonVariants = cva(
10
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium " +
11
+ "transition-all disabled:pointer-events-none disabled:opacity-50 " +
12
+ "[&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 " +
13
+ "outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] " +
14
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer",
15
+ {
16
+ variants: {
17
+ variant: {
18
+ default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
19
+ destructive: 'bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90',
20
+ outline: 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground ' +
21
+ 'dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
22
+ secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
23
+ ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
24
+ link: 'text-primary underline-offset-4 hover:underline',
25
+ },
26
+ size: {
27
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
28
+ sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
29
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
30
+ icon: 'size-9',
31
+ },
32
+ },
33
+ defaultVariants: { variant: 'default', size: 'default' },
34
+ },
35
+ )
36
+
37
+ export interface ButtonProps
38
+ extends ButtonHTMLAttributes<HTMLButtonElement>,
39
+ VariantProps<typeof buttonVariants> {}
40
+
41
+ export const Button = ({
42
+ className, variant, size, children, ...props
43
+ }: ButtonProps): ReactNode => (
44
+ <button
45
+ data-slot="button"
46
+ className={cn(buttonVariants({ variant, size, className }))}
47
+ {...props}
48
+ >
49
+ {children}
50
+ </button>
51
+ )
52
+
53
+ export { buttonVariants }
@@ -0,0 +1,97 @@
1
+ // Callout — coloured note box for docs content. Four variants matching
2
+ // the Fumadocs/Docusaurus convention: info (default), tip, warning,
3
+ // danger. Uses semantic CSS tokens so dark/light both work without
4
+ // per-variant overrides in app code.
5
+ //
6
+ // Usage:
7
+ // <Callout type="warning" title="Heads up">
8
+ // This API is still pre-release. Names + shape can change.
9
+ // </Callout>
10
+
11
+ import type { ReactNode } from 'react'
12
+ import { cn } from '../cn'
13
+
14
+ type CalloutType = 'info' | 'tip' | 'warning' | 'danger' | 'note'
15
+
16
+ interface CalloutProps {
17
+ readonly type?: CalloutType
18
+ readonly title?: ReactNode
19
+ readonly children: ReactNode
20
+ readonly className?: string
21
+ }
22
+
23
+ const styles: Record<CalloutType, { ring: string; icon: string; iconPath: ReactNode }> = {
24
+ info: {
25
+ ring: 'border-info/30 bg-info/5 text-info',
26
+ icon: 'text-info',
27
+ iconPath: (
28
+ <>
29
+ <circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.5" />
30
+ <path d="M8 5.5 V8.5 M8 10.5 V11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
31
+ </>
32
+ ),
33
+ },
34
+ tip: {
35
+ ring: 'border-success/30 bg-success/5 text-success',
36
+ icon: 'text-success',
37
+ iconPath: (
38
+ <>
39
+ <path d="M5 8.5 L7 10.5 L11 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
40
+ <circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.5" />
41
+ </>
42
+ ),
43
+ },
44
+ warning: {
45
+ ring: 'border-warning/30 bg-warning/5 text-warning',
46
+ icon: 'text-warning',
47
+ iconPath: (
48
+ <>
49
+ <path d="M8 2 L14 13 L2 13 Z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
50
+ <path d="M8 6.5 V9 M8 10.5 V11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
51
+ </>
52
+ ),
53
+ },
54
+ danger: {
55
+ ring: 'border-destructive/40 bg-destructive/5 text-destructive',
56
+ icon: 'text-destructive',
57
+ iconPath: (
58
+ <>
59
+ <circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.5" />
60
+ <path d="M5.5 5.5 L10.5 10.5 M10.5 5.5 L5.5 10.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
61
+ </>
62
+ ),
63
+ },
64
+ note: {
65
+ ring: 'border-border bg-card/40 text-muted-foreground',
66
+ icon: 'text-muted-foreground',
67
+ iconPath: (
68
+ <>
69
+ <rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke="currentColor" strokeWidth="1.5" />
70
+ <path d="M5 6 H11 M5 8.5 H11 M5 11 H8.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
71
+ </>
72
+ ),
73
+ },
74
+ }
75
+
76
+ export const Callout = ({
77
+ type = 'info', title, children, className,
78
+ }: CalloutProps): ReactNode => {
79
+ const s = styles[type]
80
+ return (
81
+ <div className={cn('not-prose my-5 rounded-lg border px-4 py-3 flex gap-3', s.ring, className)}>
82
+ <svg
83
+ width="16" height="16" viewBox="0 0 16 16" fill="none"
84
+ className={cn('shrink-0 mt-0.5', s.icon)}
85
+ aria-hidden="true"
86
+ >
87
+ {s.iconPath}
88
+ </svg>
89
+ <div className="min-w-0 flex-1">
90
+ {title ? (
91
+ <div className="font-semibold text-foreground mb-1 leading-tight">{title}</div>
92
+ ) : null}
93
+ <div className="text-sm text-foreground/90 leading-relaxed">{children}</div>
94
+ </div>
95
+ </div>
96
+ )
97
+ }
@@ -0,0 +1,68 @@
1
+ // shadcn Card — surface container with optional header / content / footer.
2
+ // Compound component: <Card><CardHeader><CardTitle/></CardHeader>...</Card>.
3
+
4
+ import type { HTMLAttributes, ReactNode } from 'react'
5
+ import { cn } from '../cn'
6
+
7
+ export const Card = ({ className, children, ...props }: HTMLAttributes<HTMLDivElement>): ReactNode => (
8
+ <div
9
+ data-slot="card"
10
+ className={cn(
11
+ 'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
12
+ className,
13
+ )}
14
+ {...props}
15
+ >
16
+ {children}
17
+ </div>
18
+ )
19
+
20
+ export const CardHeader = ({ className, children, ...props }: HTMLAttributes<HTMLDivElement>): ReactNode => (
21
+ <div
22
+ data-slot="card-header"
23
+ className={cn('grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6', className)}
24
+ {...props}
25
+ >
26
+ {children}
27
+ </div>
28
+ )
29
+
30
+ export const CardTitle = ({ className, children, ...props }: HTMLAttributes<HTMLHeadingElement>): ReactNode => (
31
+ <h3
32
+ data-slot="card-title"
33
+ className={cn('leading-none font-semibold tracking-tight', className)}
34
+ {...props}
35
+ >
36
+ {children}
37
+ </h3>
38
+ )
39
+
40
+ export const CardDescription = ({ className, children, ...props }: HTMLAttributes<HTMLParagraphElement>): ReactNode => (
41
+ <p
42
+ data-slot="card-description"
43
+ className={cn('text-muted-foreground text-sm', className)}
44
+ {...props}
45
+ >
46
+ {children}
47
+ </p>
48
+ )
49
+
50
+ export const CardContent = ({ className, children, ...props }: HTMLAttributes<HTMLDivElement>): ReactNode => (
51
+ <div
52
+ data-slot="card-content"
53
+ className={cn('px-6', className)}
54
+ {...props}
55
+ >
56
+ {children}
57
+ </div>
58
+ )
59
+
60
+ export const CardFooter = ({ className, children, ...props }: HTMLAttributes<HTMLDivElement>): ReactNode => (
61
+ <div
62
+ data-slot="card-footer"
63
+ className={cn('flex items-center px-6', className)}
64
+ {...props}
65
+ >
66
+ {children}
67
+ </div>
68
+ )