@fragments-sdk/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/package.json +44 -0
- package/src/brand.ts +15 -0
- package/src/components/Alert/Alert.fragment.tsx +163 -0
- package/src/components/Alert/Alert.module.scss +116 -0
- package/src/components/Alert/index.tsx +95 -0
- package/src/components/Avatar/Avatar.fragment.tsx +147 -0
- package/src/components/Avatar/Avatar.module.scss +136 -0
- package/src/components/Avatar/index.tsx +177 -0
- package/src/components/Badge/Badge.fragment.tsx +151 -0
- package/src/components/Badge/Badge.module.scss +87 -0
- package/src/components/Badge/index.tsx +55 -0
- package/src/components/Button/Button.fragment.tsx +159 -0
- package/src/components/Button/Button.module.scss +97 -0
- package/src/components/Button/index.tsx +51 -0
- package/src/components/Card/Card.fragment.tsx +156 -0
- package/src/components/Card/Card.module.scss +86 -0
- package/src/components/Card/index.tsx +79 -0
- package/src/components/Checkbox/Checkbox.fragment.tsx +166 -0
- package/src/components/Checkbox/Checkbox.module.scss +144 -0
- package/src/components/Checkbox/index.tsx +166 -0
- package/src/components/Dialog/Dialog.fragment.tsx +179 -0
- package/src/components/Dialog/Dialog.module.scss +158 -0
- package/src/components/Dialog/index.tsx +230 -0
- package/src/components/EmptyState/EmptyState.fragment.tsx +222 -0
- package/src/components/EmptyState/EmptyState.module.scss +120 -0
- package/src/components/EmptyState/index.tsx +80 -0
- package/src/components/Input/Input.fragment.tsx +174 -0
- package/src/components/Input/Input.module.scss +64 -0
- package/src/components/Input/index.tsx +76 -0
- package/src/components/Menu/Menu.fragment.tsx +168 -0
- package/src/components/Menu/Menu.module.scss +190 -0
- package/src/components/Menu/index.tsx +318 -0
- package/src/components/Popover/Popover.fragment.tsx +178 -0
- package/src/components/Popover/Popover.module.scss +165 -0
- package/src/components/Popover/index.tsx +229 -0
- package/src/components/Progress/Progress.fragment.tsx +142 -0
- package/src/components/Progress/Progress.module.scss +185 -0
- package/src/components/Progress/index.tsx +196 -0
- package/src/components/RadioGroup/RadioGroup.fragment.tsx +188 -0
- package/src/components/RadioGroup/RadioGroup.module.scss +155 -0
- package/src/components/RadioGroup/index.tsx +166 -0
- package/src/components/Select/Select.fragment.tsx +173 -0
- package/src/components/Select/Select.module.scss +187 -0
- package/src/components/Select/index.tsx +233 -0
- package/src/components/Separator/Separator.fragment.tsx +148 -0
- package/src/components/Separator/Separator.module.scss +92 -0
- package/src/components/Separator/index.tsx +89 -0
- package/src/components/Skeleton/Skeleton.fragment.tsx +147 -0
- package/src/components/Skeleton/Skeleton.module.scss +166 -0
- package/src/components/Skeleton/index.tsx +185 -0
- package/src/components/Table/Table.fragment.tsx +193 -0
- package/src/components/Table/Table.module.scss +152 -0
- package/src/components/Table/index.tsx +266 -0
- package/src/components/Tabs/Tabs.fragment.tsx +155 -0
- package/src/components/Tabs/Tabs.module.scss +142 -0
- package/src/components/Tabs/index.tsx +142 -0
- package/src/components/Textarea/Textarea.fragment.tsx +171 -0
- package/src/components/Textarea/Textarea.module.scss +89 -0
- package/src/components/Textarea/index.tsx +128 -0
- package/src/components/Toast/Toast.fragment.tsx +210 -0
- package/src/components/Toast/Toast.module.scss +227 -0
- package/src/components/Toast/index.tsx +315 -0
- package/src/components/Toggle/Toggle.fragment.tsx +174 -0
- package/src/components/Toggle/Toggle.module.scss +103 -0
- package/src/components/Toggle/index.tsx +80 -0
- package/src/components/Tooltip/Tooltip.fragment.tsx +158 -0
- package/src/components/Tooltip/Tooltip.module.scss +82 -0
- package/src/components/Tooltip/index.tsx +135 -0
- package/src/index.ts +151 -0
- package/src/scss.d.ts +4 -0
- package/src/styles/globals.scss +17 -0
- package/src/tokens/_mixins.scss +93 -0
- package/src/tokens/_variables.scss +276 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
|
|
3
|
+
// ============================================
|
|
4
|
+
// Toast Container
|
|
5
|
+
// ============================================
|
|
6
|
+
|
|
7
|
+
.container {
|
|
8
|
+
position: fixed;
|
|
9
|
+
z-index: 9999;
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
gap: var(--fui-space-2, $fui-space-2);
|
|
13
|
+
max-width: 420px;
|
|
14
|
+
width: calc(100% - var(--fui-space-4, $fui-space-4) * 2);
|
|
15
|
+
pointer-events: none;
|
|
16
|
+
|
|
17
|
+
> * {
|
|
18
|
+
pointer-events: auto;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Position variants
|
|
23
|
+
.topleft {
|
|
24
|
+
top: var(--fui-space-4, $fui-space-4);
|
|
25
|
+
left: var(--fui-space-4, $fui-space-4);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.topcenter {
|
|
29
|
+
top: var(--fui-space-4, $fui-space-4);
|
|
30
|
+
left: 50%;
|
|
31
|
+
transform: translateX(-50%);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.topright {
|
|
35
|
+
top: var(--fui-space-4, $fui-space-4);
|
|
36
|
+
right: var(--fui-space-4, $fui-space-4);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.bottomleft {
|
|
40
|
+
bottom: var(--fui-space-4, $fui-space-4);
|
|
41
|
+
left: var(--fui-space-4, $fui-space-4);
|
|
42
|
+
flex-direction: column-reverse;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.bottomcenter {
|
|
46
|
+
bottom: var(--fui-space-4, $fui-space-4);
|
|
47
|
+
left: 50%;
|
|
48
|
+
transform: translateX(-50%);
|
|
49
|
+
flex-direction: column-reverse;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.bottomright {
|
|
53
|
+
bottom: var(--fui-space-4, $fui-space-4);
|
|
54
|
+
right: var(--fui-space-4, $fui-space-4);
|
|
55
|
+
flex-direction: column-reverse;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============================================
|
|
59
|
+
// Toast Item
|
|
60
|
+
// ============================================
|
|
61
|
+
|
|
62
|
+
.toast {
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: flex-start;
|
|
65
|
+
gap: var(--fui-space-3, $fui-space-3);
|
|
66
|
+
padding: var(--fui-space-3, $fui-space-3) var(--fui-space-4, $fui-space-4);
|
|
67
|
+
background-color: var(--fui-bg-elevated, $fui-bg-elevated);
|
|
68
|
+
border: 1px solid var(--fui-border, $fui-border);
|
|
69
|
+
border-radius: var(--fui-radius-lg, $fui-radius-lg);
|
|
70
|
+
box-shadow: var(--fui-shadow-md, $fui-shadow-md);
|
|
71
|
+
font-family: var(--fui-font-sans, $fui-font-sans);
|
|
72
|
+
|
|
73
|
+
// Animation
|
|
74
|
+
animation: toastEnter 0.2s ease-out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@keyframes toastEnter {
|
|
78
|
+
from {
|
|
79
|
+
opacity: 0;
|
|
80
|
+
transform: translateY(8px) scale(0.96);
|
|
81
|
+
}
|
|
82
|
+
to {
|
|
83
|
+
opacity: 1;
|
|
84
|
+
transform: translateY(0) scale(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ============================================
|
|
89
|
+
// Variant Styles
|
|
90
|
+
// ============================================
|
|
91
|
+
|
|
92
|
+
.default {
|
|
93
|
+
.icon {
|
|
94
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.success {
|
|
99
|
+
border-left: 3px solid var(--fui-color-success, $fui-color-success);
|
|
100
|
+
|
|
101
|
+
.icon {
|
|
102
|
+
color: var(--fui-color-success, $fui-color-success);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.error {
|
|
107
|
+
border-left: 3px solid var(--fui-color-danger, $fui-color-danger);
|
|
108
|
+
|
|
109
|
+
.icon {
|
|
110
|
+
color: var(--fui-color-danger, $fui-color-danger);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.warning {
|
|
115
|
+
border-left: 3px solid var(--fui-color-warning, $fui-color-warning);
|
|
116
|
+
|
|
117
|
+
.icon {
|
|
118
|
+
color: var(--fui-color-warning, $fui-color-warning);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.info {
|
|
123
|
+
border-left: 3px solid var(--fui-color-info, $fui-color-info);
|
|
124
|
+
|
|
125
|
+
.icon {
|
|
126
|
+
color: var(--fui-color-info, $fui-color-info);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================
|
|
131
|
+
// Toast Parts
|
|
132
|
+
// ============================================
|
|
133
|
+
|
|
134
|
+
.icon {
|
|
135
|
+
flex-shrink: 0;
|
|
136
|
+
width: 1.25rem;
|
|
137
|
+
height: 1.25rem;
|
|
138
|
+
margin-top: 1px;
|
|
139
|
+
|
|
140
|
+
svg {
|
|
141
|
+
width: 100%;
|
|
142
|
+
height: 100%;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.content {
|
|
147
|
+
flex: 1;
|
|
148
|
+
min-width: 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.title {
|
|
152
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
153
|
+
font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
|
|
154
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
155
|
+
line-height: var(--fui-line-height-tight, $fui-line-height-tight);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.description {
|
|
159
|
+
margin-top: 2px;
|
|
160
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
161
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
162
|
+
line-height: var(--fui-line-height-normal, $fui-line-height-normal);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============================================
|
|
166
|
+
// Action Button
|
|
167
|
+
// ============================================
|
|
168
|
+
|
|
169
|
+
.action {
|
|
170
|
+
flex-shrink: 0;
|
|
171
|
+
padding: var(--fui-space-1, $fui-space-1) var(--fui-space-2, $fui-space-2);
|
|
172
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
173
|
+
font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
|
|
174
|
+
color: var(--fui-color-accent, $fui-color-accent);
|
|
175
|
+
background: transparent;
|
|
176
|
+
border: none;
|
|
177
|
+
border-radius: var(--fui-radius-sm, $fui-radius-sm);
|
|
178
|
+
cursor: pointer;
|
|
179
|
+
transition: background-color var(--fui-transition-fast, $fui-transition-fast);
|
|
180
|
+
|
|
181
|
+
&:hover {
|
|
182
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
&:focus-visible {
|
|
186
|
+
outline: var(--fui-focus-ring-width, $fui-focus-ring-width) solid var(--fui-focus-ring-color, $fui-focus-ring-color);
|
|
187
|
+
outline-offset: var(--fui-focus-ring-offset, $fui-focus-ring-offset);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ============================================
|
|
192
|
+
// Close Button
|
|
193
|
+
// ============================================
|
|
194
|
+
|
|
195
|
+
.close {
|
|
196
|
+
flex-shrink: 0;
|
|
197
|
+
display: flex;
|
|
198
|
+
align-items: center;
|
|
199
|
+
justify-content: center;
|
|
200
|
+
width: 1.5rem;
|
|
201
|
+
height: 1.5rem;
|
|
202
|
+
margin: -2px -4px -2px 0;
|
|
203
|
+
padding: 0;
|
|
204
|
+
background: transparent;
|
|
205
|
+
border: none;
|
|
206
|
+
border-radius: var(--fui-radius-sm, $fui-radius-sm);
|
|
207
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
208
|
+
cursor: pointer;
|
|
209
|
+
transition:
|
|
210
|
+
background-color var(--fui-transition-fast, $fui-transition-fast),
|
|
211
|
+
color var(--fui-transition-fast, $fui-transition-fast);
|
|
212
|
+
|
|
213
|
+
svg {
|
|
214
|
+
width: 1rem;
|
|
215
|
+
height: 1rem;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
&:hover {
|
|
219
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
220
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
&:focus-visible {
|
|
224
|
+
outline: var(--fui-focus-ring-width, $fui-focus-ring-width) solid var(--fui-focus-ring-color, $fui-focus-ring-color);
|
|
225
|
+
outline-offset: var(--fui-focus-ring-offset, $fui-focus-ring-offset);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import styles from './Toast.module.scss';
|
|
3
|
+
// Import globals to ensure CSS variables are defined
|
|
4
|
+
import '../../styles/globals.scss';
|
|
5
|
+
|
|
6
|
+
// ============================================
|
|
7
|
+
// Types
|
|
8
|
+
// ============================================
|
|
9
|
+
|
|
10
|
+
export type ToastVariant = 'default' | 'success' | 'error' | 'warning' | 'info';
|
|
11
|
+
export type ToastPosition =
|
|
12
|
+
| 'top-left'
|
|
13
|
+
| 'top-center'
|
|
14
|
+
| 'top-right'
|
|
15
|
+
| 'bottom-left'
|
|
16
|
+
| 'bottom-center'
|
|
17
|
+
| 'bottom-right';
|
|
18
|
+
|
|
19
|
+
export interface ToastData {
|
|
20
|
+
id: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
variant?: ToastVariant;
|
|
24
|
+
duration?: number;
|
|
25
|
+
action?: {
|
|
26
|
+
label: string;
|
|
27
|
+
onClick: () => void;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ToastProps extends Omit<ToastData, 'id'> {
|
|
32
|
+
/** Callback when toast should be dismissed */
|
|
33
|
+
onDismiss?: () => void;
|
|
34
|
+
/** Additional class name */
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ToastProviderProps {
|
|
39
|
+
/** Position of the toast container */
|
|
40
|
+
position?: ToastPosition;
|
|
41
|
+
/** Default duration in ms (0 = no auto-dismiss) */
|
|
42
|
+
duration?: number;
|
|
43
|
+
/** Maximum number of toasts to show */
|
|
44
|
+
max?: number;
|
|
45
|
+
/** Children */
|
|
46
|
+
children: React.ReactNode;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================
|
|
50
|
+
// Context
|
|
51
|
+
// ============================================
|
|
52
|
+
|
|
53
|
+
interface ToastContextValue {
|
|
54
|
+
toasts: ToastData[];
|
|
55
|
+
addToast: (toast: Omit<ToastData, 'id'>) => string;
|
|
56
|
+
removeToast: (id: string) => void;
|
|
57
|
+
clearToasts: () => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const ToastContext = React.createContext<ToastContextValue | null>(null);
|
|
61
|
+
|
|
62
|
+
// ============================================
|
|
63
|
+
// Hook to use toast
|
|
64
|
+
// ============================================
|
|
65
|
+
|
|
66
|
+
export function useToast() {
|
|
67
|
+
const context = React.useContext(ToastContext);
|
|
68
|
+
if (!context) {
|
|
69
|
+
throw new Error('useToast must be used within a ToastProvider');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const toast = React.useCallback((options: Omit<ToastData, 'id'>) => {
|
|
73
|
+
return context.addToast(options);
|
|
74
|
+
}, [context]);
|
|
75
|
+
|
|
76
|
+
const success = React.useCallback((title: string, description?: string) => {
|
|
77
|
+
return context.addToast({ title, description, variant: 'success' });
|
|
78
|
+
}, [context]);
|
|
79
|
+
|
|
80
|
+
const error = React.useCallback((title: string, description?: string) => {
|
|
81
|
+
return context.addToast({ title, description, variant: 'error' });
|
|
82
|
+
}, [context]);
|
|
83
|
+
|
|
84
|
+
const warning = React.useCallback((title: string, description?: string) => {
|
|
85
|
+
return context.addToast({ title, description, variant: 'warning' });
|
|
86
|
+
}, [context]);
|
|
87
|
+
|
|
88
|
+
const info = React.useCallback((title: string, description?: string) => {
|
|
89
|
+
return context.addToast({ title, description, variant: 'info' });
|
|
90
|
+
}, [context]);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
toast,
|
|
94
|
+
success,
|
|
95
|
+
error,
|
|
96
|
+
warning,
|
|
97
|
+
info,
|
|
98
|
+
dismiss: context.removeToast,
|
|
99
|
+
dismissAll: context.clearToasts,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================
|
|
104
|
+
// Icons
|
|
105
|
+
// ============================================
|
|
106
|
+
|
|
107
|
+
function SuccessIcon() {
|
|
108
|
+
return (
|
|
109
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
110
|
+
<circle cx="12" cy="12" r="10" />
|
|
111
|
+
<path d="m9 12 2 2 4-4" />
|
|
112
|
+
</svg>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function ErrorIcon() {
|
|
117
|
+
return (
|
|
118
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
119
|
+
<circle cx="12" cy="12" r="10" />
|
|
120
|
+
<line x1="15" y1="9" x2="9" y2="15" />
|
|
121
|
+
<line x1="9" y1="9" x2="15" y2="15" />
|
|
122
|
+
</svg>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function WarningIcon() {
|
|
127
|
+
return (
|
|
128
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
129
|
+
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
|
|
130
|
+
<line x1="12" y1="9" x2="12" y2="13" />
|
|
131
|
+
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
132
|
+
</svg>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function InfoIcon() {
|
|
137
|
+
return (
|
|
138
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
139
|
+
<circle cx="12" cy="12" r="10" />
|
|
140
|
+
<line x1="12" y1="16" x2="12" y2="12" />
|
|
141
|
+
<line x1="12" y1="8" x2="12.01" y2="8" />
|
|
142
|
+
</svg>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function CloseIcon() {
|
|
147
|
+
return (
|
|
148
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
149
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
150
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
151
|
+
</svg>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const variantIcons: Record<ToastVariant, React.ComponentType | null> = {
|
|
156
|
+
default: null,
|
|
157
|
+
success: SuccessIcon,
|
|
158
|
+
error: ErrorIcon,
|
|
159
|
+
warning: WarningIcon,
|
|
160
|
+
info: InfoIcon,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// ============================================
|
|
164
|
+
// Toast Component
|
|
165
|
+
// ============================================
|
|
166
|
+
|
|
167
|
+
function ToastItem({
|
|
168
|
+
title,
|
|
169
|
+
description,
|
|
170
|
+
variant = 'default',
|
|
171
|
+
action,
|
|
172
|
+
onDismiss,
|
|
173
|
+
className,
|
|
174
|
+
}: ToastProps) {
|
|
175
|
+
const Icon = variantIcons[variant];
|
|
176
|
+
|
|
177
|
+
const toastClasses = [
|
|
178
|
+
styles.toast,
|
|
179
|
+
styles[variant],
|
|
180
|
+
className,
|
|
181
|
+
].filter(Boolean).join(' ');
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div className={toastClasses} role="alert">
|
|
185
|
+
{Icon && (
|
|
186
|
+
<span className={styles.icon}>
|
|
187
|
+
<Icon />
|
|
188
|
+
</span>
|
|
189
|
+
)}
|
|
190
|
+
<div className={styles.content}>
|
|
191
|
+
{title && <div className={styles.title}>{title}</div>}
|
|
192
|
+
{description && <div className={styles.description}>{description}</div>}
|
|
193
|
+
</div>
|
|
194
|
+
{action && (
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
className={styles.action}
|
|
198
|
+
onClick={action.onClick}
|
|
199
|
+
>
|
|
200
|
+
{action.label}
|
|
201
|
+
</button>
|
|
202
|
+
)}
|
|
203
|
+
{onDismiss && (
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
className={styles.close}
|
|
207
|
+
onClick={onDismiss}
|
|
208
|
+
aria-label="Dismiss"
|
|
209
|
+
>
|
|
210
|
+
<CloseIcon />
|
|
211
|
+
</button>
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ============================================
|
|
218
|
+
// Toast Container
|
|
219
|
+
// ============================================
|
|
220
|
+
|
|
221
|
+
function ToastContainer({
|
|
222
|
+
toasts,
|
|
223
|
+
position,
|
|
224
|
+
onDismiss,
|
|
225
|
+
}: {
|
|
226
|
+
toasts: ToastData[];
|
|
227
|
+
position: ToastPosition;
|
|
228
|
+
onDismiss: (id: string) => void;
|
|
229
|
+
}) {
|
|
230
|
+
const containerClasses = [
|
|
231
|
+
styles.container,
|
|
232
|
+
styles[position.replace('-', '')],
|
|
233
|
+
].filter(Boolean).join(' ');
|
|
234
|
+
|
|
235
|
+
if (toasts.length === 0) return null;
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div className={containerClasses}>
|
|
239
|
+
{toasts.map((toast) => (
|
|
240
|
+
<ToastItem
|
|
241
|
+
key={toast.id}
|
|
242
|
+
{...toast}
|
|
243
|
+
onDismiss={() => onDismiss(toast.id)}
|
|
244
|
+
/>
|
|
245
|
+
))}
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ============================================
|
|
251
|
+
// Toast Provider
|
|
252
|
+
// ============================================
|
|
253
|
+
|
|
254
|
+
let toastCounter = 0;
|
|
255
|
+
|
|
256
|
+
export function ToastProvider({
|
|
257
|
+
position = 'bottom-right',
|
|
258
|
+
duration = 5000,
|
|
259
|
+
max = 5,
|
|
260
|
+
children,
|
|
261
|
+
}: ToastProviderProps) {
|
|
262
|
+
const [toasts, setToasts] = React.useState<ToastData[]>([]);
|
|
263
|
+
|
|
264
|
+
const addToast = React.useCallback((toast: Omit<ToastData, 'id'>) => {
|
|
265
|
+
const id = `toast-${++toastCounter}`;
|
|
266
|
+
const toastDuration = toast.duration ?? duration;
|
|
267
|
+
|
|
268
|
+
setToasts((prev) => {
|
|
269
|
+
const newToasts = [...prev, { ...toast, id }];
|
|
270
|
+
// Limit to max toasts
|
|
271
|
+
return newToasts.slice(-max);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Auto-dismiss
|
|
275
|
+
if (toastDuration > 0) {
|
|
276
|
+
setTimeout(() => {
|
|
277
|
+
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
278
|
+
}, toastDuration);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return id;
|
|
282
|
+
}, [duration, max]);
|
|
283
|
+
|
|
284
|
+
const removeToast = React.useCallback((id: string) => {
|
|
285
|
+
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
286
|
+
}, []);
|
|
287
|
+
|
|
288
|
+
const clearToasts = React.useCallback(() => {
|
|
289
|
+
setToasts([]);
|
|
290
|
+
}, []);
|
|
291
|
+
|
|
292
|
+
const value = React.useMemo(
|
|
293
|
+
() => ({ toasts, addToast, removeToast, clearToasts }),
|
|
294
|
+
[toasts, addToast, removeToast, clearToasts]
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<ToastContext.Provider value={value}>
|
|
299
|
+
{children}
|
|
300
|
+
<ToastContainer
|
|
301
|
+
toasts={toasts}
|
|
302
|
+
position={position}
|
|
303
|
+
onDismiss={removeToast}
|
|
304
|
+
/>
|
|
305
|
+
</ToastContext.Provider>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ============================================
|
|
310
|
+
// Export Toast as compound component
|
|
311
|
+
// ============================================
|
|
312
|
+
|
|
313
|
+
export const Toast = Object.assign(ToastItem, {
|
|
314
|
+
Provider: ToastProvider,
|
|
315
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { defineSegment } from '@fragments/core';
|
|
3
|
+
import { Toggle } from './index.js';
|
|
4
|
+
|
|
5
|
+
// Stateful wrapper for interactive demos
|
|
6
|
+
function StatefulToggle(props: React.ComponentProps<typeof Toggle>) {
|
|
7
|
+
const [checked, setChecked] = useState(props.checked ?? false);
|
|
8
|
+
return <Toggle {...props} checked={checked} onChange={setChecked} />;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default defineSegment({
|
|
12
|
+
component: Toggle,
|
|
13
|
+
|
|
14
|
+
meta: {
|
|
15
|
+
name: 'Toggle',
|
|
16
|
+
description: 'Binary on/off switch for settings and preferences. Provides immediate visual feedback and is ideal for options that take effect instantly.',
|
|
17
|
+
category: 'forms',
|
|
18
|
+
status: 'stable',
|
|
19
|
+
tags: ['switch', 'toggle', 'boolean', 'settings', 'preference'],
|
|
20
|
+
since: '0.1.0',
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
usage: {
|
|
24
|
+
when: [
|
|
25
|
+
'Binary settings that take effect immediately (e.g., dark mode, notifications)',
|
|
26
|
+
'Enabling/disabling features in a settings panel',
|
|
27
|
+
'Options where the result is immediately visible',
|
|
28
|
+
'Mobile-friendly boolean inputs',
|
|
29
|
+
],
|
|
30
|
+
whenNot: [
|
|
31
|
+
'Multiple options in a group (use checkbox group)',
|
|
32
|
+
'Selection requires form submission to take effect (use checkbox)',
|
|
33
|
+
'Yes/No questions in forms (use radio buttons)',
|
|
34
|
+
'Complex multi-state options (use select or radio)',
|
|
35
|
+
],
|
|
36
|
+
guidelines: [
|
|
37
|
+
'Toggle should always have a visible label explaining what it controls',
|
|
38
|
+
'The "on" state should be the positive/enabling action',
|
|
39
|
+
'Changes should take effect immediately - no save button needed',
|
|
40
|
+
'Include a description for toggles whose effect isn\'t obvious from the label',
|
|
41
|
+
'Group related toggles visually in settings panels',
|
|
42
|
+
],
|
|
43
|
+
accessibility: [
|
|
44
|
+
'Uses role="switch" with aria-checked for proper semantics',
|
|
45
|
+
'Must have an accessible label (visible or aria-label)',
|
|
46
|
+
'Focus indicator must be clearly visible',
|
|
47
|
+
'State change must be announced by screen readers',
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
props: {
|
|
52
|
+
checked: {
|
|
53
|
+
type: 'boolean',
|
|
54
|
+
description: 'Whether the toggle is in the on state',
|
|
55
|
+
default: 'false',
|
|
56
|
+
},
|
|
57
|
+
onChange: {
|
|
58
|
+
type: 'function',
|
|
59
|
+
description: 'Callback with new checked state: (checked: boolean) => void',
|
|
60
|
+
},
|
|
61
|
+
label: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
description: 'Visible label text',
|
|
64
|
+
},
|
|
65
|
+
description: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
description: 'Helper text shown below the label',
|
|
68
|
+
},
|
|
69
|
+
disabled: {
|
|
70
|
+
type: 'boolean',
|
|
71
|
+
description: 'Whether the toggle is non-interactive',
|
|
72
|
+
default: 'false',
|
|
73
|
+
},
|
|
74
|
+
size: {
|
|
75
|
+
type: 'enum',
|
|
76
|
+
description: 'Toggle track size',
|
|
77
|
+
values: ['sm', 'md'],
|
|
78
|
+
default: 'md',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
relations: [
|
|
83
|
+
{ component: 'Input', relationship: 'sibling', note: 'Input handles text/number entry; Toggle handles boolean state' },
|
|
84
|
+
{ component: 'Checkbox', relationship: 'alternative', note: 'Use Checkbox when change requires form submission' },
|
|
85
|
+
],
|
|
86
|
+
|
|
87
|
+
contract: {
|
|
88
|
+
propsSummary: [
|
|
89
|
+
'checked: boolean - on/off state',
|
|
90
|
+
'onChange: (checked) => void - state change handler',
|
|
91
|
+
'label: string - visible label text',
|
|
92
|
+
'description: string - helper text below label',
|
|
93
|
+
'disabled: boolean - non-interactive state',
|
|
94
|
+
'size: sm|md - toggle size',
|
|
95
|
+
],
|
|
96
|
+
scenarioTags: [
|
|
97
|
+
'form.boolean',
|
|
98
|
+
'settings.toggle',
|
|
99
|
+
'settings.preference',
|
|
100
|
+
'form.switch',
|
|
101
|
+
],
|
|
102
|
+
a11yRules: ['A11Y_SWITCH_ROLE', 'A11Y_SWITCH_LABEL', 'A11Y_SWITCH_FOCUS'],
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
variants: [
|
|
106
|
+
{
|
|
107
|
+
name: 'Default Off',
|
|
108
|
+
description: 'Toggle in the off state',
|
|
109
|
+
render: () => <StatefulToggle label="Email notifications" />,
|
|
110
|
+
args: { label: 'Email notifications' },
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'Checked',
|
|
114
|
+
description: 'Toggle in the on state',
|
|
115
|
+
render: () => <StatefulToggle checked label="Dark mode" />,
|
|
116
|
+
args: { checked: true, label: 'Dark mode' },
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'With Description',
|
|
120
|
+
description: 'Toggle with explanatory helper text',
|
|
121
|
+
render: () => (
|
|
122
|
+
<StatefulToggle
|
|
123
|
+
checked
|
|
124
|
+
label="Auto-save"
|
|
125
|
+
description="Automatically save changes as you type"
|
|
126
|
+
/>
|
|
127
|
+
),
|
|
128
|
+
args: { checked: true, label: 'Auto-save', description: 'Automatically save changes as you type' },
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'Small Size',
|
|
132
|
+
description: 'Compact toggle for dense settings panels',
|
|
133
|
+
render: () => (
|
|
134
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
|
135
|
+
<StatefulToggle size="sm" checked label="Show line numbers" />
|
|
136
|
+
<StatefulToggle size="sm" label="Word wrap" />
|
|
137
|
+
<StatefulToggle size="sm" checked label="Minimap" />
|
|
138
|
+
</div>
|
|
139
|
+
),
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'Disabled States',
|
|
143
|
+
description: 'Non-interactive toggles showing both states',
|
|
144
|
+
render: () => (
|
|
145
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
|
146
|
+
<Toggle disabled label="Premium feature (upgrade required)" />
|
|
147
|
+
<Toggle disabled checked label="System managed (read-only)" />
|
|
148
|
+
</div>
|
|
149
|
+
),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'Settings Panel',
|
|
153
|
+
description: 'Multiple toggles in a realistic settings layout',
|
|
154
|
+
render: () => (
|
|
155
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px', maxWidth: '320px' }}>
|
|
156
|
+
<StatefulToggle
|
|
157
|
+
checked
|
|
158
|
+
label="Push notifications"
|
|
159
|
+
description="Receive push notifications on your device"
|
|
160
|
+
/>
|
|
161
|
+
<StatefulToggle
|
|
162
|
+
checked
|
|
163
|
+
label="Email digest"
|
|
164
|
+
description="Weekly summary of your activity"
|
|
165
|
+
/>
|
|
166
|
+
<StatefulToggle
|
|
167
|
+
label="Marketing emails"
|
|
168
|
+
description="Product updates and promotional offers"
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
),
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
});
|