@fr0mpy/component-system 2.0.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/bin/cli.js +283 -0
- package/index.js +12 -0
- package/package.json +45 -0
- package/templates/commands/component-harness.md +116 -0
- package/templates/commands/setup-styling.md +111 -0
- package/templates/component-recipes/accordion.md +153 -0
- package/templates/component-recipes/alert.md +145 -0
- package/templates/component-recipes/avatar.md +165 -0
- package/templates/component-recipes/badge.md +126 -0
- package/templates/component-recipes/breadcrumb.md +220 -0
- package/templates/component-recipes/button.md +90 -0
- package/templates/component-recipes/card.md +130 -0
- package/templates/component-recipes/carousel.md +277 -0
- package/templates/component-recipes/checkbox.md +117 -0
- package/templates/component-recipes/collapsible.md +201 -0
- package/templates/component-recipes/combobox.md +193 -0
- package/templates/component-recipes/context-menu.md +254 -0
- package/templates/component-recipes/dialog.md +193 -0
- package/templates/component-recipes/drawer.md +196 -0
- package/templates/component-recipes/dropdown-menu.md +263 -0
- package/templates/component-recipes/hover-card.md +230 -0
- package/templates/component-recipes/input.md +113 -0
- package/templates/component-recipes/label.md +259 -0
- package/templates/component-recipes/modal.md +155 -0
- package/templates/component-recipes/navigation-menu.md +310 -0
- package/templates/component-recipes/pagination.md +223 -0
- package/templates/component-recipes/popover.md +156 -0
- package/templates/component-recipes/progress.md +185 -0
- package/templates/component-recipes/radio.md +148 -0
- package/templates/component-recipes/select.md +154 -0
- package/templates/component-recipes/separator.md +124 -0
- package/templates/component-recipes/skeleton.md +186 -0
- package/templates/component-recipes/slider.md +114 -0
- package/templates/component-recipes/spinner.md +225 -0
- package/templates/component-recipes/switch.md +100 -0
- package/templates/component-recipes/table.md +161 -0
- package/templates/component-recipes/tabs.md +145 -0
- package/templates/component-recipes/textarea.md +234 -0
- package/templates/component-recipes/toast.md +209 -0
- package/templates/component-recipes/toggle-group.md +216 -0
- package/templates/component-recipes/tooltip.md +115 -0
- package/templates/hooks/triggers.d/styling.json +23 -0
- package/templates/skills/styling.md +173 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Toast/Notification Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Viewport container (fixed position, stacks toasts)
|
|
5
|
+
- Individual toast with title, description, action
|
|
6
|
+
- Auto-dismiss with progress indicator (optional)
|
|
7
|
+
- Support variants: default, success, error, warning
|
|
8
|
+
|
|
9
|
+
## Tailwind Classes
|
|
10
|
+
|
|
11
|
+
### Viewport (container for all toasts)
|
|
12
|
+
```
|
|
13
|
+
fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4
|
|
14
|
+
sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Toast
|
|
18
|
+
```
|
|
19
|
+
group pointer-events-auto relative flex w-full items-center justify-between space-x-2
|
|
20
|
+
overflow-hidden {tokens.radius} border border-border p-4 pr-6 {tokens.shadow}
|
|
21
|
+
transition-all
|
|
22
|
+
data-[swipe=cancel]:translate-x-0
|
|
23
|
+
data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)]
|
|
24
|
+
data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)]
|
|
25
|
+
data-[swipe=move]:transition-none
|
|
26
|
+
data-[state=open]:animate-in data-[state=closed]:animate-out
|
|
27
|
+
data-[swipe=end]:animate-out
|
|
28
|
+
data-[state=closed]:fade-out-80
|
|
29
|
+
data-[state=closed]:slide-out-to-right-full
|
|
30
|
+
data-[state=open]:slide-in-from-top-full
|
|
31
|
+
data-[state=open]:sm:slide-in-from-bottom-full
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Variants
|
|
35
|
+
```
|
|
36
|
+
default: bg-background text-foreground
|
|
37
|
+
success: bg-green-500 text-white border-green-600
|
|
38
|
+
error: bg-destructive text-destructive-foreground border-destructive
|
|
39
|
+
warning: bg-yellow-500 text-white border-yellow-600
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Title
|
|
43
|
+
```
|
|
44
|
+
text-sm font-semibold [&+div]:text-xs
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Description
|
|
48
|
+
```
|
|
49
|
+
text-sm opacity-90
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Action Button
|
|
53
|
+
```
|
|
54
|
+
inline-flex h-8 shrink-0 items-center justify-center {tokens.radius}
|
|
55
|
+
border border-border bg-transparent px-3 text-sm font-medium
|
|
56
|
+
transition-colors hover:bg-secondary
|
|
57
|
+
focus:outline-none focus:ring-1 focus:ring-ring
|
|
58
|
+
disabled:pointer-events-none disabled:opacity-50
|
|
59
|
+
group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30
|
|
60
|
+
group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground
|
|
61
|
+
group-[.destructive]:focus:ring-destructive
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Close Button
|
|
65
|
+
```
|
|
66
|
+
absolute right-1 top-1 rounded-md p-1 text-foreground/50
|
|
67
|
+
opacity-0 transition-opacity hover:text-foreground
|
|
68
|
+
focus:opacity-100 focus:outline-none focus:ring-1
|
|
69
|
+
group-hover:opacity-100
|
|
70
|
+
group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50
|
|
71
|
+
group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Props Interface
|
|
75
|
+
```typescript
|
|
76
|
+
interface ToastProps {
|
|
77
|
+
id: string
|
|
78
|
+
title?: string
|
|
79
|
+
description?: string
|
|
80
|
+
action?: React.ReactNode
|
|
81
|
+
variant?: 'default' | 'success' | 'error' | 'warning'
|
|
82
|
+
duration?: number
|
|
83
|
+
onOpenChange?: (open: boolean) => void
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface ToastActionProps {
|
|
87
|
+
altText: string
|
|
88
|
+
onClick: () => void
|
|
89
|
+
children: React.ReactNode
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Hook interface
|
|
93
|
+
function useToast(): {
|
|
94
|
+
toast: (props: Omit<ToastProps, 'id'>) => void
|
|
95
|
+
dismiss: (id?: string) => void
|
|
96
|
+
toasts: ToastProps[]
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Do
|
|
101
|
+
- Use a toast provider at app root
|
|
102
|
+
- Support swipe-to-dismiss on mobile
|
|
103
|
+
- Include close button
|
|
104
|
+
- Use appropriate variant for context
|
|
105
|
+
- Limit visible toasts (3-5 max)
|
|
106
|
+
|
|
107
|
+
## Don't
|
|
108
|
+
- Hardcode colors
|
|
109
|
+
- Skip auto-dismiss (except for errors)
|
|
110
|
+
- Use for critical information (use Alert)
|
|
111
|
+
- Stack too many toasts
|
|
112
|
+
|
|
113
|
+
## Example
|
|
114
|
+
```tsx
|
|
115
|
+
import * as ToastPrimitives from '@radix-ui/react-toast'
|
|
116
|
+
import { X } from 'lucide-react'
|
|
117
|
+
import { cn } from '@/lib/utils'
|
|
118
|
+
|
|
119
|
+
const ToastProvider = ToastPrimitives.Provider
|
|
120
|
+
|
|
121
|
+
const ToastViewport = ({ className, ...props }) => (
|
|
122
|
+
<ToastPrimitives.Viewport
|
|
123
|
+
className={cn(
|
|
124
|
+
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4',
|
|
125
|
+
'sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
|
|
126
|
+
className
|
|
127
|
+
)}
|
|
128
|
+
{...props}
|
|
129
|
+
/>
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const toastVariants = {
|
|
133
|
+
default: 'bg-background text-foreground border-border',
|
|
134
|
+
success: 'bg-green-500 text-white border-green-600',
|
|
135
|
+
error: 'bg-destructive text-destructive-foreground border-destructive',
|
|
136
|
+
warning: 'bg-yellow-500 text-white border-yellow-600',
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const Toast = ({ className, variant = 'default', ...props }) => (
|
|
140
|
+
<ToastPrimitives.Root
|
|
141
|
+
className={cn(
|
|
142
|
+
'group pointer-events-auto relative flex w-full items-center justify-between space-x-2',
|
|
143
|
+
'overflow-hidden rounded-lg border p-4 pr-6 shadow-lg transition-all',
|
|
144
|
+
'data-[swipe=cancel]:translate-x-0',
|
|
145
|
+
'data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)]',
|
|
146
|
+
'data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)]',
|
|
147
|
+
'data-[swipe=move]:transition-none',
|
|
148
|
+
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
149
|
+
'data-[swipe=end]:animate-out data-[state=closed]:fade-out-80',
|
|
150
|
+
'data-[state=closed]:slide-out-to-right-full',
|
|
151
|
+
'data-[state=open]:slide-in-from-top-full',
|
|
152
|
+
'data-[state=open]:sm:slide-in-from-bottom-full',
|
|
153
|
+
toastVariants[variant],
|
|
154
|
+
className
|
|
155
|
+
)}
|
|
156
|
+
{...props}
|
|
157
|
+
/>
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const ToastAction = ({ className, ...props }) => (
|
|
161
|
+
<ToastPrimitives.Action
|
|
162
|
+
className={cn(
|
|
163
|
+
'inline-flex h-8 shrink-0 items-center justify-center rounded-md',
|
|
164
|
+
'border bg-transparent px-3 text-sm font-medium',
|
|
165
|
+
'hover:bg-secondary focus:outline-none focus:ring-1',
|
|
166
|
+
className
|
|
167
|
+
)}
|
|
168
|
+
{...props}
|
|
169
|
+
/>
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
const ToastClose = ({ className, ...props }) => (
|
|
173
|
+
<ToastPrimitives.Close
|
|
174
|
+
className={cn(
|
|
175
|
+
'absolute right-1 top-1 rounded-md p-1 text-foreground/50',
|
|
176
|
+
'opacity-0 transition-opacity hover:text-foreground',
|
|
177
|
+
'focus:opacity-100 focus:outline-none group-hover:opacity-100',
|
|
178
|
+
className
|
|
179
|
+
)}
|
|
180
|
+
{...props}
|
|
181
|
+
>
|
|
182
|
+
<X className="h-4 w-4" />
|
|
183
|
+
</ToastPrimitives.Close>
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
const ToastTitle = ({ className, ...props }) => (
|
|
187
|
+
<ToastPrimitives.Title className={cn('text-sm font-semibold', className)} {...props} />
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const ToastDescription = ({ className, ...props }) => (
|
|
191
|
+
<ToastPrimitives.Description className={cn('text-sm opacity-90', className)} {...props} />
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
// Usage with hook
|
|
195
|
+
const { toast } = useToast()
|
|
196
|
+
|
|
197
|
+
toast({
|
|
198
|
+
title: 'Success!',
|
|
199
|
+
description: 'Your changes have been saved.',
|
|
200
|
+
variant: 'success',
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
toast({
|
|
204
|
+
title: 'Error',
|
|
205
|
+
description: 'Something went wrong.',
|
|
206
|
+
variant: 'error',
|
|
207
|
+
action: <ToastAction altText="Try again" onClick={retry}>Try again</ToastAction>,
|
|
208
|
+
})
|
|
209
|
+
```
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Toggle Group Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Group of toggle buttons
|
|
5
|
+
- Single or multiple selection modes
|
|
6
|
+
- Mutually exclusive options (single) or multi-select (multiple)
|
|
7
|
+
- Often used for view switchers, formatting options, filters
|
|
8
|
+
|
|
9
|
+
## Tailwind Classes
|
|
10
|
+
|
|
11
|
+
### Container
|
|
12
|
+
```
|
|
13
|
+
inline-flex items-center justify-center {tokens.radius} bg-muted p-1
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Item (Base)
|
|
17
|
+
```
|
|
18
|
+
inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5
|
|
19
|
+
text-sm font-medium ring-offset-background transition-all
|
|
20
|
+
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2
|
|
21
|
+
disabled:pointer-events-none disabled:opacity-50
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Item States
|
|
25
|
+
```
|
|
26
|
+
Default: text-muted-foreground hover:bg-background/50 hover:text-foreground
|
|
27
|
+
Active: bg-background text-foreground shadow-sm
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Variant: Outline
|
|
31
|
+
```
|
|
32
|
+
Container: inline-flex items-center rounded-lg border border-border
|
|
33
|
+
Item: border-r border-border last:border-r-0
|
|
34
|
+
Item Active: bg-muted
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Sizes
|
|
38
|
+
```
|
|
39
|
+
sm: h-8 px-2.5 text-xs
|
|
40
|
+
md: h-9 px-3 text-sm (default)
|
|
41
|
+
lg: h-10 px-4 text-sm
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### With Icons
|
|
45
|
+
```
|
|
46
|
+
Item: gap-2
|
|
47
|
+
Icon: h-4 w-4
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Full Width
|
|
51
|
+
```
|
|
52
|
+
Container: flex w-full
|
|
53
|
+
Item: flex-1
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Props Interface
|
|
57
|
+
```typescript
|
|
58
|
+
interface ToggleGroupProps {
|
|
59
|
+
type: 'single' | 'multiple'
|
|
60
|
+
value?: string | string[]
|
|
61
|
+
defaultValue?: string | string[]
|
|
62
|
+
onValueChange?: (value: string | string[]) => void
|
|
63
|
+
disabled?: boolean
|
|
64
|
+
className?: string
|
|
65
|
+
children: React.ReactNode
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface ToggleGroupItemProps {
|
|
69
|
+
value: string
|
|
70
|
+
disabled?: boolean
|
|
71
|
+
className?: string
|
|
72
|
+
children: React.ReactNode
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Single selection
|
|
76
|
+
interface ToggleGroupSingleProps {
|
|
77
|
+
type: 'single'
|
|
78
|
+
value?: string
|
|
79
|
+
defaultValue?: string
|
|
80
|
+
onValueChange?: (value: string) => void
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Multiple selection
|
|
84
|
+
interface ToggleGroupMultipleProps {
|
|
85
|
+
type: 'multiple'
|
|
86
|
+
value?: string[]
|
|
87
|
+
defaultValue?: string[]
|
|
88
|
+
onValueChange?: (value: string[]) => void
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Do
|
|
93
|
+
- Use Radix ToggleGroup primitive
|
|
94
|
+
- Clearly indicate selected state
|
|
95
|
+
- Support keyboard navigation
|
|
96
|
+
- Include proper ARIA attributes
|
|
97
|
+
- Group related options logically
|
|
98
|
+
|
|
99
|
+
## Don't
|
|
100
|
+
- Hardcode colors or dimensions
|
|
101
|
+
- Mix toggle group with regular buttons
|
|
102
|
+
- Use for navigation (use tabs or nav)
|
|
103
|
+
- Forget disabled states
|
|
104
|
+
|
|
105
|
+
## Example
|
|
106
|
+
```tsx
|
|
107
|
+
import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'
|
|
108
|
+
import { cn } from '@/lib/utils'
|
|
109
|
+
|
|
110
|
+
const ToggleGroup = ({ className, variant = 'default', size = 'md', ...props }) => (
|
|
111
|
+
<ToggleGroupPrimitive.Root
|
|
112
|
+
className={cn(
|
|
113
|
+
'inline-flex items-center justify-center rounded-lg',
|
|
114
|
+
variant === 'default' && 'bg-muted p-1',
|
|
115
|
+
variant === 'outline' && 'border border-border',
|
|
116
|
+
className
|
|
117
|
+
)}
|
|
118
|
+
{...props}
|
|
119
|
+
/>
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
const toggleGroupItemSizes = {
|
|
123
|
+
sm: 'h-8 px-2.5 text-xs',
|
|
124
|
+
md: 'h-9 px-3 text-sm',
|
|
125
|
+
lg: 'h-10 px-4 text-sm',
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const ToggleGroupItem = ({ className, variant = 'default', size = 'md', ...props }) => (
|
|
129
|
+
<ToggleGroupPrimitive.Item
|
|
130
|
+
className={cn(
|
|
131
|
+
'inline-flex items-center justify-center whitespace-nowrap rounded-md',
|
|
132
|
+
'font-medium ring-offset-background transition-all',
|
|
133
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2',
|
|
134
|
+
'disabled:pointer-events-none disabled:opacity-50',
|
|
135
|
+
variant === 'default' && [
|
|
136
|
+
'text-muted-foreground hover:bg-background/50 hover:text-foreground',
|
|
137
|
+
'data-[state=on]:bg-background data-[state=on]:text-foreground data-[state=on]:shadow-sm',
|
|
138
|
+
],
|
|
139
|
+
variant === 'outline' && [
|
|
140
|
+
'border-r border-border last:border-r-0',
|
|
141
|
+
'text-muted-foreground hover:bg-muted hover:text-foreground',
|
|
142
|
+
'data-[state=on]:bg-muted data-[state=on]:text-foreground',
|
|
143
|
+
],
|
|
144
|
+
toggleGroupItemSizes[size],
|
|
145
|
+
className
|
|
146
|
+
)}
|
|
147
|
+
{...props}
|
|
148
|
+
/>
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
// Usage examples
|
|
152
|
+
|
|
153
|
+
// View switcher (single selection)
|
|
154
|
+
import { LayoutGrid, List, Rows } from 'lucide-react'
|
|
155
|
+
|
|
156
|
+
const ViewToggle = ({ value, onChange }) => (
|
|
157
|
+
<ToggleGroup type="single" value={value} onValueChange={onChange}>
|
|
158
|
+
<ToggleGroupItem value="grid" aria-label="Grid view">
|
|
159
|
+
<LayoutGrid className="h-4 w-4" />
|
|
160
|
+
</ToggleGroupItem>
|
|
161
|
+
<ToggleGroupItem value="list" aria-label="List view">
|
|
162
|
+
<List className="h-4 w-4" />
|
|
163
|
+
</ToggleGroupItem>
|
|
164
|
+
<ToggleGroupItem value="rows" aria-label="Rows view">
|
|
165
|
+
<Rows className="h-4 w-4" />
|
|
166
|
+
</ToggleGroupItem>
|
|
167
|
+
</ToggleGroup>
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
// Text formatting (multiple selection)
|
|
171
|
+
import { Bold, Italic, Underline, Strikethrough } from 'lucide-react'
|
|
172
|
+
|
|
173
|
+
const TextFormatting = ({ value, onChange }) => (
|
|
174
|
+
<ToggleGroup type="multiple" value={value} onValueChange={onChange}>
|
|
175
|
+
<ToggleGroupItem value="bold" aria-label="Bold">
|
|
176
|
+
<Bold className="h-4 w-4" />
|
|
177
|
+
</ToggleGroupItem>
|
|
178
|
+
<ToggleGroupItem value="italic" aria-label="Italic">
|
|
179
|
+
<Italic className="h-4 w-4" />
|
|
180
|
+
</ToggleGroupItem>
|
|
181
|
+
<ToggleGroupItem value="underline" aria-label="Underline">
|
|
182
|
+
<Underline className="h-4 w-4" />
|
|
183
|
+
</ToggleGroupItem>
|
|
184
|
+
<ToggleGroupItem value="strikethrough" aria-label="Strikethrough">
|
|
185
|
+
<Strikethrough className="h-4 w-4" />
|
|
186
|
+
</ToggleGroupItem>
|
|
187
|
+
</ToggleGroup>
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
// Filter options with text
|
|
191
|
+
const FilterToggle = ({ value, onChange }) => (
|
|
192
|
+
<ToggleGroup type="single" value={value} onValueChange={onChange} variant="outline">
|
|
193
|
+
<ToggleGroupItem value="all">All</ToggleGroupItem>
|
|
194
|
+
<ToggleGroupItem value="active">Active</ToggleGroupItem>
|
|
195
|
+
<ToggleGroupItem value="completed">Completed</ToggleGroupItem>
|
|
196
|
+
</ToggleGroup>
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
// Full width toggle
|
|
200
|
+
const PlanToggle = ({ value, onChange }) => (
|
|
201
|
+
<ToggleGroup type="single" value={value} onValueChange={onChange} className="w-full">
|
|
202
|
+
<ToggleGroupItem value="monthly" className="flex-1">Monthly</ToggleGroupItem>
|
|
203
|
+
<ToggleGroupItem value="yearly" className="flex-1">
|
|
204
|
+
Yearly
|
|
205
|
+
<span className="ml-1 text-xs text-primary">Save 20%</span>
|
|
206
|
+
</ToggleGroupItem>
|
|
207
|
+
</ToggleGroup>
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
// With controlled state
|
|
211
|
+
const [view, setView] = useState('grid')
|
|
212
|
+
const [formatting, setFormatting] = useState<string[]>([])
|
|
213
|
+
|
|
214
|
+
<ViewToggle value={view} onChange={setView} />
|
|
215
|
+
<TextFormatting value={formatting} onChange={setFormatting} />
|
|
216
|
+
```
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Tooltip Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Trigger element (wraps any interactive element)
|
|
5
|
+
- Content popup with arrow
|
|
6
|
+
- Support multiple positions
|
|
7
|
+
- Delay on hover before showing
|
|
8
|
+
|
|
9
|
+
## Tailwind Classes
|
|
10
|
+
|
|
11
|
+
### Content
|
|
12
|
+
```
|
|
13
|
+
z-50 overflow-hidden {tokens.radius} bg-foreground px-3 py-1.5 text-xs text-background
|
|
14
|
+
animate-in fade-in-0 zoom-in-95
|
|
15
|
+
data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95
|
|
16
|
+
data-[side=bottom]:slide-in-from-top-2
|
|
17
|
+
data-[side=left]:slide-in-from-right-2
|
|
18
|
+
data-[side=right]:slide-in-from-left-2
|
|
19
|
+
data-[side=top]:slide-in-from-bottom-2
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Arrow
|
|
23
|
+
```
|
|
24
|
+
fill-foreground
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Alternative Light Style
|
|
28
|
+
```
|
|
29
|
+
bg-background border border-border text-foreground {tokens.shadow}
|
|
30
|
+
Arrow: fill-background stroke-border
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Props Interface
|
|
34
|
+
```typescript
|
|
35
|
+
interface TooltipProps {
|
|
36
|
+
content: React.ReactNode
|
|
37
|
+
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
38
|
+
align?: 'start' | 'center' | 'end'
|
|
39
|
+
delayDuration?: number
|
|
40
|
+
children: React.ReactNode
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
- Default delay: 200ms (adjust based on UX needs)
|
|
46
|
+
- Default side: 'top'
|
|
47
|
+
- Skip delay on hover when moving between tooltips
|
|
48
|
+
|
|
49
|
+
## Do
|
|
50
|
+
- Use Radix Tooltip for accessibility
|
|
51
|
+
- Include enter/exit animations
|
|
52
|
+
- Support arrow pointing to trigger
|
|
53
|
+
- Use inverted colors (dark bg, light text) for contrast
|
|
54
|
+
|
|
55
|
+
## Don't
|
|
56
|
+
- Show tooltip on disabled elements without wrapper
|
|
57
|
+
- Use too long delay (frustrating UX)
|
|
58
|
+
- Put interactive content in tooltips (use popover instead)
|
|
59
|
+
- Hardcode colors
|
|
60
|
+
|
|
61
|
+
## Example
|
|
62
|
+
```tsx
|
|
63
|
+
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
|
64
|
+
import { cn } from '@/lib/utils'
|
|
65
|
+
|
|
66
|
+
const TooltipProvider = TooltipPrimitive.Provider
|
|
67
|
+
|
|
68
|
+
const Tooltip = TooltipPrimitive.Root
|
|
69
|
+
|
|
70
|
+
const TooltipTrigger = TooltipPrimitive.Trigger
|
|
71
|
+
|
|
72
|
+
const TooltipContent = ({ className, sideOffset = 4, ...props }) => (
|
|
73
|
+
<TooltipPrimitive.Content
|
|
74
|
+
sideOffset={sideOffset}
|
|
75
|
+
className={cn(
|
|
76
|
+
'z-50 overflow-hidden rounded-md bg-foreground px-3 py-1.5 text-xs text-background',
|
|
77
|
+
'animate-in fade-in-0 zoom-in-95',
|
|
78
|
+
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
|
79
|
+
'data-[side=bottom]:slide-in-from-top-2',
|
|
80
|
+
'data-[side=left]:slide-in-from-right-2',
|
|
81
|
+
'data-[side=right]:slide-in-from-left-2',
|
|
82
|
+
'data-[side=top]:slide-in-from-bottom-2',
|
|
83
|
+
className
|
|
84
|
+
)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
const TooltipArrow = ({ className, ...props }) => (
|
|
90
|
+
<TooltipPrimitive.Arrow
|
|
91
|
+
className={cn('fill-foreground', className)}
|
|
92
|
+
{...props}
|
|
93
|
+
/>
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
// Simplified wrapper component
|
|
97
|
+
const SimpleTooltip = ({ content, side = 'top', delayDuration = 200, children }) => (
|
|
98
|
+
<TooltipProvider>
|
|
99
|
+
<Tooltip delayDuration={delayDuration}>
|
|
100
|
+
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
|
101
|
+
<TooltipContent side={side}>
|
|
102
|
+
{content}
|
|
103
|
+
<TooltipArrow />
|
|
104
|
+
</TooltipContent>
|
|
105
|
+
</Tooltip>
|
|
106
|
+
</TooltipProvider>
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
// Usage
|
|
110
|
+
<SimpleTooltip content="Add to library">
|
|
111
|
+
<Button variant="ghost" size="icon">
|
|
112
|
+
<Plus className="h-4 w-4" />
|
|
113
|
+
</Button>
|
|
114
|
+
</SimpleTooltip>
|
|
115
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"styling": [
|
|
3
|
+
"style",
|
|
4
|
+
"styling",
|
|
5
|
+
"css",
|
|
6
|
+
"tailwind",
|
|
7
|
+
"theme",
|
|
8
|
+
"color",
|
|
9
|
+
"colours",
|
|
10
|
+
"spacing",
|
|
11
|
+
"design system",
|
|
12
|
+
"design tokens",
|
|
13
|
+
"dark mode",
|
|
14
|
+
"light mode",
|
|
15
|
+
"ui component",
|
|
16
|
+
"component library",
|
|
17
|
+
"className",
|
|
18
|
+
"aesthetic",
|
|
19
|
+
"visual",
|
|
20
|
+
"look and feel"
|
|
21
|
+
],
|
|
22
|
+
"_comment": "Styling trigger extension for @claude-tools/component-system"
|
|
23
|
+
}
|