@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.
- package/CHANGELOG.md +52 -0
- package/LICENSE +57 -0
- package/README.md +26 -0
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +3016 -0
- package/dist/brand.d.ts +73 -0
- package/dist/brand.js +177 -0
- package/dist/cn.d.ts +11 -0
- package/dist/cn.js +6 -0
- package/dist/index.d.ts +1183 -0
- package/dist/index.js +2636 -0
- package/dist/tokens.css +532 -0
- package/package.json +64 -0
- package/src/brand/README.md +60 -0
- package/src/brand/assets/voltro-favicon.svg +30 -0
- package/src/brand/assets/voltro-icon-dark.svg +26 -0
- package/src/brand/assets/voltro-icon.svg +14 -0
- package/src/brand/assets/voltro-mark-mono.svg +5 -0
- package/src/brand/assets/voltro-mark.svg +14 -0
- package/src/brand/voltroLogo.tsx +244 -0
- package/src/cn.ts +10 -0
- package/src/compositions/appShell.tsx +72 -0
- package/src/compositions/codeCompare.tsx +88 -0
- package/src/compositions/docShell.tsx +112 -0
- package/src/compositions/docsLayout.tsx +577 -0
- package/src/compositions/featureBento.tsx +103 -0
- package/src/compositions/featureGrid.tsx +41 -0
- package/src/compositions/heroSection.tsx +55 -0
- package/src/compositions/landingCta.tsx +85 -0
- package/src/compositions/landingHero.tsx +174 -0
- package/src/compositions/landingStats.tsx +99 -0
- package/src/compositions/loginCard.tsx +139 -0
- package/src/compositions/pageHeader.tsx +58 -0
- package/src/compositions/profileMenu.tsx +316 -0
- package/src/compositions/siteFooter.tsx +250 -0
- package/src/compositions/themeToggle.tsx +82 -0
- package/src/cookies.ts +109 -0
- package/src/index.ts +160 -0
- package/src/primitives/animatedNumber.tsx +73 -0
- package/src/primitives/avatar.tsx +39 -0
- package/src/primitives/badge.tsx +39 -0
- package/src/primitives/button.tsx +53 -0
- package/src/primitives/callout.tsx +97 -0
- package/src/primitives/card.tsx +68 -0
- package/src/primitives/checkbox.tsx +55 -0
- package/src/primitives/codeBlock.tsx +134 -0
- package/src/primitives/codeWindow.tsx +84 -0
- package/src/primitives/dialog.tsx +43 -0
- package/src/primitives/docCard.tsx +109 -0
- package/src/primitives/docIcons.tsx +268 -0
- package/src/primitives/dropdownMenu.tsx +162 -0
- package/src/primitives/gridOverlay.tsx +51 -0
- package/src/primitives/highlightedCode.tsx +112 -0
- package/src/primitives/input.tsx +25 -0
- package/src/primitives/label.tsx +19 -0
- package/src/primitives/localeSwitcher.tsx +90 -0
- package/src/primitives/meshBackdrop.tsx +62 -0
- package/src/primitives/scrollReveal.tsx +70 -0
- package/src/primitives/searchModal.tsx +304 -0
- package/src/primitives/select.tsx +24 -0
- package/src/primitives/separator.tsx +24 -0
- package/src/primitives/skeleton.tsx +12 -0
- package/src/primitives/sparkles.tsx +105 -0
- package/src/primitives/steps.tsx +55 -0
- package/src/primitives/tabs.tsx +102 -0
- package/src/primitives/textarea.tsx +24 -0
- package/src/primitives/toast.tsx +44 -0
- package/src/primitives/tocScrollSpy.tsx +110 -0
- package/src/primitives/toggle.tsx +50 -0
- package/src/primitives/toggleGroup.tsx +62 -0
- package/src/tokens.css +532 -0
- package/src/widgets.tsx +297 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" fill="none" role="img" aria-label="Voltro">
|
|
2
|
+
<!-- Dark variant: glowing violet bolt on a near-black violet-tinted squircle.
|
|
3
|
+
Matches the dark product aesthetic; great for dark-mode favicons / OG. -->
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="voltroIconDarkBg" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
|
|
6
|
+
<stop stop-color="#1B1726"/>
|
|
7
|
+
<stop offset="1" stop-color="#120F1A"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
<radialGradient id="voltroIconDarkGlow" cx="0.5" cy="0.46" r="0.5">
|
|
10
|
+
<stop stop-color="#7C3AED" stop-opacity="0.55"/>
|
|
11
|
+
<stop offset="1" stop-color="#7C3AED" stop-opacity="0"/>
|
|
12
|
+
</radialGradient>
|
|
13
|
+
<linearGradient id="voltroIconDarkFront" x1="13" y1="5" x2="33" y2="43" gradientUnits="userSpaceOnUse">
|
|
14
|
+
<stop stop-color="#C4A6FF"/>
|
|
15
|
+
<stop offset="1" stop-color="#9D5BFF"/>
|
|
16
|
+
</linearGradient>
|
|
17
|
+
<linearGradient id="voltroIconDarkBack" x1="22" y1="5" x2="35" y2="43" gradientUnits="userSpaceOnUse">
|
|
18
|
+
<stop stop-color="#A874FF"/>
|
|
19
|
+
<stop offset="1" stop-color="#7C3AED"/>
|
|
20
|
+
</linearGradient>
|
|
21
|
+
</defs>
|
|
22
|
+
<rect width="64" height="64" rx="14.5" fill="url(#voltroIconDarkBg)"/>
|
|
23
|
+
<ellipse cx="32" cy="30" rx="26" ry="26" fill="url(#voltroIconDarkGlow)"/>
|
|
24
|
+
<path transform="translate(8 8)" d="M28 5 13 27 22 26 20 43Z" fill="url(#voltroIconDarkFront)"/>
|
|
25
|
+
<path transform="translate(8 8)" d="M28 5 20 43 35 21 26 22Z" fill="url(#voltroIconDarkBack)"/>
|
|
26
|
+
</svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" fill="none" role="img" aria-label="Voltro">
|
|
2
|
+
<!-- Primary app / advertising icon: two-tone bolt (white + pale violet) on
|
|
3
|
+
the brand violet squircle. Keeps the folded-light look — not a flat
|
|
4
|
+
glyph — while staying legible. Self-contained bg → works anywhere. -->
|
|
5
|
+
<defs>
|
|
6
|
+
<linearGradient id="voltroIconBg" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
|
|
7
|
+
<stop stop-color="#8B5CFF"/>
|
|
8
|
+
<stop offset="1" stop-color="#6D28D9"/>
|
|
9
|
+
</linearGradient>
|
|
10
|
+
</defs>
|
|
11
|
+
<rect width="64" height="64" rx="14.5" fill="url(#voltroIconBg)"/>
|
|
12
|
+
<path transform="translate(8 8)" d="M28 5 13 27 22 26 20 43Z" fill="#FFFFFF"/>
|
|
13
|
+
<path transform="translate(8 8)" d="M28 5 20 43 35 21 26 22Z" fill="#D7C4FF"/>
|
|
14
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor" role="img" aria-label="Voltro">
|
|
2
|
+
<!-- Single-colour mark. Inherits `color` via currentColor — drop it on any
|
|
3
|
+
surface and it adapts to light/dark. Override with fill="#7C3AED" etc. -->
|
|
4
|
+
<path d="M28 5 13 27 22 26 20 43 35 21 26 22Z"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none" role="img" aria-label="Voltro">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="voltroMarkFront" x1="13" y1="5" x2="33" y2="43" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop stop-color="#A874FF"/>
|
|
5
|
+
<stop offset="1" stop-color="#7C3AED"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="voltroMarkBack" x1="22" y1="5" x2="35" y2="43" gradientUnits="userSpaceOnUse">
|
|
8
|
+
<stop stop-color="#6D28D9"/>
|
|
9
|
+
<stop offset="1" stop-color="#4C1D95"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
</defs>
|
|
12
|
+
<path d="M28 5 13 27 22 26 20 43Z" fill="url(#voltroMarkFront)"/>
|
|
13
|
+
<path d="M28 5 20 43 35 21 26 22Z" fill="url(#voltroMarkBack)"/>
|
|
14
|
+
</svg>
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// Voltro brand mark — the company + framework logo.
|
|
2
|
+
//
|
|
3
|
+
// The mark is a faceted lightning bolt ("volt") that also reads as the
|
|
4
|
+
// "V" of Voltro: two folded facets catch light off a central crease, so
|
|
5
|
+
// it has depth at billboard size yet stays legible as a 16px favicon.
|
|
6
|
+
// On-brand violet (oklch(0.55 0.25 290) ≈ #7C3AED — the same hue the
|
|
7
|
+
// text-gradient + mesh backdrop use).
|
|
8
|
+
//
|
|
9
|
+
// ▲ VoltroMark — the bolt alone. variant: gradient | mono | white.
|
|
10
|
+
// ◳ VoltroIcon — app/ad icon: bolt on a rounded "squircle".
|
|
11
|
+
// ▦ VoltroWordmark — mark + "Voltro" set in the current font.
|
|
12
|
+
//
|
|
13
|
+
// Every app consumes ONE source: `import { VoltroMark } from
|
|
14
|
+
// '@voltro/ui-shadcn'`. `mono` paints in `currentColor`, so it inherits
|
|
15
|
+
// the surrounding text colour and is automatically correct in light AND
|
|
16
|
+
// dark mode. The gradient + icon variants carry their own colour and
|
|
17
|
+
// work on any background. For static assets (favicon, OG image, app-store
|
|
18
|
+
// icon) use the sibling `assets/*.svg` files.
|
|
19
|
+
|
|
20
|
+
import type { CSSProperties, ReactNode, SVGProps } from 'react'
|
|
21
|
+
import { useId } from 'react'
|
|
22
|
+
import { cn } from '../cn'
|
|
23
|
+
|
|
24
|
+
// The bolt geometry, authored once in a 48×48 box and reused everywhere.
|
|
25
|
+
// Sharp joins read as "electric"; the facets share the crease T→C.
|
|
26
|
+
const BOLT_FULL = 'M28 5 13 27 22 26 20 43 35 21 26 22Z' // outer silhouette (one fill)
|
|
27
|
+
const BOLT_FRONT = 'M28 5 13 27 22 26 20 43Z' // left / lit facet
|
|
28
|
+
const BOLT_BACK = 'M28 5 20 43 35 21 26 22Z' // right / shaded facet
|
|
29
|
+
|
|
30
|
+
/** Brand violet — the canonical primary. Exposed for ad-hoc usage. */
|
|
31
|
+
export const VOLTRO_VIOLET = '#7C3AED'
|
|
32
|
+
|
|
33
|
+
export type VoltroMarkVariant = 'gradient' | 'mono' | 'white'
|
|
34
|
+
|
|
35
|
+
// The component owns the structural attrs (viewBox / fill / xmlns / children) and
|
|
36
|
+
// the a11y trio (role / aria-label / aria-hidden, driven by `title`), so they are
|
|
37
|
+
// excluded from the pass-through: leaving them in would let the last-spread `rest`
|
|
38
|
+
// clobber the component's own literals with `T | undefined`, which trips a strict
|
|
39
|
+
// consumer's `exactOptionalPropertyTypes` (the mark typechecks in its own project but
|
|
40
|
+
// broke when a downstream app compiled this source under that flag).
|
|
41
|
+
//
|
|
42
|
+
// `ref` is excluded too, and for a different reason: this is a plain function
|
|
43
|
+
// component (not `forwardRef`), so it never wires a ref anywhere — the prop was
|
|
44
|
+
// never honoured. Keeping it in the pass-through also spreads `SVGProps['ref']`
|
|
45
|
+
// onto the `<svg>`; when a downstream app compiles THIS source, that `ref` type
|
|
46
|
+
// resolves against the framework's `@types/react` copy while the intrinsic `<svg>`
|
|
47
|
+
// resolves against the app's own copy. Same version, but two structurally-distinct
|
|
48
|
+
// `Ref`/`VoidOrUndefinedOnly` types → TS reports them as "unrelated" and the spread
|
|
49
|
+
// fails. Dropping `ref` from the surface removes the dead prop and the clash at once.
|
|
50
|
+
export interface VoltroMarkProps
|
|
51
|
+
extends Omit<
|
|
52
|
+
SVGProps<SVGSVGElement>,
|
|
53
|
+
'width' | 'height' | 'viewBox' | 'fill' | 'xmlns' | 'children' | 'ref' | 'role' | 'aria-label' | 'aria-hidden'
|
|
54
|
+
> {
|
|
55
|
+
/** Pixel size (square). Default 32. Pass a string like '1em' to track font-size. */
|
|
56
|
+
readonly size?: number | string
|
|
57
|
+
/** `gradient` (default, two-facet violet) · `mono` (currentColor) · `white`. */
|
|
58
|
+
readonly variant?: VoltroMarkVariant
|
|
59
|
+
/** Accessible label. Omit + set `aria-hidden` when paired with a text label. */
|
|
60
|
+
readonly title?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The bolt mark on a transparent background. */
|
|
64
|
+
export const VoltroMark = ({
|
|
65
|
+
size = 32,
|
|
66
|
+
variant = 'gradient',
|
|
67
|
+
title,
|
|
68
|
+
className,
|
|
69
|
+
...rest
|
|
70
|
+
}: VoltroMarkProps): ReactNode => {
|
|
71
|
+
const uid = useId()
|
|
72
|
+
const front = `vmF-${uid}`
|
|
73
|
+
const back = `vmB-${uid}`
|
|
74
|
+
const labelled = title !== undefined
|
|
75
|
+
// One non-union type: explicit `role={... : undefined}` props violate
|
|
76
|
+
// exactOptionalPropertyTypes, and a conditional-spread union chokes older
|
|
77
|
+
// checkers in cross-repo consumers.
|
|
78
|
+
const a11y: Pick<SVGProps<SVGSVGElement>, 'role' | 'aria-label' | 'aria-hidden'> = labelled
|
|
79
|
+
? { role: 'img', 'aria-label': title }
|
|
80
|
+
: { 'aria-hidden': true }
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<svg
|
|
84
|
+
width={size}
|
|
85
|
+
height={size}
|
|
86
|
+
viewBox="0 0 48 48"
|
|
87
|
+
fill="none"
|
|
88
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
89
|
+
{...a11y}
|
|
90
|
+
className={cn('inline-block shrink-0', className)}
|
|
91
|
+
{...rest}
|
|
92
|
+
>
|
|
93
|
+
{variant === 'gradient' ? (
|
|
94
|
+
<>
|
|
95
|
+
<defs>
|
|
96
|
+
<linearGradient id={front} x1="13" y1="5" x2="33" y2="43" gradientUnits="userSpaceOnUse">
|
|
97
|
+
<stop stopColor="#A874FF" />
|
|
98
|
+
<stop offset="1" stopColor="#7C3AED" />
|
|
99
|
+
</linearGradient>
|
|
100
|
+
<linearGradient id={back} x1="22" y1="5" x2="35" y2="43" gradientUnits="userSpaceOnUse">
|
|
101
|
+
<stop stopColor="#6D28D9" />
|
|
102
|
+
<stop offset="1" stopColor="#4C1D95" />
|
|
103
|
+
</linearGradient>
|
|
104
|
+
</defs>
|
|
105
|
+
<path d={BOLT_FRONT} fill={`url(#${front})`} />
|
|
106
|
+
<path d={BOLT_BACK} fill={`url(#${back})`} />
|
|
107
|
+
</>
|
|
108
|
+
) : (
|
|
109
|
+
<path d={BOLT_FULL} fill={variant === 'white' ? '#FFFFFF' : 'currentColor'} />
|
|
110
|
+
)}
|
|
111
|
+
</svg>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type VoltroIconTone = 'brand' | 'dark'
|
|
116
|
+
|
|
117
|
+
export interface VoltroIconProps {
|
|
118
|
+
/** Pixel size (square). Default 40. */
|
|
119
|
+
readonly size?: number
|
|
120
|
+
/** `brand` (violet squircle + white bolt — works on any surface) · `dark` (near-black squircle + glowing violet bolt). */
|
|
121
|
+
readonly tone?: VoltroIconTone
|
|
122
|
+
/** Corner radius as a fraction of size. Default 0.225 (iOS-style squircle look). */
|
|
123
|
+
readonly radius?: number
|
|
124
|
+
readonly title?: string
|
|
125
|
+
readonly className?: string
|
|
126
|
+
readonly style?: CSSProperties
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The app / advertising icon: the bolt centred on a rounded square with
|
|
131
|
+
* its own background, so it stands on a home screen, store listing, or
|
|
132
|
+
* ad placement regardless of the surface behind it.
|
|
133
|
+
*/
|
|
134
|
+
export const VoltroIcon = ({
|
|
135
|
+
size = 40,
|
|
136
|
+
tone = 'brand',
|
|
137
|
+
radius = 0.225,
|
|
138
|
+
title = 'Voltro',
|
|
139
|
+
className,
|
|
140
|
+
style,
|
|
141
|
+
}: VoltroIconProps): ReactNode => {
|
|
142
|
+
const uid = useId()
|
|
143
|
+
const bg = `viBg-${uid}`
|
|
144
|
+
const glow = `viGlow-${uid}`
|
|
145
|
+
const front = `viF-${uid}`
|
|
146
|
+
const back = `viB-${uid}`
|
|
147
|
+
const rx = (64 * radius).toFixed(2)
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<svg
|
|
151
|
+
width={size}
|
|
152
|
+
height={size}
|
|
153
|
+
viewBox="0 0 64 64"
|
|
154
|
+
fill="none"
|
|
155
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
156
|
+
role="img"
|
|
157
|
+
aria-label={title}
|
|
158
|
+
className={cn('inline-block shrink-0', className)}
|
|
159
|
+
style={style}
|
|
160
|
+
>
|
|
161
|
+
<defs>
|
|
162
|
+
<linearGradient id={bg} x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
|
|
163
|
+
{tone === 'brand' ? (
|
|
164
|
+
<>
|
|
165
|
+
<stop stopColor="#8B5CFF" />
|
|
166
|
+
<stop offset="1" stopColor="#6D28D9" />
|
|
167
|
+
</>
|
|
168
|
+
) : (
|
|
169
|
+
<>
|
|
170
|
+
<stop stopColor="#1B1726" />
|
|
171
|
+
<stop offset="1" stopColor="#120F1A" />
|
|
172
|
+
</>
|
|
173
|
+
)}
|
|
174
|
+
</linearGradient>
|
|
175
|
+
{tone === 'dark' ? (
|
|
176
|
+
<radialGradient id={glow} cx="0.5" cy="0.46" r="0.5">
|
|
177
|
+
<stop stopColor="#7C3AED" stopOpacity="0.55" />
|
|
178
|
+
<stop offset="1" stopColor="#7C3AED" stopOpacity="0" />
|
|
179
|
+
</radialGradient>
|
|
180
|
+
) : null}
|
|
181
|
+
<linearGradient id={front} x1="13" y1="5" x2="33" y2="43" gradientUnits="userSpaceOnUse">
|
|
182
|
+
<stop stopColor="#C4A6FF" />
|
|
183
|
+
<stop offset="1" stopColor="#9D5BFF" />
|
|
184
|
+
</linearGradient>
|
|
185
|
+
<linearGradient id={back} x1="22" y1="5" x2="35" y2="43" gradientUnits="userSpaceOnUse">
|
|
186
|
+
<stop stopColor="#A874FF" />
|
|
187
|
+
<stop offset="1" stopColor="#7C3AED" />
|
|
188
|
+
</linearGradient>
|
|
189
|
+
</defs>
|
|
190
|
+
|
|
191
|
+
<rect width="64" height="64" rx={rx} fill={`url(#${bg})`} />
|
|
192
|
+
{tone === 'brand' ? (
|
|
193
|
+
// Two-tone bolt — white front facet + pale-violet back facet keep the
|
|
194
|
+
// folded-light identity readable on the violet field (not a flat glyph).
|
|
195
|
+
<>
|
|
196
|
+
<path transform="translate(8 8)" d={BOLT_FRONT} fill="#FFFFFF" />
|
|
197
|
+
<path transform="translate(8 8)" d={BOLT_BACK} fill="#D7C4FF" />
|
|
198
|
+
</>
|
|
199
|
+
) : (
|
|
200
|
+
<>
|
|
201
|
+
<ellipse cx="32" cy="30" rx="26" ry="26" fill={`url(#${glow})`} />
|
|
202
|
+
<path transform="translate(8 8)" d={BOLT_FRONT} fill={`url(#${front})`} />
|
|
203
|
+
<path transform="translate(8 8)" d={BOLT_BACK} fill={`url(#${back})`} />
|
|
204
|
+
</>
|
|
205
|
+
)}
|
|
206
|
+
</svg>
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface VoltroWordmarkProps {
|
|
211
|
+
/** Mark height in px; the wordmark text scales with it. Default 28. */
|
|
212
|
+
readonly size?: number
|
|
213
|
+
readonly markVariant?: VoltroMarkVariant
|
|
214
|
+
/** Wordmark label. Default "Voltro". */
|
|
215
|
+
readonly label?: string
|
|
216
|
+
readonly className?: string
|
|
217
|
+
readonly style?: CSSProperties
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Mark + wordmark lock-up for headers / marketing. The text is real
|
|
222
|
+
* type set in the surrounding font and `currentColor`, so it inherits
|
|
223
|
+
* the app's typography and is correct in light + dark automatically.
|
|
224
|
+
*/
|
|
225
|
+
export const VoltroWordmark = ({
|
|
226
|
+
size = 28,
|
|
227
|
+
markVariant = 'gradient',
|
|
228
|
+
label = 'Voltro',
|
|
229
|
+
className,
|
|
230
|
+
style,
|
|
231
|
+
}: VoltroWordmarkProps): ReactNode => (
|
|
232
|
+
<span
|
|
233
|
+
className={cn('inline-flex items-center font-semibold tracking-tight leading-none', className)}
|
|
234
|
+
style={{ gap: size * 0.28, fontSize: size * 0.74, ...style }}
|
|
235
|
+
>
|
|
236
|
+
{/* Mark rendered larger than the cap height so the bolt reads as the
|
|
237
|
+
lead element, not a small adornment beside the wordmark. */}
|
|
238
|
+
<VoltroMark size={Math.round(size * 1.18)} variant={markVariant} aria-hidden />
|
|
239
|
+
<span>{label}</span>
|
|
240
|
+
</span>
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
/** Raw bolt path data, for apps that need to compose their own SVG. */
|
|
244
|
+
export const VOLTRO_BOLT_PATH = { full: BOLT_FULL, front: BOLT_FRONT, back: BOLT_BACK } as const
|
package/src/cn.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Class-name composition helper used by every shadcn primitive.
|
|
2
|
+
// clsx handles conditional / array / object forms; tailwind-merge
|
|
3
|
+
// dedupes conflicting Tailwind utilities (e.g. `px-2 px-4` → `px-4`).
|
|
4
|
+
// Exported as a tiny subpath so consumers can grab it without
|
|
5
|
+
// pulling the entire `@voltro/ui-shadcn` surface.
|
|
6
|
+
|
|
7
|
+
import { clsx, type ClassValue } from 'clsx'
|
|
8
|
+
import { twMerge } from 'tailwind-merge'
|
|
9
|
+
|
|
10
|
+
export const cn = (...inputs: ClassValue[]): string => twMerge(clsx(inputs))
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// AppShell composition — topbar + main scroll region. Used by the
|
|
2
|
+
// dashboard, portal, admin templates as the authenticated-app skeleton.
|
|
3
|
+
// The shell itself is dumb; auth gates / sidebar collapse / search /
|
|
4
|
+
// command palette layer on top.
|
|
5
|
+
//
|
|
6
|
+
// `LinkComponent` lets apps pass `Link` from `@voltro/web` so the
|
|
7
|
+
// top nav uses client-side routing + loader prefetch instead of full
|
|
8
|
+
// page reloads. Default is plain `<a>` so the kit stays
|
|
9
|
+
// router-agnostic.
|
|
10
|
+
//
|
|
11
|
+
// The top bar chrome (sticky, glass-on-scroll, h-16, max-w-7xl) is
|
|
12
|
+
// the shared <PageHeader> — same one consumed by DocsLayout and by
|
|
13
|
+
// landing / marketplace pages directly. That's the single visual
|
|
14
|
+
// source of truth across the four sibling apps; do not duplicate
|
|
15
|
+
// the header treatment here.
|
|
16
|
+
|
|
17
|
+
import type { ComponentType, ReactNode } from 'react'
|
|
18
|
+
import { cn } from '../cn'
|
|
19
|
+
import type { ShellLinkProps } from './docShell'
|
|
20
|
+
import { PageHeader } from './pageHeader'
|
|
21
|
+
|
|
22
|
+
interface NavLink {
|
|
23
|
+
readonly label: string
|
|
24
|
+
readonly href: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface AppShellProps {
|
|
28
|
+
readonly brand: ReactNode
|
|
29
|
+
readonly nav?: ReadonlyArray<NavLink>
|
|
30
|
+
readonly right?: ReactNode
|
|
31
|
+
readonly children: ReactNode
|
|
32
|
+
readonly className?: string
|
|
33
|
+
readonly LinkComponent?: ComponentType<ShellLinkProps>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const PlainLink = ({ to, className, children }: ShellLinkProps): ReactNode => (
|
|
37
|
+
<a href={to} className={className}>{children}</a>
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
export const AppShell = ({
|
|
41
|
+
brand, nav, right, children, className,
|
|
42
|
+
LinkComponent = PlainLink,
|
|
43
|
+
}: AppShellProps): ReactNode => {
|
|
44
|
+
const Link = LinkComponent
|
|
45
|
+
return (
|
|
46
|
+
<div className={cn('min-h-screen flex flex-col bg-background text-foreground', className)}>
|
|
47
|
+
<PageHeader>
|
|
48
|
+
<div className="font-semibold flex items-center gap-2">{brand}</div>
|
|
49
|
+
<div className="flex items-center gap-6">
|
|
50
|
+
{nav && nav.length > 0 ? (
|
|
51
|
+
<nav className="hidden md:flex gap-6 text-sm">
|
|
52
|
+
{nav.map((l) => (
|
|
53
|
+
<Link
|
|
54
|
+
key={l.href}
|
|
55
|
+
to={l.href}
|
|
56
|
+
prefetch
|
|
57
|
+
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
58
|
+
>
|
|
59
|
+
{l.label}
|
|
60
|
+
</Link>
|
|
61
|
+
))}
|
|
62
|
+
</nav>
|
|
63
|
+
) : null}
|
|
64
|
+
{right ? <div>{right}</div> : null}
|
|
65
|
+
</div>
|
|
66
|
+
</PageHeader>
|
|
67
|
+
<main className="flex-1">{children}</main>
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// CodeCompare — side-by-side "before / after" code panels. Common
|
|
2
|
+
// landing-page pattern for dev tooling: left = the messy world,
|
|
3
|
+
// right = your product's clean replacement. The dividing label sits
|
|
4
|
+
// in the centre on desktop, between the two panels on mobile.
|
|
5
|
+
|
|
6
|
+
import type { ReactNode } from 'react'
|
|
7
|
+
import { cn } from '../cn'
|
|
8
|
+
import { HighlightedCode, type ShikiLang } from '../primitives/highlightedCode'
|
|
9
|
+
|
|
10
|
+
interface CodeComparePanelProps {
|
|
11
|
+
readonly label: ReactNode
|
|
12
|
+
/** Pre-highlighted HTML (Shiki) — skips internal highlighting. */
|
|
13
|
+
readonly html?: string
|
|
14
|
+
/** Plain code text. When `lang` is also provided, the panel
|
|
15
|
+
* runs Shiki syntax highlighting automatically. */
|
|
16
|
+
readonly code?: string
|
|
17
|
+
/** Shiki language. Triggers syntax highlighting on `code`. */
|
|
18
|
+
readonly lang?: ShikiLang
|
|
19
|
+
/** Visual treatment — `'muted'` is dimmed (the "before"),
|
|
20
|
+
* `'highlight'` keeps the full styling + adds a primary border. */
|
|
21
|
+
readonly tone?: 'muted' | 'highlight'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CodeCompareProps {
|
|
25
|
+
readonly before: CodeComparePanelProps
|
|
26
|
+
readonly after: CodeComparePanelProps
|
|
27
|
+
readonly className?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const Panel = ({ label, html, code, lang, tone = 'muted' }: CodeComparePanelProps): ReactNode => (
|
|
31
|
+
<div
|
|
32
|
+
className={cn(
|
|
33
|
+
'relative rounded-xl border overflow-hidden bg-card/40 backdrop-blur-sm',
|
|
34
|
+
tone === 'highlight'
|
|
35
|
+
? 'border-primary/40 shadow-[0_0_0_1px_oklch(0.55_0.25_290_/_0.15),0_12px_40px_-12px_oklch(0.55_0.25_290_/_0.35)]'
|
|
36
|
+
: 'border-border',
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
<div className="flex items-center justify-between px-4 py-2 border-b border-border/60 bg-background/40">
|
|
40
|
+
<div className="text-xs font-mono text-muted-foreground">{label}</div>
|
|
41
|
+
<span
|
|
42
|
+
className={cn(
|
|
43
|
+
'text-[0.65rem] uppercase tracking-[0.08em] font-semibold',
|
|
44
|
+
tone === 'highlight' ? 'text-primary' : 'text-muted-foreground/70',
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
{tone === 'highlight' ? 'With Voltro' : 'Before'}
|
|
48
|
+
</span>
|
|
49
|
+
</div>
|
|
50
|
+
<div
|
|
51
|
+
className={cn(
|
|
52
|
+
'text-sm leading-relaxed [&_pre]:!m-0 [&_pre]:!bg-transparent [&_pre]:p-5 overflow-x-auto',
|
|
53
|
+
tone === 'muted' && 'opacity-60',
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
{html ? (
|
|
57
|
+
<div dangerouslySetInnerHTML={{ __html: html }} />
|
|
58
|
+
) : code && lang ? (
|
|
59
|
+
<HighlightedCode code={code} lang={lang} />
|
|
60
|
+
) : (
|
|
61
|
+
<pre className="p-5 font-mono"><code>{code}</code></pre>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
export const CodeCompare = ({
|
|
68
|
+
before, after, className,
|
|
69
|
+
}: CodeCompareProps): ReactNode => (
|
|
70
|
+
<div className={cn('relative grid grid-cols-1 lg:grid-cols-[1fr_auto_1fr] gap-4 lg:gap-6 items-stretch', className)}>
|
|
71
|
+
<Panel {...before} tone="muted" />
|
|
72
|
+
{/* Arrow / divider — sits between the two panels */}
|
|
73
|
+
<div
|
|
74
|
+
className="flex lg:flex-col items-center justify-center gap-2 text-muted-foreground"
|
|
75
|
+
aria-hidden="true"
|
|
76
|
+
>
|
|
77
|
+
<div className="hidden lg:block w-px h-12 bg-gradient-to-b from-transparent via-primary/60 to-transparent" />
|
|
78
|
+
<svg
|
|
79
|
+
width="20" height="20" viewBox="0 0 20 20" fill="none"
|
|
80
|
+
className="rotate-90 lg:rotate-0 text-primary"
|
|
81
|
+
>
|
|
82
|
+
<path d="M4 10 H16 M11 5 L16 10 L11 15" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
83
|
+
</svg>
|
|
84
|
+
<div className="hidden lg:block w-px h-12 bg-gradient-to-b from-primary/60 via-transparent to-transparent" />
|
|
85
|
+
</div>
|
|
86
|
+
<Panel {...after} tone="highlight" />
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// DocShell composition — sidebar + main grid for documentation.
|
|
2
|
+
// Sidebar nav is data-driven (group → entry tree); apps pass their
|
|
3
|
+
// own structure. Sticky sidebar, scrollable main, responsive collapse
|
|
4
|
+
// to single-column under 800px.
|
|
5
|
+
//
|
|
6
|
+
// `LinkComponent` lets the app inject a client-side router (e.g.
|
|
7
|
+
// `Link` from `@voltro/web`) so sidebar nav doesn't trigger full
|
|
8
|
+
// page reloads. Default is plain `<a>` so the kit stays
|
|
9
|
+
// router-agnostic and works in any host.
|
|
10
|
+
|
|
11
|
+
import type { ComponentType, ReactNode } from 'react'
|
|
12
|
+
import { cn } from '../cn'
|
|
13
|
+
|
|
14
|
+
export interface DocNavEntry {
|
|
15
|
+
readonly label: string
|
|
16
|
+
readonly href: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DocNavGroup {
|
|
20
|
+
readonly section: string
|
|
21
|
+
readonly entries: ReadonlyArray<DocNavEntry>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Minimal Link prop surface — matches @voltro/web's Link with `to`,
|
|
25
|
+
// and degrades cleanly to a plain <a href> when not provided.
|
|
26
|
+
export interface ShellLinkProps {
|
|
27
|
+
readonly to: string
|
|
28
|
+
readonly className?: string
|
|
29
|
+
readonly children: ReactNode
|
|
30
|
+
/** Pre-warm the destination on hover/focus. */
|
|
31
|
+
readonly prefetch?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface DocShellProps {
|
|
35
|
+
readonly brand: ReactNode
|
|
36
|
+
readonly nav: ReadonlyArray<DocNavGroup>
|
|
37
|
+
readonly children: ReactNode
|
|
38
|
+
readonly currentPath?: string
|
|
39
|
+
readonly className?: string
|
|
40
|
+
/** Search form GET action. When set, renders a search input that
|
|
41
|
+
* POSTs ?q=… to this URL. */
|
|
42
|
+
readonly searchAction?: string
|
|
43
|
+
/** Placeholder for the search input. Default `'Search docs…'`. */
|
|
44
|
+
readonly searchPlaceholder?: string
|
|
45
|
+
/** `aria-label` for the search input. Default `'Search docs'`. */
|
|
46
|
+
readonly searchLabel?: string
|
|
47
|
+
/** Optional client-router Link. Passing `Link` from `@voltro/web`
|
|
48
|
+
* upgrades every sidebar entry to client-side navigation with
|
|
49
|
+
* loader prefetch. */
|
|
50
|
+
readonly LinkComponent?: ComponentType<ShellLinkProps>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const PlainLink = ({ to, className, children }: ShellLinkProps): ReactNode => (
|
|
54
|
+
<a href={to} className={className}>{children}</a>
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
export const DocShell = ({
|
|
58
|
+
brand, nav, children, currentPath, className, searchAction,
|
|
59
|
+
searchPlaceholder = 'Search docs…', searchLabel = 'Search docs',
|
|
60
|
+
LinkComponent = PlainLink,
|
|
61
|
+
}: DocShellProps): ReactNode => {
|
|
62
|
+
const Link = LinkComponent
|
|
63
|
+
return (
|
|
64
|
+
<div className={cn('min-h-screen grid grid-cols-1 md:grid-cols-[260px_1fr] bg-background text-foreground', className)}>
|
|
65
|
+
<aside className="border-r bg-card/30 p-6 md:sticky md:top-0 md:h-screen md:overflow-y-auto">
|
|
66
|
+
<div className="mb-6">{brand}</div>
|
|
67
|
+
{searchAction ? (
|
|
68
|
+
<form method="get" action={searchAction} className="mb-6">
|
|
69
|
+
<input
|
|
70
|
+
type="search"
|
|
71
|
+
name="q"
|
|
72
|
+
placeholder={searchPlaceholder}
|
|
73
|
+
aria-label={searchLabel}
|
|
74
|
+
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-ring"
|
|
75
|
+
/>
|
|
76
|
+
</form>
|
|
77
|
+
) : null}
|
|
78
|
+
{nav.map((group) => (
|
|
79
|
+
<div key={group.section} className="mb-6">
|
|
80
|
+
<h4 className="text-xs uppercase tracking-wider text-muted-foreground font-medium mb-2">
|
|
81
|
+
{group.section}
|
|
82
|
+
</h4>
|
|
83
|
+
<ul className="space-y-0.5">
|
|
84
|
+
{group.entries.map((e) => {
|
|
85
|
+
const active = currentPath === e.href
|
|
86
|
+
return (
|
|
87
|
+
<li key={e.href}>
|
|
88
|
+
<Link
|
|
89
|
+
to={e.href}
|
|
90
|
+
prefetch
|
|
91
|
+
className={cn(
|
|
92
|
+
'block rounded-md px-2 py-1.5 text-sm transition-colors',
|
|
93
|
+
active
|
|
94
|
+
? 'bg-accent text-accent-foreground font-medium'
|
|
95
|
+
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground',
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
{e.label}
|
|
99
|
+
</Link>
|
|
100
|
+
</li>
|
|
101
|
+
)
|
|
102
|
+
})}
|
|
103
|
+
</ul>
|
|
104
|
+
</div>
|
|
105
|
+
))}
|
|
106
|
+
</aside>
|
|
107
|
+
<main className="p-8 md:p-12 lg:p-16 max-w-3xl text-foreground">
|
|
108
|
+
{children}
|
|
109
|
+
</main>
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|