@windforge/ui 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 (61) hide show
  1. package/dist/index.d.ts +1195 -0
  2. package/dist/index.js +3628 -0
  3. package/package.json +66 -0
  4. package/src/catalog.ts +654 -0
  5. package/src/components/accordion.tsx +91 -0
  6. package/src/components/alert.tsx +58 -0
  7. package/src/components/autocomplete.tsx +174 -0
  8. package/src/components/avatar.tsx +60 -0
  9. package/src/components/badge.tsx +37 -0
  10. package/src/components/breadcrumb.tsx +62 -0
  11. package/src/components/button-group.tsx +23 -0
  12. package/src/components/button.tsx +53 -0
  13. package/src/components/calendar.tsx +61 -0
  14. package/src/components/card.tsx +72 -0
  15. package/src/components/chart.tsx +130 -0
  16. package/src/components/checkbox.tsx +27 -0
  17. package/src/components/chip.tsx +75 -0
  18. package/src/components/code-block.tsx +126 -0
  19. package/src/components/command.tsx +139 -0
  20. package/src/components/data-table.tsx +194 -0
  21. package/src/components/date-picker.tsx +77 -0
  22. package/src/components/dialog.tsx +57 -0
  23. package/src/components/dropdown-menu.tsx +186 -0
  24. package/src/components/form-field.tsx +97 -0
  25. package/src/components/input.tsx +29 -0
  26. package/src/components/label.tsx +18 -0
  27. package/src/components/layout.tsx +179 -0
  28. package/src/components/link.tsx +37 -0
  29. package/src/components/modal.tsx +67 -0
  30. package/src/components/multi-select.tsx +175 -0
  31. package/src/components/pagination.tsx +72 -0
  32. package/src/components/popover.tsx +25 -0
  33. package/src/components/progress.tsx +31 -0
  34. package/src/components/radio-group.tsx +34 -0
  35. package/src/components/select.tsx +134 -0
  36. package/src/components/separator.tsx +21 -0
  37. package/src/components/sheet.tsx +80 -0
  38. package/src/components/skeleton.tsx +11 -0
  39. package/src/components/slider.tsx +28 -0
  40. package/src/components/stepper.tsx +69 -0
  41. package/src/components/switch.tsx +33 -0
  42. package/src/components/table.tsx +121 -0
  43. package/src/components/tabs.tsx +90 -0
  44. package/src/components/text.tsx +109 -0
  45. package/src/components/textarea.tsx +27 -0
  46. package/src/components/toast.tsx +107 -0
  47. package/src/components/toggle-button.tsx +103 -0
  48. package/src/components/tooltip.tsx +26 -0
  49. package/src/icons/forge-icon.tsx +55 -0
  50. package/src/icons/icon-set.ts +60 -0
  51. package/src/icons/svg-icon.tsx +43 -0
  52. package/src/index.ts +80 -0
  53. package/src/layouts/app-bar.tsx +95 -0
  54. package/src/layouts/app-shell.tsx +80 -0
  55. package/src/layouts/side-nav.tsx +196 -0
  56. package/src/layouts/theme-provider.tsx +128 -0
  57. package/src/lib/recipes.ts +50 -0
  58. package/src/lib/types.ts +3 -0
  59. package/src/lib/use-media-query.ts +18 -0
  60. package/src/lib/utils.ts +10 -0
  61. package/tailwind-preset.cjs +77 -0
@@ -0,0 +1,109 @@
1
+ import * as React from 'react'
2
+ import { Slot } from '@radix-ui/react-slot'
3
+ import { cva, type VariantProps } from 'class-variance-authority'
4
+ import { cn } from '../lib/utils'
5
+ import type { NoStyle } from '../lib/types'
6
+
7
+ /**
8
+ * Text — the typography primitive (our `Typography`). Size, weight, tone and
9
+ * alignment are a closed token vocabulary; nothing renders off-scale. Renders a
10
+ * `<p>` by default; use `asChild` for the right semantic element:
11
+ *
12
+ * <Text size="3xl" weight="bold" asChild><h1>Title</h1></Text>
13
+ * <Text tone="muted">Body copy</Text>
14
+ * <Text span weight="medium">inline label</Text> // inline <span>, not a block <p>
15
+ * <Text variant="inline-code">defaultOpen</Text> // inline <code> within prose
16
+ */
17
+ const text = cva('', {
18
+ variants: {
19
+ size: {
20
+ sm: 'text-sm', base: 'text-base', lg: 'text-lg', xl: 'text-xl',
21
+ '2xl': 'text-2xl', '3xl': 'text-3xl', '4xl': 'text-4xl', '5xl': 'text-5xl', '6xl': 'text-6xl',
22
+ },
23
+ weight: {
24
+ light: 'font-light', regular: 'font-normal', medium: 'font-medium', semibold: 'font-semibold', bold: 'font-bold',
25
+ },
26
+ tone: {
27
+ default: 'text-primary',
28
+ muted: 'text-secondary',
29
+ subtle: 'text-tertiary',
30
+ disabled: 'text-disabled',
31
+ inverse: 'text-inverse',
32
+ brand: 'text-brand',
33
+ link: 'text-link',
34
+ // A brand→secondary gradient clipped to the glyphs — for hero/display type.
35
+ gradient: 'bg-gradient-to-r from-brand to-brand-secondary bg-clip-text text-transparent',
36
+ },
37
+ align: { left: 'text-left', center: 'text-center', right: 'text-right' },
38
+ truncate: { true: 'truncate', false: '' },
39
+ mono: { true: 'font-mono', false: '' },
40
+ // `variant="inline-code"` renders a <code> element styled as inline code:
41
+ // monospace, a subtle fill, em-sized so it scales with the surrounding text.
42
+ // Listed last so its text-size wins; no horizontal padding so it hugs the text.
43
+ variant: {
44
+ 'inline-code': 'rounded border border-subtle bg-surface-inset py-0.5 font-mono text-[0.875em] text-primary',
45
+ },
46
+ },
47
+ defaultVariants: { size: 'base', tone: 'default' },
48
+ })
49
+
50
+ export interface TextProps extends NoStyle<React.HTMLAttributes<HTMLElement>>, VariantProps<typeof text> {
51
+ asChild?: boolean
52
+ /** Render inline as a `<span>` instead of a block `<p>` — for labels, chips and text inside flex rows. */
53
+ span?: boolean
54
+ }
55
+
56
+ export const Text = React.forwardRef<HTMLElement, TextProps>(
57
+ ({ asChild, span, size, weight, tone, align, truncate, mono, variant, ...props }, ref) => {
58
+ const Comp: React.ElementType = asChild ? Slot : span ? 'span' : variant === 'inline-code' ? 'code' : 'p'
59
+ return (
60
+ <Comp
61
+ ref={ref as React.Ref<HTMLParagraphElement>}
62
+ className={cn(text({ size, weight, tone, align, truncate, mono, variant }))}
63
+ {...props}
64
+ />
65
+ )
66
+ },
67
+ )
68
+ Text.displayName = 'Text'
69
+
70
+ /**
71
+ * Headings — semantic `H1`–`H6` components. Each picks the right element and a
72
+ * sensible default size; override `size`/`weight`/`tone` to decouple visual
73
+ * scale from document outline.
74
+ *
75
+ * <H1>Page title</H1>
76
+ * <H3 size="lg">Section</H3>
77
+ */
78
+ export interface HeadingProps
79
+ extends NoStyle<React.HTMLAttributes<HTMLHeadingElement>>,
80
+ Pick<VariantProps<typeof text>, 'size' | 'weight' | 'tone' | 'align' | 'truncate'> {}
81
+
82
+ type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6
83
+ const HEADING_SIZE: Record<HeadingLevel, NonNullable<VariantProps<typeof text>['size']>> = {
84
+ 1: '3xl', 2: '2xl', 3: 'xl', 4: 'lg', 5: 'base', 6: 'sm',
85
+ }
86
+
87
+ function createHeading(level: HeadingLevel) {
88
+ const Tag = `h${level}` as const
89
+ const Component = React.forwardRef<HTMLHeadingElement, HeadingProps>(
90
+ ({ size, weight = 'bold', tone, align, truncate, ...props }, ref) => (
91
+ <Tag
92
+ ref={ref}
93
+ className={cn(text({ size: size ?? HEADING_SIZE[level], weight, tone, align, truncate }), 'tracking-tight')}
94
+ {...props}
95
+ />
96
+ ),
97
+ )
98
+ Component.displayName = `H${level}`
99
+ return Component
100
+ }
101
+
102
+ export const H1 = createHeading(1)
103
+ export const H2 = createHeading(2)
104
+ export const H3 = createHeading(3)
105
+ export const H4 = createHeading(4)
106
+ export const H5 = createHeading(5)
107
+ export const H6 = createHeading(6)
108
+
109
+ export { text as textVariants }
@@ -0,0 +1,27 @@
1
+ import * as React from 'react'
2
+ import { cn } from '../lib/utils'
3
+ import type { NoStyle } from '../lib/types'
4
+ import { focusRingField } from '../lib/recipes'
5
+
6
+ export interface TextareaProps extends NoStyle<React.TextareaHTMLAttributes<HTMLTextAreaElement>> {
7
+ /** Error state — red control outline + aria-invalid. Usually set for you by FormField. */
8
+ invalid?: boolean
9
+ }
10
+
11
+ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
12
+ ({ invalid, ...props }, ref) => (
13
+ <textarea
14
+ ref={ref}
15
+ aria-invalid={invalid || undefined}
16
+ className={cn(
17
+ 'flex min-h-20 w-full rounded-lg border border-strong bg-surface px-3 py-2 text-sm text-primary',
18
+ 'transition-colors placeholder:text-tertiary',
19
+ focusRingField,
20
+ 'disabled:cursor-not-allowed disabled:opacity-50',
21
+ invalid && 'border-error focus-visible:border-error',
22
+ )}
23
+ {...props}
24
+ />
25
+ ),
26
+ )
27
+ Textarea.displayName = 'Textarea'
@@ -0,0 +1,107 @@
1
+ import * as React from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { CheckCircle2, XCircle, AlertTriangle, Info, X } from 'lucide-react'
4
+ import { cn } from '../lib/utils'
5
+ import { focusRingInset } from '../lib/recipes'
6
+
7
+ /**
8
+ * Toast / Snackbar — a transient message. Call `toast({...})` from anywhere
9
+ * (event handlers, effects, outside React), and render `<Toaster />` once near
10
+ * the app root. Text stays the normal foreground; only the icon carries the
11
+ * status hue (matching Alert).
12
+ */
13
+ export type ToastVariant = 'neutral' | 'success' | 'error' | 'warning' | 'info'
14
+
15
+ export interface ToastOptions {
16
+ title?: React.ReactNode
17
+ description?: React.ReactNode
18
+ variant?: ToastVariant
19
+ duration?: number
20
+ action?: React.ReactNode
21
+ }
22
+ interface ToastRecord extends ToastOptions {
23
+ id: number
24
+ }
25
+
26
+ // ── tiny external store (callable outside React) ────────────────────────────────
27
+ let toasts: ToastRecord[] = []
28
+ const listeners = new Set<() => void>()
29
+ const emit = () => listeners.forEach((listener) => listener())
30
+ let nextId = 0
31
+
32
+ export function toast(options: ToastOptions): number {
33
+ const id = nextId++
34
+ const duration = options.duration ?? 5000
35
+ toasts = [...toasts, { variant: 'neutral', ...options, duration, id }]
36
+ emit()
37
+ if (duration > 0) window.setTimeout(() => dismissToast(id), duration)
38
+ return id
39
+ }
40
+ export function dismissToast(id: number) {
41
+ toasts = toasts.filter((existing) => existing.id !== id)
42
+ emit()
43
+ }
44
+
45
+ function useToasts() {
46
+ return React.useSyncExternalStore(
47
+ (onStoreChange) => {
48
+ listeners.add(onStoreChange)
49
+ return () => listeners.delete(onStoreChange)
50
+ },
51
+ () => toasts,
52
+ () => toasts,
53
+ )
54
+ }
55
+
56
+ const ICONS: Record<ToastVariant, React.ReactNode | null> = {
57
+ neutral: null,
58
+ success: <CheckCircle2 className="text-success" />,
59
+ error: <XCircle className="text-error" />,
60
+ warning: <AlertTriangle className="text-warning" />,
61
+ info: <Info className="text-info" />,
62
+ }
63
+
64
+ function ToastItem({ toast }: { toast: ToastRecord }) {
65
+ // Urgent variants interrupt the screen reader; the rest announce politely.
66
+ const urgent = toast.variant === 'error' || toast.variant === 'warning'
67
+ return (
68
+ <li
69
+ role={urgent ? 'alert' : 'status'}
70
+ aria-live={urgent ? 'assertive' : 'polite'}
71
+ className="pointer-events-auto flex w-80 gap-3 rounded-xl border border-border bg-surface p-4 text-primary shadow-lg animate-scale-in [&_svg]:size-5 [&_svg]:shrink-0"
72
+ >
73
+ {toast.variant && toast.variant !== 'neutral' && <span className="mt-0.5">{ICONS[toast.variant]}</span>}
74
+ <div className="min-w-0 flex-1">
75
+ {toast.title && <div className="font-semibold leading-snug">{toast.title}</div>}
76
+ {toast.description && <div className="text-sm text-primary">{toast.description}</div>}
77
+ {toast.action && <div className="mt-2">{toast.action}</div>}
78
+ </div>
79
+ <button
80
+ type="button"
81
+ aria-label="Dismiss"
82
+ onClick={() => dismissToast(toast.id)}
83
+ className={cn('-mr-1 -mt-1 h-fit rounded-md p-1 text-secondary opacity-70 transition-opacity hover:opacity-100', focusRingInset)}
84
+ >
85
+ <X className="h-4 w-4" />
86
+ </button>
87
+ </li>
88
+ )
89
+ }
90
+
91
+ const emptySubscribe = () => () => {}
92
+
93
+ export function Toaster() {
94
+ const activeToasts = useToasts()
95
+ // Hydration gate: false on the server snapshot, true once on the client — no
96
+ // mismatch, and no Effect needed to flip a `mounted` flag.
97
+ const mounted = React.useSyncExternalStore(emptySubscribe, () => true, () => false)
98
+ if (!mounted) return null
99
+ return createPortal(
100
+ <ol className="pointer-events-none fixed bottom-4 right-4 z-[100] flex w-80 flex-col gap-2">
101
+ {activeToasts.map((toast) => (
102
+ <ToastItem key={toast.id} toast={toast} />
103
+ ))}
104
+ </ol>,
105
+ document.body,
106
+ )
107
+ }
@@ -0,0 +1,103 @@
1
+ import * as React from 'react'
2
+ import { cva } from 'class-variance-authority'
3
+ import { cn } from '../lib/utils'
4
+ import type { NoStyle } from '../lib/types'
5
+ import { focusRingInset } from '../lib/recipes'
6
+
7
+ /**
8
+ * ToggleButtonGroup / ToggleButton — a segmented control. `type="single"` keeps
9
+ * one value selected; `type="multiple"` keeps an array. Controlled via
10
+ * `value`/`onValueChange`. Buttons share edges like a ButtonGroup.
11
+ */
12
+ type SingleProps = {
13
+ type?: 'single'
14
+ value: string | null
15
+ onValueChange: (value: string | null) => void
16
+ }
17
+ type MultipleProps = {
18
+ type: 'multiple'
19
+ value: string[]
20
+ onValueChange: (value: string[]) => void
21
+ }
22
+ export type ToggleButtonGroupProps = (SingleProps | MultipleProps) &
23
+ NoStyle<Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'>>
24
+
25
+ interface ToggleGroupValue {
26
+ isSelected: (itemValue: string) => boolean
27
+ toggle: (itemValue: string) => void
28
+ }
29
+ const ToggleGroupContext = React.createContext<ToggleGroupValue | null>(null)
30
+
31
+ export function ToggleButtonGroup({
32
+ type = 'single', value, onValueChange, children, ...props
33
+ }: ToggleButtonGroupProps) {
34
+ const contextValue = React.useMemo<ToggleGroupValue>(() => {
35
+ if (type === 'multiple') {
36
+ const values = (value as string[]) ?? []
37
+ return {
38
+ isSelected: (itemValue) => values.includes(itemValue),
39
+ toggle: (itemValue) =>
40
+ (onValueChange as MultipleProps['onValueChange'])(
41
+ values.includes(itemValue) ? values.filter((existing) => existing !== itemValue) : [...values, itemValue],
42
+ ),
43
+ }
44
+ }
45
+ return {
46
+ isSelected: (itemValue) => value === itemValue,
47
+ toggle: (itemValue) => (onValueChange as SingleProps['onValueChange'])(value === itemValue ? null : itemValue),
48
+ }
49
+ }, [type, value, onValueChange])
50
+
51
+ return (
52
+ <ToggleGroupContext.Provider value={contextValue}>
53
+ <div
54
+ role="group"
55
+ className={cn(
56
+ 'inline-flex isolate',
57
+ '[&>*]:rounded-none [&>*:first-child]:rounded-l-lg [&>*:last-child]:rounded-r-lg [&>*:not(:first-child)]:-ml-px',
58
+ )}
59
+ {...props}
60
+ >
61
+ {children}
62
+ </div>
63
+ </ToggleGroupContext.Provider>
64
+ )
65
+ }
66
+
67
+ const toggleButton = cva(
68
+ 'inline-flex h-10 items-center justify-center gap-2 border border-strong px-4 text-sm font-medium transition-colors ' +
69
+ 'disabled:opacity-50 disabled:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 ' + focusRingInset,
70
+ {
71
+ variants: {
72
+ selected: {
73
+ true: 'z-10 bg-surface-inverse text-inverse border-transparent',
74
+ false: 'bg-surface text-secondary hover:bg-surface-subtle hover:text-primary',
75
+ },
76
+ },
77
+ },
78
+ )
79
+
80
+ export interface ToggleButtonProps extends NoStyle<React.ButtonHTMLAttributes<HTMLButtonElement>> {
81
+ value: string
82
+ }
83
+
84
+ export const ToggleButton = React.forwardRef<HTMLButtonElement, ToggleButtonProps>(
85
+ ({ value, onClick, ...props }, ref) => {
86
+ const group = React.useContext(ToggleGroupContext)
87
+ const selected = group?.isSelected(value) ?? false
88
+ return (
89
+ <button
90
+ ref={ref}
91
+ type="button"
92
+ aria-pressed={selected}
93
+ className={cn(toggleButton({ selected }))}
94
+ onClick={(event) => {
95
+ group?.toggle(value)
96
+ onClick?.(event)
97
+ }}
98
+ {...props}
99
+ />
100
+ )
101
+ },
102
+ )
103
+ ToggleButton.displayName = 'ToggleButton'
@@ -0,0 +1,26 @@
1
+ import * as React from 'react'
2
+ import * as TooltipPrimitive from '@radix-ui/react-tooltip'
3
+ import { cn } from '../lib/utils'
4
+ import type { NoStyle } from '../lib/types'
5
+
6
+ export const TooltipProvider = TooltipPrimitive.Provider
7
+ export const Tooltip = TooltipPrimitive.Root
8
+ export const TooltipTrigger = TooltipPrimitive.Trigger
9
+
10
+ export const TooltipContent = React.forwardRef<
11
+ React.ElementRef<typeof TooltipPrimitive.Content>,
12
+ NoStyle<React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>>
13
+ >(({ sideOffset = 6, ...props }, ref) => (
14
+ <TooltipPrimitive.Portal>
15
+ <TooltipPrimitive.Content
16
+ ref={ref}
17
+ sideOffset={sideOffset}
18
+ className={cn(
19
+ 'z-50 overflow-hidden rounded-md bg-surface-inverse px-2.5 py-1.5 text-sm font-medium text-inverse shadow-md',
20
+ 'animate-scale-in',
21
+ )}
22
+ {...props}
23
+ />
24
+ </TooltipPrimitive.Portal>
25
+ ))
26
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName
@@ -0,0 +1,55 @@
1
+ import * as React from 'react'
2
+ import { cn } from '../lib/utils'
3
+
4
+ /**
5
+ * The Windforge brand mark — a hammer ("forge") fused with a wind swirl.
6
+ * Plain SVG (no MUI SvgIcon dependency): sized by `className` (default 1.5rem),
7
+ * renders the brand purple gradient. The gradient id is per-instance (useId) so
8
+ * many marks on one page stay collision-free.
9
+ */
10
+ const WINDFORGE_PATH =
11
+ 'M403.1,144.03 403.39,140.32 401.1,134.61 349.97,85.76 345.97,84.05 340.26,84.62 328.27,95.47 324.84,95.47 283.14,58.34 270,52.06 262,52.63 252.86,57.77 200.02,115.75 196.59,122.04 199.44,131.75 261.71,190.02 256,202.02 236.01,230.58 236.29,232.58 260.28,236.01 290.56,242.86 327.69,257.14 351.69,273.14 390.53,310.27 394.53,311.98 399.67,311.41 403.39,308.27 455.37,248.86 461.09,236.86 461.09,229.44 456.52,219.72 394.82,160.88 394.82,157.46 403.1,144.03Z M138.03,155.46 150.6,154.89 159.74,157.17 180.31,169.74 177.16,161.45 169.74,150.03 156.31,137.75 143.17,130.89 130.04,127.46 110.61,127.46 95.76,131.46 78.62,141.17 66.34,153.46 56.06,171.17 50.91,192.3 50.91,204.3 53.77,219.72 57.77,230.58 66.34,244.86 86.05,263.43 102.61,272.57 115.18,277.14 148.89,283.42 183.73,284.56 246.57,279.99 272.85,280.56 295.13,283.42 316.27,288.56 334.55,295.42 355.69,306.27 372.82,318.27 373.68,317.41 351.12,293.7 339.69,283.99 320.84,271.42 297.99,260.57 257.43,249.14 167.17,236.58 139.75,228.01 124.89,219.44 115.47,210.01 111.47,203.16 108.61,193.45 109.18,180.88 113.75,170.02 120.89,162.31 131.18,156.6 138.03,155.46Z M277.42,296.56 276.28,294.28 248.29,293.13 182.59,297.13 88.62,402.53 84.05,412.24 83.48,424.81 85.76,432.24 92.05,442.52 102.04,451.95 116.32,458.8 129.46,459.94 143.75,454.23 277.42,296.56Z'
12
+
13
+ export interface ForgeIconProps extends React.SVGProps<SVGSVGElement> {}
14
+
15
+ export const ForgeIcon = React.forwardRef<SVGSVGElement, ForgeIconProps>(
16
+ ({ className, ...props }, ref) => {
17
+ const gradId = `forge-${React.useId().replace(/:/g, '')}`
18
+ return (
19
+ <svg
20
+ ref={ref}
21
+ viewBox="0 0 512 512"
22
+ role="img"
23
+ aria-label="Windforge"
24
+ className={cn('h-6 w-6', className)}
25
+ {...props}
26
+ >
27
+ <defs>
28
+ <linearGradient id={gradId} x1="0" y1="1" x2="1" y2="0">
29
+ <stop offset="0" stopColor="var(--wf-color-brand-primary-active)" />
30
+ <stop offset="1" stopColor="var(--wf-color-brand-secondary)" />
31
+ </linearGradient>
32
+ </defs>
33
+ <path fill={`url(#${gradId})`} fillRule="evenodd" d={WINDFORGE_PATH} />
34
+ </svg>
35
+ )
36
+ },
37
+ )
38
+ ForgeIcon.displayName = 'ForgeIcon'
39
+
40
+ export interface WindforgeLogoProps {
41
+ className?: string
42
+ /** Hide the wordmark, show only the mark. */
43
+ markOnly?: boolean
44
+ }
45
+
46
+ export function WindforgeLogo({ className, markOnly = false }: WindforgeLogoProps) {
47
+ return (
48
+ <span className={cn('inline-flex items-center gap-2.5', className)}>
49
+ <ForgeIcon className="h-8 w-8 drop-shadow-sm" />
50
+ {!markOnly && (
51
+ <span className="text-base font-bold tracking-tight text-primary leading-none">WindForge</span>
52
+ )}
53
+ </span>
54
+ )
55
+ }
@@ -0,0 +1,60 @@
1
+ import {
2
+ // arrows & chevrons
3
+ ArrowUp, ArrowDown, ArrowLeft, ArrowRight, ArrowUpRight, ChevronUp, ChevronDown,
4
+ ChevronLeft, ChevronRight, ChevronsUpDown, ChevronsLeft, ChevronsRight, CornerDownLeft,
5
+ // navigation & layout
6
+ Menu, MoreHorizontal, MoreVertical, Home, Search, LayoutDashboard, LayoutGrid,
7
+ PanelLeft, PanelRight, PanelTop, Map, MapPin, Globe, ExternalLink, Link, Filter,
8
+ // actions
9
+ Plus, Minus, X, Check, Copy, ClipboardCheck, Pencil, Trash2, Save, Download, Upload,
10
+ Share2, RefreshCw, RotateCw, Settings, SlidersHorizontal, Send, Eye, EyeOff, Maximize2,
11
+ // status & feedback
12
+ Info, AlertTriangle, AlertCircle, CheckCircle2, XCircle, HelpCircle, Ban, ShieldCheck,
13
+ Shield, Bell, BellRing, BellOff, LoaderCircle, Star, Heart, Bookmark, Flag, ThumbsUp,
14
+ // files & data
15
+ File, FileText, Folder, FolderOpen, Image, Paperclip, Inbox, Archive, Package, Box,
16
+ Boxes, Database, Server, Cloud, Calendar, Clock,
17
+ // communication
18
+ Mail, MessageSquare, MessageCircle, Phone, AtSign, Hash, Megaphone, Rss,
19
+ // media
20
+ Play, Pause, SkipForward, SkipBack, Volume2, VolumeX, Mic, MicOff, Camera, Video, Music,
21
+ // text & formatting
22
+ Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, AlignJustify, List,
23
+ ListOrdered, ListChecks, Type, Code2, Quote,
24
+ // people & commerce
25
+ User, Users, UserPlus, CircleUser, LogIn, LogOut, CreditCard, ShoppingCart, DollarSign,
26
+ Wallet, Gift, Briefcase, Building2,
27
+ // toggles & shapes
28
+ ToggleLeft, ToggleRight, Circle, CircleDot, CircleDashed, Square, CheckSquare, Shapes, Tag,
29
+ // theming & system
30
+ Palette, SwatchBook, Sun, Moon, MonitorSmartphone, Sparkles, Rocket, Zap, Layers, Ruler,
31
+ Gauge, Accessibility, Lock, Unlock, Bot, Terminal, GitBranch, TrendingUp, Activity,
32
+ type LucideIcon,
33
+ } from 'lucide-react'
34
+
35
+ /**
36
+ * COMMONLY-USED LUCIDE ICONS — a reference selection, grouped by purpose.
37
+ *
38
+ * lucide-react is the design system's supported icon library; consumers import
39
+ * icons directly from it. These are NOT re-exported as Windforge icons — this
40
+ * map just powers a browsable, on-brand subset on the Iconography page (the ones
41
+ * the system actually uses plus the obvious gaps), with an escape hatch to the
42
+ * full library for the long tail. Object-shorthand keys keep each entry's name in
43
+ * sync with its component, and the explicit imports keep this list tree-shakeable.
44
+ */
45
+ export const commonIconGroups: { label: string; icons: Record<string, LucideIcon> }[] = [
46
+ { label: 'Arrows & chevrons', icons: { ArrowUp, ArrowDown, ArrowLeft, ArrowRight, ArrowUpRight, ChevronUp, ChevronDown, ChevronLeft, ChevronRight, ChevronsUpDown, ChevronsLeft, ChevronsRight, CornerDownLeft } },
47
+ { label: 'Navigation & layout', icons: { Menu, MoreHorizontal, MoreVertical, Home, Search, LayoutDashboard, LayoutGrid, PanelLeft, PanelRight, PanelTop, Map, MapPin, Globe, ExternalLink, Link, Filter } },
48
+ { label: 'Actions', icons: { Plus, Minus, X, Check, Copy, ClipboardCheck, Pencil, Trash2, Save, Download, Upload, Share2, RefreshCw, RotateCw, Settings, SlidersHorizontal, Send, Eye, EyeOff, Maximize2 } },
49
+ { label: 'Status & feedback', icons: { Info, AlertTriangle, AlertCircle, CheckCircle2, XCircle, HelpCircle, Ban, ShieldCheck, Shield, Bell, BellRing, BellOff, LoaderCircle, Star, Heart, Bookmark, Flag, ThumbsUp } },
50
+ { label: 'Files & data', icons: { File, FileText, Folder, FolderOpen, Image, Paperclip, Inbox, Archive, Package, Box, Boxes, Database, Server, Cloud, Calendar, Clock } },
51
+ { label: 'Communication', icons: { Mail, MessageSquare, MessageCircle, Phone, AtSign, Hash, Megaphone, Rss } },
52
+ { label: 'Media', icons: { Play, Pause, SkipForward, SkipBack, Volume2, VolumeX, Mic, MicOff, Camera, Video, Music } },
53
+ { label: 'Text & formatting', icons: { Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, AlignJustify, List, ListOrdered, ListChecks, Type, Code2, Quote } },
54
+ { label: 'People & commerce', icons: { User, Users, UserPlus, CircleUser, LogIn, LogOut, CreditCard, ShoppingCart, DollarSign, Wallet, Gift, Briefcase, Building2 } },
55
+ { label: 'Toggles & shapes', icons: { ToggleLeft, ToggleRight, Circle, CircleDot, CircleDashed, Square, CheckSquare, Shapes, Tag } },
56
+ { label: 'Theming & system', icons: { Palette, SwatchBook, Sun, Moon, MonitorSmartphone, Sparkles, Rocket, Zap, Layers, Ruler, Gauge, Accessibility, Lock, Unlock, Bot, Terminal, GitBranch, TrendingUp, Activity } },
57
+ ]
58
+
59
+ /** The flat name → component map across all groups. */
60
+ export const commonIcons: Record<string, LucideIcon> = Object.assign({}, ...commonIconGroups.map((g) => g.icons))
@@ -0,0 +1,43 @@
1
+ import * as React from 'react'
2
+
3
+ export interface SVGIconProps extends React.SVGProps<SVGSVGElement> {
4
+ /** SVG viewBox — defaults to the 24×24 grid lucide draws on, so custom icons
5
+ * line up with the rest of the set. */
6
+ viewBox?: string
7
+ /** Accessible label. Omit for a purely decorative icon (rendered aria-hidden). */
8
+ label?: string
9
+ }
10
+
11
+ /**
12
+ * SVGIcon — render a custom icon with the same conventions as the supported
13
+ * lucide set: a 24×24 viewBox, 2px round strokes painted in `currentColor`, and
14
+ * sizing through h-/w- utilities. Pass the shape as children; override `stroke`,
15
+ * `fill`, or `strokeWidth` for a filled or heavier glyph.
16
+ *
17
+ * <SVGIcon label="Spark" className="h-5 w-5 text-brand">
18
+ * <path d="M12 2 4 7v10l8 5 8-5V7z" />
19
+ * </SVGIcon>
20
+ */
21
+ export const SVGIcon = React.forwardRef<SVGSVGElement, SVGIconProps>(
22
+ ({ viewBox = '0 0 24 24', label, children, ...props }, ref) => (
23
+ <svg
24
+ ref={ref}
25
+ xmlns="http://www.w3.org/2000/svg"
26
+ viewBox={viewBox}
27
+ width={24}
28
+ height={24}
29
+ fill="none"
30
+ stroke="currentColor"
31
+ strokeWidth={2}
32
+ strokeLinecap="round"
33
+ strokeLinejoin="round"
34
+ role={label ? 'img' : undefined}
35
+ aria-label={label}
36
+ aria-hidden={label ? undefined : true}
37
+ {...props}
38
+ >
39
+ {children}
40
+ </svg>
41
+ ),
42
+ )
43
+ SVGIcon.displayName = 'SVGIcon'
package/src/index.ts ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @windforge/ui — the single import surface.
3
+ *
4
+ * Re-exports every component, layout, the brand mark, the `cn` helper, and the
5
+ * full @windforge/tokens surface (wf* constants, brandVars/brandRamp, source maps).
6
+ * Consumers import from here and nowhere else.
7
+ */
8
+
9
+ // ── catalog (machine-readable component registry) ──
10
+ export * from './catalog'
11
+
12
+ // ── utilities ──
13
+ export { cn } from './lib/utils'
14
+ export { useMediaQuery } from './lib/use-media-query'
15
+ export * from './lib/recipes'
16
+
17
+ // ── primitives ──
18
+ export * from './components/button'
19
+ export * from './components/button-group'
20
+ export * from './components/toggle-button'
21
+ export * from './components/badge'
22
+ export * from './components/chip'
23
+ export * from './components/text'
24
+ export * from './components/link'
25
+ export * from './components/card'
26
+ export * from './components/input'
27
+ export * from './components/textarea'
28
+ export * from './components/label'
29
+ export * from './components/form-field'
30
+ export * from './components/separator'
31
+ export * from './components/skeleton'
32
+ export * from './components/code-block'
33
+ export * from './components/alert'
34
+ export * from './components/layout'
35
+
36
+ // ── interactive (Radix-backed) ──
37
+ export * from './components/switch'
38
+ export * from './components/checkbox'
39
+ export * from './components/radio-group'
40
+ export * from './components/tabs'
41
+ export * from './components/tooltip'
42
+ export * from './components/select'
43
+ export * from './components/autocomplete'
44
+ export * from './components/modal'
45
+ export * from './components/dialog'
46
+ export * from './components/pagination'
47
+ export * from './components/stepper'
48
+ export * from './components/toast'
49
+ export * from './components/accordion'
50
+ export * from './components/avatar'
51
+ export * from './components/progress'
52
+ export * from './components/slider'
53
+ export * from './components/popover'
54
+ export * from './components/dropdown-menu'
55
+ export * from './components/table'
56
+ export * from './components/data-table'
57
+ export * from './components/breadcrumb'
58
+ export * from './components/sheet'
59
+ export * from './components/multi-select'
60
+ export * from './components/command'
61
+ export * from './components/calendar'
62
+ export * from './components/date-picker'
63
+ export * from './components/chart'
64
+
65
+ // ── brand & icons ──
66
+ // lucide-react is the supported icon library (imported directly by consumers).
67
+ // `commonIcons`/`commonIconGroups` are a reference selection for the docs gallery;
68
+ // `SVGIcon` wraps a custom SVG in the same conventions.
69
+ export * from './icons/forge-icon'
70
+ export * from './icons/svg-icon'
71
+ export { commonIcons, commonIconGroups } from './icons/icon-set'
72
+
73
+ // ── layouts ──
74
+ export * from './layouts/theme-provider'
75
+ export * from './layouts/app-shell'
76
+ export * from './layouts/side-nav'
77
+ export * from './layouts/app-bar'
78
+
79
+ // ── tokens (single token surface) ──
80
+ export * from '@windforge/tokens'