@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
@@ -0,0 +1,41 @@
1
+ // FeatureGrid composition — auto-fitting grid of feature cards.
2
+ // Tile shape inherits from the shadcn Card primitive.
3
+
4
+ import type { ReactNode } from 'react'
5
+ import { Card, CardHeader, CardTitle, CardDescription } from '../primitives/card'
6
+ import { cn } from '../cn'
7
+
8
+ interface Feature {
9
+ readonly title: string
10
+ readonly body: string
11
+ readonly icon?: ReactNode
12
+ }
13
+
14
+ interface FeatureGridProps {
15
+ readonly sectionTitle?: string
16
+ readonly features: ReadonlyArray<Feature>
17
+ readonly className?: string
18
+ }
19
+
20
+ export const FeatureGrid = ({
21
+ sectionTitle, features, className,
22
+ }: FeatureGridProps): ReactNode => (
23
+ <section className={cn('py-16 max-w-6xl mx-auto px-6', className)}>
24
+ {sectionTitle ? (
25
+ <h2 className="text-2xl md:text-3xl font-semibold text-center mb-12 text-foreground">
26
+ {sectionTitle}
27
+ </h2>
28
+ ) : null}
29
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
30
+ {features.map((f) => (
31
+ <Card key={f.title} className="hover:border-primary/40 transition-colors">
32
+ <CardHeader>
33
+ {f.icon ? <div className="text-primary mb-2">{f.icon}</div> : null}
34
+ <CardTitle className="text-base">{f.title}</CardTitle>
35
+ <CardDescription>{f.body}</CardDescription>
36
+ </CardHeader>
37
+ </Card>
38
+ ))}
39
+ </div>
40
+ </section>
41
+ )
@@ -0,0 +1,55 @@
1
+ // HeroSection composition — opinionated marketing hero with eyebrow
2
+ // pill, gradient title, sub copy, and dual CTAs.
3
+
4
+ import type { ReactNode } from 'react'
5
+ import { Button } from '../primitives/button'
6
+ import { Badge } from '../primitives/badge'
7
+ import { cn } from '../cn'
8
+
9
+ interface HeroCta {
10
+ readonly label: string
11
+ readonly href: string
12
+ }
13
+
14
+ interface HeroSectionProps {
15
+ readonly eyebrow?: string
16
+ readonly title: ReactNode
17
+ readonly subtitle?: ReactNode
18
+ readonly primaryCta?: HeroCta
19
+ readonly secondaryCta?: HeroCta
20
+ readonly footnote?: ReactNode
21
+ readonly className?: string
22
+ }
23
+
24
+ export const HeroSection = ({
25
+ eyebrow, title, subtitle, primaryCta, secondaryCta, footnote, className,
26
+ }: HeroSectionProps): ReactNode => (
27
+ <section className={cn('py-20 text-center max-w-4xl mx-auto px-6', className)}>
28
+ {eyebrow ? (
29
+ <Badge variant="outline" className="mb-6 border-primary/30 text-primary">{eyebrow}</Badge>
30
+ ) : null}
31
+ <h1 className="text-4xl md:text-6xl font-bold tracking-tight leading-tight mb-4 text-foreground">
32
+ {title}
33
+ </h1>
34
+ {subtitle ? (
35
+ <p className="text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto mb-8">
36
+ {subtitle}
37
+ </p>
38
+ ) : null}
39
+ <div className="flex gap-3 justify-center flex-wrap mb-4">
40
+ {primaryCta ? (
41
+ <a href={primaryCta.href}>
42
+ <Button size="lg">{primaryCta.label}</Button>
43
+ </a>
44
+ ) : null}
45
+ {secondaryCta ? (
46
+ <a href={secondaryCta.href}>
47
+ <Button size="lg" variant="outline">{secondaryCta.label}</Button>
48
+ </a>
49
+ ) : null}
50
+ </div>
51
+ {footnote ? (
52
+ <p className="text-sm text-muted-foreground">{footnote}</p>
53
+ ) : null}
54
+ </section>
55
+ )
@@ -0,0 +1,85 @@
1
+ // LandingCta — big final "start shipping" section with a deploy-style
2
+ // animated gradient backdrop. Use once near the footer.
3
+
4
+ import type { ComponentType, ReactNode } from 'react'
5
+ import { cn } from '../cn'
6
+ import type { ShellLinkProps } from './docShell'
7
+
8
+ interface LandingCtaProps {
9
+ readonly title: ReactNode
10
+ readonly subtitle?: ReactNode
11
+ readonly primaryCta: { readonly label: string; readonly href: string }
12
+ readonly secondaryCta?: { readonly label: string; readonly href: string }
13
+ readonly footnote?: ReactNode
14
+ readonly className?: string
15
+ readonly LinkComponent?: ComponentType<ShellLinkProps>
16
+ }
17
+
18
+ // A schemed href (`https://…`) points off-origin → open in a new tab with a safe
19
+ // rel; relative/hash hrefs stay same-tab.
20
+ const isSchemed = (href: string): boolean => /^[a-z][a-z0-9+.-]*:\/\//i.test(href)
21
+
22
+ const PlainLink = ({ to, className, children }: ShellLinkProps): ReactNode =>
23
+ isSchemed(to)
24
+ ? <a href={to} className={className} target="_blank" rel="noopener noreferrer">{children}</a>
25
+ : <a href={to} className={className}>{children}</a>
26
+
27
+
28
+ export const LandingCta = ({
29
+ title, subtitle, primaryCta, secondaryCta, footnote,
30
+ className, LinkComponent = PlainLink,
31
+ }: LandingCtaProps): ReactNode => {
32
+ const Link = LinkComponent
33
+ return (
34
+ <section
35
+ className={cn(
36
+ 'relative overflow-hidden rounded-2xl border border-border',
37
+ 'bg-gradient-to-br from-card/60 via-card/30 to-card/60 backdrop-blur-sm',
38
+ // Slow-panning gradient layer behind the content
39
+ 'before:absolute before:inset-0 before:bg-[radial-gradient(circle_at_20%_30%,oklch(0.55_0.25_290_/_0.25),transparent_60%),radial-gradient(circle_at_80%_70%,oklch(0.55_0.2_220_/_0.2),transparent_60%)]',
40
+ // Hairline highlight along top
41
+ 'after:absolute after:inset-x-12 after:top-0 after:h-px after:bg-gradient-to-r after:from-transparent after:via-primary/60 after:to-transparent',
42
+ 'p-12 md:p-16 lg:p-20 text-center',
43
+ className,
44
+ )}
45
+ >
46
+ <div className="relative max-w-2xl mx-auto">
47
+ <h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-[-0.02em] leading-[1.1] text-foreground">
48
+ {title}
49
+ </h2>
50
+ {subtitle ? (
51
+ <p className="mt-5 text-base md:text-lg text-muted-foreground leading-relaxed">
52
+ {subtitle}
53
+ </p>
54
+ ) : null}
55
+ <div className="mt-8 flex flex-wrap items-center justify-center gap-3">
56
+ <Link
57
+ to={primaryCta.href}
58
+ prefetch
59
+ className={cn(
60
+ 'group inline-flex items-center gap-2 px-6 py-3 rounded-md',
61
+ 'bg-foreground text-background font-medium text-sm',
62
+ 'hover:opacity-90 active:scale-[0.98] transition-all',
63
+ 'shadow-[0_0_0_1px_oklch(1_0_0_/_0.08),0_8px_32px_-8px_oklch(0.55_0.25_290_/_0.45)]',
64
+ )}
65
+ >
66
+ {primaryCta.label}
67
+ <span aria-hidden="true" className="transition-transform group-hover:translate-x-0.5">→</span>
68
+ </Link>
69
+ {secondaryCta ? (
70
+ <Link
71
+ to={secondaryCta.href}
72
+ prefetch
73
+ className="inline-flex items-center gap-2 px-6 py-3 rounded-md border border-border bg-card/40 backdrop-blur-sm text-foreground font-medium text-sm hover:bg-card/70 hover:border-input transition-colors"
74
+ >
75
+ {secondaryCta.label}
76
+ </Link>
77
+ ) : null}
78
+ </div>
79
+ {footnote ? (
80
+ <div className="mt-6 text-xs text-muted-foreground">{footnote}</div>
81
+ ) : null}
82
+ </div>
83
+ </section>
84
+ )
85
+ }
@@ -0,0 +1,174 @@
1
+ // LandingHero — cinematic above-the-fold for dev-tooling landings.
2
+ //
3
+ // Layout (desktop):
4
+ // ┌───────────────────────────────────────────────────────────────┐
5
+ // │ • eyebrow │
6
+ // │ ▌ │
7
+ // │ Big gradient heading (2 lines) ┌────────────────────┐│
8
+ // │ │ CodeWindow (right) ││
9
+ // │ subtitle, body-size │ filename.tsx ││
10
+ // │ │ …syntax… ││
11
+ // │ [Primary CTA] [Secondary CTA] └────────────────────┘│
12
+ // │ │
13
+ // │ footnote │
14
+ // └───────────────────────────────────────────────────────────────┘
15
+ //
16
+ // On mobile the code window moves below the text. The component
17
+ // itself does not render the background — wrap it with MeshBackdrop +
18
+ // GridOverlay in the parent so multiple sections can share the same
19
+ // ambient layers.
20
+
21
+ import type { ComponentType, ReactNode } from 'react'
22
+ import { cn } from '../cn'
23
+ import type { ShellLinkProps } from './docShell'
24
+
25
+ interface CtaSpec {
26
+ readonly label: string
27
+ readonly href: string
28
+ }
29
+
30
+ interface LandingHeroProps {
31
+ readonly eyebrow?: ReactNode
32
+ /** Two-line heading: the second line gets the gradient + animation. */
33
+ readonly title: ReactNode
34
+ /** Optional second line — when set, the layout renders title on
35
+ * line 1 and this on line 2 with the gradient/animation. */
36
+ readonly titleAccent?: ReactNode
37
+ readonly subtitle?: ReactNode
38
+ readonly primaryCta?: CtaSpec
39
+ readonly secondaryCta?: CtaSpec
40
+ readonly footnote?: ReactNode
41
+ /** Code window / illustration slot — sits on the right at md+, below
42
+ * the CTAs on mobile. */
43
+ readonly aside?: ReactNode
44
+ /** Anchor link for "scroll down" hint. */
45
+ readonly scrollAnchor?: string
46
+ /** Visible caption on the scroll-down hint. Default `'Scroll'`. */
47
+ readonly scrollLabel?: ReactNode
48
+ /** `aria-label` for the scroll-down hint. Default `'Scroll down'`. */
49
+ readonly scrollAriaLabel?: string
50
+ readonly className?: string
51
+ readonly LinkComponent?: ComponentType<ShellLinkProps>
52
+ }
53
+
54
+ // A schemed href (`https://…`, `mailto:` …) points off-origin, so open it in a
55
+ // new tab with a safe rel; relative/hash hrefs stay same-tab.
56
+ const isSchemed = (href: string): boolean => /^[a-z][a-z0-9+.-]*:\/\//i.test(href)
57
+
58
+ const PlainLink = ({ to, className, children }: ShellLinkProps): ReactNode =>
59
+ isSchemed(to)
60
+ ? <a href={to} className={className} target="_blank" rel="noopener noreferrer">{children}</a>
61
+ : <a href={to} className={className}>{children}</a>
62
+
63
+
64
+ export const LandingHero = ({
65
+ eyebrow, title, titleAccent, subtitle, primaryCta, secondaryCta,
66
+ footnote, aside, scrollAnchor, scrollLabel = 'Scroll',
67
+ scrollAriaLabel = 'Scroll down', className,
68
+ LinkComponent = PlainLink,
69
+ }: LandingHeroProps): ReactNode => {
70
+ const Link = LinkComponent
71
+ return (
72
+ <section
73
+ className={cn(
74
+ 'relative isolate overflow-hidden',
75
+ // Extra bottom padding leaves room for the scroll hint to sit BELOW the
76
+ // content (it's anchored to the section, not the content column).
77
+ 'pt-24 md:pt-32 pb-24 md:pb-32',
78
+ className,
79
+ )}
80
+ >
81
+ <div className="relative mx-auto max-w-7xl px-6">
82
+ <div className="grid grid-cols-1 lg:grid-cols-[1.1fr_1fr] gap-10 lg:gap-16 items-center">
83
+ {/* ---- Left column: copy + CTAs ---- */}
84
+ <div className="motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-4 motion-safe:duration-700">
85
+ {eyebrow ? (
86
+ <div className="inline-flex items-center gap-2 text-xs uppercase tracking-[0.12em] text-primary font-semibold mb-5">
87
+ <span className="inline-block w-1.5 h-1.5 rounded-full bg-primary motion-safe:animate-pulse" />
88
+ {eyebrow}
89
+ </div>
90
+ ) : null}
91
+
92
+ <h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold tracking-[-0.02em] leading-[1.05] text-foreground">
93
+ {title}
94
+ {titleAccent ? (
95
+ <>
96
+ <br />
97
+ <span className="text-gradient motion-safe:animate-gradient-pan">
98
+ {titleAccent}
99
+ </span>
100
+ </>
101
+ ) : null}
102
+ </h1>
103
+
104
+ {subtitle ? (
105
+ <p className="mt-6 text-base sm:text-lg text-muted-foreground leading-relaxed max-w-xl">
106
+ {subtitle}
107
+ </p>
108
+ ) : null}
109
+
110
+ {(primaryCta || secondaryCta) ? (
111
+ <div className="mt-8 flex flex-wrap items-center gap-3">
112
+ {primaryCta ? (
113
+ <Link
114
+ to={primaryCta.href}
115
+ prefetch
116
+ className={cn(
117
+ 'group relative inline-flex items-center gap-2 px-5 py-2.5 rounded-md',
118
+ 'bg-foreground text-background font-medium text-sm',
119
+ 'hover:opacity-90 active:scale-[0.98] transition-all',
120
+ 'shadow-[0_0_0_1px_oklch(1_0_0_/_0.08),0_8px_24px_-8px_oklch(0_0_0_/_0.5)]',
121
+ )}
122
+ >
123
+ {primaryCta.label}
124
+ <span aria-hidden="true" className="transition-transform group-hover:translate-x-0.5">→</span>
125
+ </Link>
126
+ ) : null}
127
+ {secondaryCta ? (
128
+ <Link
129
+ to={secondaryCta.href}
130
+ prefetch
131
+ className={cn(
132
+ 'inline-flex items-center gap-2 px-5 py-2.5 rounded-md',
133
+ 'border border-border bg-card/40 backdrop-blur-sm',
134
+ 'text-foreground font-medium text-sm',
135
+ 'hover:bg-card/70 hover:border-input transition-colors',
136
+ )}
137
+ >
138
+ {secondaryCta.label}
139
+ </Link>
140
+ ) : null}
141
+ </div>
142
+ ) : null}
143
+
144
+ {footnote ? (
145
+ <div className="mt-8 text-sm text-muted-foreground">{footnote}</div>
146
+ ) : null}
147
+ </div>
148
+
149
+ {/* ---- Right column: code/illustration ---- */}
150
+ {aside ? (
151
+ <div className="lg:pl-4 motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-6 motion-safe:duration-700 motion-safe:delay-150">
152
+ {aside}
153
+ </div>
154
+ ) : null}
155
+ </div>
156
+ </div>
157
+
158
+ {/* Scroll hint — anchored to the SECTION (not the content column), so it
159
+ sits at the bottom of the full-height hero, at the fold. */}
160
+ {scrollAnchor ? (
161
+ <a
162
+ href={scrollAnchor}
163
+ className="hidden md:flex absolute left-1/2 -translate-x-1/2 bottom-8 text-xs text-muted-foreground items-center gap-1.5 hover:text-foreground transition-colors"
164
+ aria-label={scrollAriaLabel}
165
+ >
166
+ <span>{scrollLabel}</span>
167
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true" className="motion-safe:animate-bounce">
168
+ <path d="M7 2 V12 M3 8 L7 12 L11 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
169
+ </svg>
170
+ </a>
171
+ ) : null}
172
+ </section>
173
+ )
174
+ }
@@ -0,0 +1,99 @@
1
+ // LandingStats — strip of 3-4 big numbers under a "by the numbers"
2
+ // section. Each value uses AnimatedNumber so the digits roll up when
3
+ // the section scrolls into view.
4
+ //
5
+ // Layout per cell (top → bottom):
6
+ // 1. Eyebrow row: icon chip + small uppercase label aligned together
7
+ // so the icon reads AS a label, not as decoration. Chip is sized
8
+ // to match cap-height of the label (gives the icon real weight
9
+ // without competing with the number).
10
+ // 2. Big number with tabular figures, generous breathing room.
11
+ // Prefix + unit are visually subordinate via size, not opacity
12
+ // collapse (kept above the 4.5:1 contrast floor).
13
+ // 3. Sub-caption — one line of detail, muted but readable.
14
+ //
15
+ // The previous version had a 16px svg next to a 60px number, a
16
+ // freestanding hairline "accent rail" that aligned to nothing, and a
17
+ // glass background with a decorative hover-gradient hairline. All
18
+ // three were AI-grammar tells; this revision removes them. Hierarchy
19
+ // is carried by the size step between the number and the eyebrow,
20
+ // plus the kit's normal hover state on the cell.
21
+
22
+ import type { ReactNode } from 'react'
23
+ import { cn } from '../cn'
24
+ import { AnimatedNumber } from '../primitives/animatedNumber'
25
+
26
+ export interface StatSpec {
27
+ /** Numeric value — drives the count-up animation. */
28
+ readonly value: number
29
+ /** Optional unit suffix shown next to the value (e.g. "ms", "%"). */
30
+ readonly unit?: string
31
+ /** Static prefix (e.g. "<", "~"). Useful for things like "<5ms". */
32
+ readonly prefix?: string
33
+ /** Formatter — passed through to AnimatedNumber. */
34
+ readonly format?: 'integer' | 'percent' | ((n: number) => string)
35
+ /** Caption below the number (becomes the eyebrow label at the top
36
+ * of the cell). */
37
+ readonly label: ReactNode
38
+ /** Small sub-caption below the number. */
39
+ readonly sub?: ReactNode
40
+ /** Optional icon — pass any kit icon. Sized to the eyebrow's
41
+ * cap-height so it reads as part of the label, not as decoration. */
42
+ readonly icon?: ReactNode
43
+ }
44
+
45
+ interface LandingStatsProps {
46
+ readonly stats: ReadonlyArray<StatSpec>
47
+ readonly className?: string
48
+ }
49
+
50
+ export const LandingStats = ({
51
+ stats, className,
52
+ }: LandingStatsProps): ReactNode => (
53
+ <div
54
+ className={cn(
55
+ // grid-cols-N gives a clean wall of cells with hairline separators
56
+ // (the gap-px + bg-border below the grid layer). The outer
57
+ // rounded-2xl frame ties them into one composed strip.
58
+ 'grid grid-cols-2 lg:grid-cols-4 gap-px overflow-hidden rounded-2xl border border-border bg-border',
59
+ className,
60
+ )}
61
+ >
62
+ {stats.map((s, i) => (
63
+ <div
64
+ key={i}
65
+ className="relative bg-card p-7 lg:p-9 transition-colors duration-300 hover:bg-accent/40"
66
+ >
67
+ {/* Eyebrow row — icon + label travel together. Icon is sized
68
+ * to the label's cap-height so it reads as a sibling, not a
69
+ * floating chip. */}
70
+ <div className="flex items-center gap-2 mb-6 text-xs uppercase tracking-[0.14em] text-muted-foreground font-medium">
71
+ {s.icon ? (
72
+ <span className="text-foreground/80 [&_svg]:w-3.5 [&_svg]:h-3.5">
73
+ {s.icon}
74
+ </span>
75
+ ) : null}
76
+ <span>{s.label}</span>
77
+ </div>
78
+
79
+ {/* Big number — tabular figures, leading-none for tight
80
+ * vertical rhythm against the eyebrow above and the sub
81
+ * below. Prefix/unit are subordinate via SIZE (not via
82
+ * pushing opacity below readable contrast). */}
83
+ <div className="text-5xl lg:text-6xl font-semibold tracking-[-0.035em] text-foreground inline-flex items-baseline gap-1 font-tabular leading-none">
84
+ {s.prefix ? (
85
+ <span className="text-3xl lg:text-4xl text-muted-foreground font-medium">{s.prefix}</span>
86
+ ) : null}
87
+ <AnimatedNumber value={s.value} {...(s.format ? { format: s.format } : {})} />
88
+ {s.unit ? (
89
+ <span className="text-2xl lg:text-3xl text-muted-foreground font-medium ml-0.5">{s.unit}</span>
90
+ ) : null}
91
+ </div>
92
+
93
+ {s.sub ? (
94
+ <div className="mt-4 text-sm text-muted-foreground leading-relaxed">{s.sub}</div>
95
+ ) : null}
96
+ </div>
97
+ ))}
98
+ </div>
99
+ )
@@ -0,0 +1,139 @@
1
+ // LoginCard composition — pairs email + password inputs with an
2
+ // optional OAuth row. The card is stylistically opinionated (max
3
+ // width, centred, raised) so apps don't reinvent the same shell.
4
+ //
5
+ // Submit handling stays with the consumer (form action / onSubmit).
6
+ // The component just renders the surface.
7
+
8
+ import type { FormHTMLAttributes, ReactNode } from 'react'
9
+ import { Button } from '../primitives/button'
10
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from '../primitives/card'
11
+ import { Input } from '../primitives/input'
12
+ import { Label } from '../primitives/label'
13
+ import { cn } from '../cn'
14
+
15
+ interface OAuthProvider {
16
+ readonly id: string
17
+ readonly label: string
18
+ }
19
+
20
+ /** User-facing strings — pass catalog values to localize. Defaults are
21
+ * English so the card works unconfigured. */
22
+ export interface LoginCardLabels {
23
+ /** Email field label. Default `'Email'`. */
24
+ readonly email?: string
25
+ /** Email input placeholder. Default `'you@company.com'`. */
26
+ readonly emailPlaceholder?: string
27
+ /** Password field label. Default `'Password'`. */
28
+ readonly password?: string
29
+ /** "Forgot password?" link text (shown when `forgotHref` is set).
30
+ * Default `'Forgot?'`. */
31
+ readonly forgot?: string
32
+ /** OAuth divider caption. Default `'or continue with'`. */
33
+ readonly oauthDivider?: string
34
+ /** Sign-up footer prompt (shown when `signUpHref` is set).
35
+ * Default `'New here?'`. */
36
+ readonly signUpPrompt?: string
37
+ /** Sign-up footer link text. Default `'Create an account'`. */
38
+ readonly signUp?: string
39
+ }
40
+
41
+ const DEFAULT_LOGIN_LABELS: Required<LoginCardLabels> = {
42
+ email: 'Email',
43
+ emailPlaceholder: 'you@company.com',
44
+ password: 'Password',
45
+ forgot: 'Forgot?',
46
+ oauthDivider: 'or continue with',
47
+ signUpPrompt: 'New here?',
48
+ signUp: 'Create an account',
49
+ }
50
+
51
+ interface LoginCardProps extends Omit<FormHTMLAttributes<HTMLFormElement>, 'children'> {
52
+ readonly title?: string
53
+ readonly description?: string
54
+ readonly eyebrow?: string
55
+ readonly submitLabel?: string
56
+ readonly oauthProviders?: ReadonlyArray<OAuthProvider>
57
+ readonly onOAuth?: (providerId: string) => void
58
+ readonly signUpHref?: string
59
+ readonly forgotHref?: string
60
+ readonly className?: string
61
+ /** Override the card's own chrome strings. Defaults are English. */
62
+ readonly labels?: LoginCardLabels
63
+ }
64
+
65
+ export const LoginCard = ({
66
+ title = 'Welcome back',
67
+ description = 'Sign in to your account to continue.',
68
+ eyebrow,
69
+ submitLabel = 'Sign in',
70
+ oauthProviders,
71
+ onOAuth,
72
+ signUpHref,
73
+ forgotHref,
74
+ className,
75
+ labels,
76
+ ...formProps
77
+ }: LoginCardProps): ReactNode => {
78
+ const t = { ...DEFAULT_LOGIN_LABELS, ...labels }
79
+ return (
80
+ <div className={cn('w-full max-w-md mx-auto', className)}>
81
+ <Card>
82
+ <CardHeader>
83
+ {eyebrow ? (
84
+ <div className="text-xs uppercase tracking-wider text-muted-foreground font-medium">
85
+ {eyebrow}
86
+ </div>
87
+ ) : null}
88
+ <CardTitle className="text-2xl">{title}</CardTitle>
89
+ <CardDescription>{description}</CardDescription>
90
+ </CardHeader>
91
+ <CardContent>
92
+ <form className="space-y-4" {...formProps}>
93
+ <div className="space-y-1.5">
94
+ <Label htmlFor="email">{t.email}</Label>
95
+ <Input id="email" name="email" type="email" autoComplete="email" placeholder={t.emailPlaceholder} />
96
+ </div>
97
+ <div className="space-y-1.5">
98
+ <div className="flex items-center justify-between">
99
+ <Label htmlFor="password">{t.password}</Label>
100
+ {forgotHref ? (
101
+ <a href={forgotHref} className="text-xs text-muted-foreground hover:text-foreground">{t.forgot}</a>
102
+ ) : null}
103
+ </div>
104
+ <Input id="password" name="password" type="password" autoComplete="current-password" placeholder="••••••••" />
105
+ </div>
106
+ <Button type="submit" className="w-full">{submitLabel}</Button>
107
+ </form>
108
+
109
+ {oauthProviders && oauthProviders.length > 0 ? (
110
+ <>
111
+ <div className="my-5 flex items-center gap-3 text-xs text-muted-foreground">
112
+ <div className="h-px flex-1 bg-border" />
113
+ <span>{t.oauthDivider}</span>
114
+ <div className="h-px flex-1 bg-border" />
115
+ </div>
116
+ <div className="grid gap-2" style={{ gridTemplateColumns: `repeat(${oauthProviders.length}, minmax(0, 1fr))` }}>
117
+ {oauthProviders.map((p) => (
118
+ <Button
119
+ key={p.id}
120
+ type="button"
121
+ variant="outline"
122
+ onClick={() => onOAuth?.(p.id)}
123
+ >
124
+ {p.label}
125
+ </Button>
126
+ ))}
127
+ </div>
128
+ </>
129
+ ) : null}
130
+ </CardContent>
131
+ {signUpHref ? (
132
+ <CardFooter className="justify-center text-xs text-muted-foreground">
133
+ {t.signUpPrompt} <a href={signUpHref} className="ml-1 text-foreground hover:underline">{t.signUp}</a>
134
+ </CardFooter>
135
+ ) : null}
136
+ </Card>
137
+ </div>
138
+ )
139
+ }
@@ -0,0 +1,58 @@
1
+ // Shared page header chrome for every Voltro-Cloud-style app:
2
+ // landing, docs, dashboard, marketplace. The single source of truth
3
+ // for the sticky top bar that wraps brand + navigation across all
4
+ // four web apps — keeps height, glass treatment, max-width, and
5
+ // padding identical so they read as one product.
6
+ //
7
+ // Treatment:
8
+ // • sticky top-0, z-40 (above the docs sidebar's top-16 sticky)
9
+ // • Fixed `h-16` (64px). NOT scroll-shrinking — DocsLayout's
10
+ // sidebar uses `sticky top-16` and a height-changing header
11
+ // would leave a gap or overlap on scroll. The visual
12
+ // "compactness on scroll" effect is delivered by the glass
13
+ // fade-in instead.
14
+ // • Glass + border fade in once `scrollY > 24`. At rest the
15
+ // header is fully transparent → the hero / page background
16
+ // bleeds through.
17
+ //
18
+ // The children prop is the row contents (brand on the left, nav /
19
+ // actions on the right). Consumers compose freely; the kit just
20
+ // owns the wrapper.
21
+
22
+ import { useEffect, useState, type ReactNode } from 'react'
23
+ import { cn } from '../cn'
24
+
25
+ interface PageHeaderProps {
26
+ readonly children: ReactNode
27
+ readonly className?: string
28
+ }
29
+
30
+ export const PageHeader = ({ children, className }: PageHeaderProps): ReactNode => {
31
+ const [scrolled, setScrolled] = useState(false)
32
+ useEffect(() => {
33
+ // 24px threshold — smaller flickers on subpixel scroll;
34
+ // bigger leaves a moment where content touches the bare header.
35
+ const onScroll = (): void => setScrolled(window.scrollY > 24)
36
+ onScroll()
37
+ window.addEventListener('scroll', onScroll, { passive: true })
38
+ return () => window.removeEventListener('scroll', onScroll)
39
+ }, [])
40
+
41
+ return (
42
+ <header className={cn('sticky top-0 z-40', className)}>
43
+ {/* Glass + hairline border, fade-in on scroll. Separate <div>
44
+ * so opacity transitions don't affect the foreground row. */}
45
+ <div
46
+ aria-hidden="true"
47
+ className={cn(
48
+ 'absolute inset-0 -z-10 transition-opacity duration-300 ease-out',
49
+ 'bg-background/80 backdrop-blur-lg border-b border-border',
50
+ scrolled ? 'opacity-100' : 'opacity-0',
51
+ )}
52
+ />
53
+ <div className="mx-auto max-w-7xl px-6 h-16 flex items-center justify-between gap-4">
54
+ {children}
55
+ </div>
56
+ </header>
57
+ )
58
+ }