@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,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
+ }