@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
package/src/widgets.tsx
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// shadcn-styled widgets that plug into the @voltro/ui widget seam. Plain,
|
|
2
|
+
// accessible HTML with Tailwind utility classes against the design tokens in
|
|
3
|
+
// `./tokens.css`. Drop them in app-wide:
|
|
4
|
+
//
|
|
5
|
+
// import { shadcnWidgets } from '@voltro/ui-shadcn'
|
|
6
|
+
// import '@voltro/ui-shadcn/tokens.css'
|
|
7
|
+
// <WidgetRegistryProvider widgets={shadcnWidgets}>…</WidgetRegistryProvider>
|
|
8
|
+
//
|
|
9
|
+
// They implement the SAME `Widget` contract as @voltro/ui's plain defaults —
|
|
10
|
+
// the seam falls back to the plain default for any kind not overridden here.
|
|
11
|
+
|
|
12
|
+
import { useId, type ReactNode } from 'react'
|
|
13
|
+
import type { Widget, WidgetProps, WidgetRegistry } from '@voltro/ui'
|
|
14
|
+
|
|
15
|
+
const cx = {
|
|
16
|
+
field: 'voltro-field flex flex-col gap-1',
|
|
17
|
+
label: 'text-sm font-medium text-foreground',
|
|
18
|
+
control:
|
|
19
|
+
'block w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm ' +
|
|
20
|
+
'focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
|
21
|
+
checkbox: 'h-4 w-4 rounded border-input text-primary focus:ring-2 focus:ring-ring',
|
|
22
|
+
error: 'text-sm text-destructive',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const asString = (v: unknown): string => (v === undefined || v === null ? '' : String(v))
|
|
26
|
+
const describedBy = (id: string, error: string | undefined): string | undefined =>
|
|
27
|
+
error !== undefined ? `${id}-error` : undefined
|
|
28
|
+
|
|
29
|
+
const Shell = (props: {
|
|
30
|
+
readonly id: string
|
|
31
|
+
readonly label: string
|
|
32
|
+
readonly required: boolean
|
|
33
|
+
readonly error?: string | undefined
|
|
34
|
+
readonly children: ReactNode
|
|
35
|
+
}): ReactNode => (
|
|
36
|
+
<div className={cx.field}>
|
|
37
|
+
<label className={cx.label} htmlFor={props.id}>
|
|
38
|
+
{props.label}
|
|
39
|
+
{props.required ? ' *' : ''}
|
|
40
|
+
</label>
|
|
41
|
+
{props.children}
|
|
42
|
+
{props.error !== undefined ? (
|
|
43
|
+
<p id={`${props.id}-error`} className={cx.error} role="alert">
|
|
44
|
+
{props.error}
|
|
45
|
+
</p>
|
|
46
|
+
) : null}
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const inputWidget = (type: string, toValue: (raw: string) => unknown): Widget => {
|
|
51
|
+
const Component: Widget = (props) => {
|
|
52
|
+
const id = useId()
|
|
53
|
+
return (
|
|
54
|
+
<Shell id={id} label={props.label} required={props.required} error={props.error}>
|
|
55
|
+
<input
|
|
56
|
+
id={id}
|
|
57
|
+
type={type}
|
|
58
|
+
name={props.name}
|
|
59
|
+
className={cx.control}
|
|
60
|
+
value={asString(props.value)}
|
|
61
|
+
onChange={(e) => props.onChange(toValue(e.target.value))}
|
|
62
|
+
disabled={props.disabled}
|
|
63
|
+
required={props.required}
|
|
64
|
+
aria-invalid={props.error !== undefined}
|
|
65
|
+
aria-describedby={describedBy(id, props.error)}
|
|
66
|
+
/>
|
|
67
|
+
</Shell>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
return Component
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const TextareaWidget: Widget = (props) => {
|
|
74
|
+
const id = useId()
|
|
75
|
+
return (
|
|
76
|
+
<Shell id={id} label={props.label} required={props.required} error={props.error}>
|
|
77
|
+
<textarea
|
|
78
|
+
id={id}
|
|
79
|
+
name={props.name}
|
|
80
|
+
className={cx.control}
|
|
81
|
+
value={asString(props.value)}
|
|
82
|
+
onChange={(e) => props.onChange(e.target.value)}
|
|
83
|
+
disabled={props.disabled}
|
|
84
|
+
required={props.required}
|
|
85
|
+
aria-invalid={props.error !== undefined}
|
|
86
|
+
aria-describedby={describedBy(id, props.error)}
|
|
87
|
+
/>
|
|
88
|
+
</Shell>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const CheckboxWidget: Widget = (props) => {
|
|
93
|
+
const id = useId()
|
|
94
|
+
return (
|
|
95
|
+
<div className="voltro-field flex items-center gap-2">
|
|
96
|
+
<input
|
|
97
|
+
id={id}
|
|
98
|
+
type="checkbox"
|
|
99
|
+
name={props.name}
|
|
100
|
+
className={cx.checkbox}
|
|
101
|
+
checked={props.value === true}
|
|
102
|
+
onChange={(e) => props.onChange(e.target.checked)}
|
|
103
|
+
disabled={props.disabled}
|
|
104
|
+
aria-invalid={props.error !== undefined}
|
|
105
|
+
aria-describedby={describedBy(id, props.error)}
|
|
106
|
+
/>
|
|
107
|
+
<label className={cx.label} htmlFor={id}>
|
|
108
|
+
{props.label}
|
|
109
|
+
{props.required ? ' *' : ''}
|
|
110
|
+
</label>
|
|
111
|
+
{props.error !== undefined ? (
|
|
112
|
+
<p id={`${id}-error`} className={cx.error} role="alert">
|
|
113
|
+
{props.error}
|
|
114
|
+
</p>
|
|
115
|
+
) : null}
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const SwitchWidget: Widget = (props) => {
|
|
121
|
+
const id = useId()
|
|
122
|
+
const on = props.value === true
|
|
123
|
+
return (
|
|
124
|
+
<div className="voltro-field flex items-center gap-2">
|
|
125
|
+
<button
|
|
126
|
+
id={id}
|
|
127
|
+
type="button"
|
|
128
|
+
role="switch"
|
|
129
|
+
aria-checked={on}
|
|
130
|
+
onClick={() => props.onChange(!on)}
|
|
131
|
+
disabled={props.disabled}
|
|
132
|
+
aria-invalid={props.error !== undefined}
|
|
133
|
+
aria-describedby={describedBy(id, props.error)}
|
|
134
|
+
className={
|
|
135
|
+
'relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-input ' +
|
|
136
|
+
'transition-colors focus:outline-none focus:ring-2 focus:ring-ring ' +
|
|
137
|
+
'disabled:cursor-not-allowed disabled:opacity-50 ' +
|
|
138
|
+
(on ? 'bg-primary' : 'bg-input')
|
|
139
|
+
}
|
|
140
|
+
>
|
|
141
|
+
<span
|
|
142
|
+
aria-hidden="true"
|
|
143
|
+
className={
|
|
144
|
+
'block h-4 w-4 rounded-full bg-background shadow-sm transition-transform ' +
|
|
145
|
+
(on ? 'translate-x-4' : 'translate-x-0.5')
|
|
146
|
+
}
|
|
147
|
+
/>
|
|
148
|
+
</button>
|
|
149
|
+
<label className={cx.label} htmlFor={id}>
|
|
150
|
+
{props.label}
|
|
151
|
+
{props.required ? ' *' : ''}
|
|
152
|
+
</label>
|
|
153
|
+
{props.error !== undefined ? (
|
|
154
|
+
<p id={`${id}-error`} className={cx.error} role="alert">
|
|
155
|
+
{props.error}
|
|
156
|
+
</p>
|
|
157
|
+
) : null}
|
|
158
|
+
</div>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const SelectWidget: Widget = (props) => {
|
|
163
|
+
const id = useId()
|
|
164
|
+
return (
|
|
165
|
+
<Shell id={id} label={props.label} required={props.required} error={props.error}>
|
|
166
|
+
<select
|
|
167
|
+
id={id}
|
|
168
|
+
name={props.name}
|
|
169
|
+
className={cx.control}
|
|
170
|
+
value={asString(props.value)}
|
|
171
|
+
onChange={(e) => props.onChange(e.target.value)}
|
|
172
|
+
disabled={props.disabled}
|
|
173
|
+
required={props.required}
|
|
174
|
+
aria-invalid={props.error !== undefined}
|
|
175
|
+
aria-describedby={describedBy(id, props.error)}
|
|
176
|
+
>
|
|
177
|
+
<option value="">—</option>
|
|
178
|
+
{(props.options ?? []).map((o) => (
|
|
179
|
+
<option key={o.value} value={o.value}>
|
|
180
|
+
{o.label}
|
|
181
|
+
</option>
|
|
182
|
+
))}
|
|
183
|
+
</select>
|
|
184
|
+
</Shell>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const RadioWidget: Widget = (props) => {
|
|
189
|
+
const id = useId()
|
|
190
|
+
return (
|
|
191
|
+
<fieldset className={cx.field} aria-invalid={props.error !== undefined}>
|
|
192
|
+
<legend className={cx.label}>
|
|
193
|
+
{props.label}
|
|
194
|
+
{props.required ? ' *' : ''}
|
|
195
|
+
</legend>
|
|
196
|
+
<div className="flex flex-col gap-1">
|
|
197
|
+
{(props.options ?? []).map((o) => {
|
|
198
|
+
const oid = `${id}-${o.value}`
|
|
199
|
+
return (
|
|
200
|
+
<span key={o.value} className="flex items-center gap-2">
|
|
201
|
+
<input
|
|
202
|
+
id={oid}
|
|
203
|
+
type="radio"
|
|
204
|
+
name={props.name}
|
|
205
|
+
value={o.value}
|
|
206
|
+
className={cx.checkbox}
|
|
207
|
+
checked={asString(props.value) === o.value}
|
|
208
|
+
onChange={() => props.onChange(o.value)}
|
|
209
|
+
disabled={props.disabled}
|
|
210
|
+
/>
|
|
211
|
+
<label className="text-sm text-foreground" htmlFor={oid}>
|
|
212
|
+
{o.label}
|
|
213
|
+
</label>
|
|
214
|
+
</span>
|
|
215
|
+
)
|
|
216
|
+
})}
|
|
217
|
+
</div>
|
|
218
|
+
{props.error !== undefined ? (
|
|
219
|
+
<p id={`${id}-error`} className={cx.error} role="alert">
|
|
220
|
+
{props.error}
|
|
221
|
+
</p>
|
|
222
|
+
) : null}
|
|
223
|
+
</fieldset>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const MultiSelectWidget: Widget = (props) => {
|
|
228
|
+
const id = useId()
|
|
229
|
+
const selected = Array.isArray(props.value) ? props.value.map(String) : []
|
|
230
|
+
return (
|
|
231
|
+
<Shell id={id} label={props.label} required={props.required} error={props.error}>
|
|
232
|
+
<select
|
|
233
|
+
id={id}
|
|
234
|
+
name={props.name}
|
|
235
|
+
multiple
|
|
236
|
+
className={cx.control + ' min-h-24'}
|
|
237
|
+
value={selected}
|
|
238
|
+
onChange={(e) => props.onChange(Array.from(e.target.selectedOptions, (o) => o.value))}
|
|
239
|
+
disabled={props.disabled}
|
|
240
|
+
aria-invalid={props.error !== undefined}
|
|
241
|
+
aria-describedby={describedBy(id, props.error)}
|
|
242
|
+
>
|
|
243
|
+
{(props.options ?? []).map((o) => (
|
|
244
|
+
<option key={o.value} value={o.value}>
|
|
245
|
+
{o.label}
|
|
246
|
+
</option>
|
|
247
|
+
))}
|
|
248
|
+
</select>
|
|
249
|
+
</Shell>
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const FileWidget: Widget = (props) => {
|
|
254
|
+
const id = useId()
|
|
255
|
+
return (
|
|
256
|
+
<Shell id={id} label={props.label} required={props.required} error={props.error}>
|
|
257
|
+
<input
|
|
258
|
+
id={id}
|
|
259
|
+
type="file"
|
|
260
|
+
name={props.name}
|
|
261
|
+
className={
|
|
262
|
+
cx.control +
|
|
263
|
+
' file:mr-3 file:rounded file:border-0 file:bg-primary file:px-2 file:py-1 ' +
|
|
264
|
+
'file:text-sm file:text-primary-foreground'
|
|
265
|
+
}
|
|
266
|
+
onChange={(e) => props.onChange(e.target.value)}
|
|
267
|
+
disabled={props.disabled}
|
|
268
|
+
required={props.required}
|
|
269
|
+
aria-invalid={props.error !== undefined}
|
|
270
|
+
aria-describedby={describedBy(id, props.error)}
|
|
271
|
+
/>
|
|
272
|
+
</Shell>
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Tailwind-styled widgets keyed by widget kind. Pass to
|
|
277
|
+
* `<WidgetRegistryProvider widgets={shadcnWidgets}>`. Any kind not listed
|
|
278
|
+
* here falls back to @voltro/ui's plain accessible default — today that is
|
|
279
|
+
* only `hidden` (invisible anyway) and `custom` (the render-prop signal).
|
|
280
|
+
* Value contracts mirror the plain defaults exactly: `daterange` and
|
|
281
|
+
* `async-select` are styled text inputs until the framework grows richer
|
|
282
|
+
* bindings for those kinds (same ceiling as @voltro/ui's defaults). */
|
|
283
|
+
export const shadcnWidgets: WidgetRegistry = {
|
|
284
|
+
text: inputWidget('text', (s) => s),
|
|
285
|
+
number: inputWidget('number', (s) => (s === '' ? undefined : Number(s))),
|
|
286
|
+
date: inputWidget('date', (s) => s),
|
|
287
|
+
datetime: inputWidget('datetime-local', (s) => s),
|
|
288
|
+
daterange: inputWidget('text', (s) => s),
|
|
289
|
+
'async-select': inputWidget('text', (s) => s),
|
|
290
|
+
textarea: TextareaWidget,
|
|
291
|
+
checkbox: CheckboxWidget,
|
|
292
|
+
switch: SwitchWidget,
|
|
293
|
+
select: SelectWidget,
|
|
294
|
+
radio: RadioWidget,
|
|
295
|
+
'multi-select': MultiSelectWidget,
|
|
296
|
+
file: FileWidget,
|
|
297
|
+
}
|