@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.
- package/dist/index.d.ts +1195 -0
- package/dist/index.js +3628 -0
- package/package.json +66 -0
- package/src/catalog.ts +654 -0
- package/src/components/accordion.tsx +91 -0
- package/src/components/alert.tsx +58 -0
- package/src/components/autocomplete.tsx +174 -0
- package/src/components/avatar.tsx +60 -0
- package/src/components/badge.tsx +37 -0
- package/src/components/breadcrumb.tsx +62 -0
- package/src/components/button-group.tsx +23 -0
- package/src/components/button.tsx +53 -0
- package/src/components/calendar.tsx +61 -0
- package/src/components/card.tsx +72 -0
- package/src/components/chart.tsx +130 -0
- package/src/components/checkbox.tsx +27 -0
- package/src/components/chip.tsx +75 -0
- package/src/components/code-block.tsx +126 -0
- package/src/components/command.tsx +139 -0
- package/src/components/data-table.tsx +194 -0
- package/src/components/date-picker.tsx +77 -0
- package/src/components/dialog.tsx +57 -0
- package/src/components/dropdown-menu.tsx +186 -0
- package/src/components/form-field.tsx +97 -0
- package/src/components/input.tsx +29 -0
- package/src/components/label.tsx +18 -0
- package/src/components/layout.tsx +179 -0
- package/src/components/link.tsx +37 -0
- package/src/components/modal.tsx +67 -0
- package/src/components/multi-select.tsx +175 -0
- package/src/components/pagination.tsx +72 -0
- package/src/components/popover.tsx +25 -0
- package/src/components/progress.tsx +31 -0
- package/src/components/radio-group.tsx +34 -0
- package/src/components/select.tsx +134 -0
- package/src/components/separator.tsx +21 -0
- package/src/components/sheet.tsx +80 -0
- package/src/components/skeleton.tsx +11 -0
- package/src/components/slider.tsx +28 -0
- package/src/components/stepper.tsx +69 -0
- package/src/components/switch.tsx +33 -0
- package/src/components/table.tsx +121 -0
- package/src/components/tabs.tsx +90 -0
- package/src/components/text.tsx +109 -0
- package/src/components/textarea.tsx +27 -0
- package/src/components/toast.tsx +107 -0
- package/src/components/toggle-button.tsx +103 -0
- package/src/components/tooltip.tsx +26 -0
- package/src/icons/forge-icon.tsx +55 -0
- package/src/icons/icon-set.ts +60 -0
- package/src/icons/svg-icon.tsx +43 -0
- package/src/index.ts +80 -0
- package/src/layouts/app-bar.tsx +95 -0
- package/src/layouts/app-shell.tsx +80 -0
- package/src/layouts/side-nav.tsx +196 -0
- package/src/layouts/theme-provider.tsx +128 -0
- package/src/lib/recipes.ts +50 -0
- package/src/lib/types.ts +3 -0
- package/src/lib/use-media-query.ts +18 -0
- package/src/lib/utils.ts +10 -0
- 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'
|