@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.
Files changed (73) hide show
  1. package/package.json +44 -0
  2. package/src/brand.ts +15 -0
  3. package/src/components/Alert/Alert.fragment.tsx +163 -0
  4. package/src/components/Alert/Alert.module.scss +116 -0
  5. package/src/components/Alert/index.tsx +95 -0
  6. package/src/components/Avatar/Avatar.fragment.tsx +147 -0
  7. package/src/components/Avatar/Avatar.module.scss +136 -0
  8. package/src/components/Avatar/index.tsx +177 -0
  9. package/src/components/Badge/Badge.fragment.tsx +151 -0
  10. package/src/components/Badge/Badge.module.scss +87 -0
  11. package/src/components/Badge/index.tsx +55 -0
  12. package/src/components/Button/Button.fragment.tsx +159 -0
  13. package/src/components/Button/Button.module.scss +97 -0
  14. package/src/components/Button/index.tsx +51 -0
  15. package/src/components/Card/Card.fragment.tsx +156 -0
  16. package/src/components/Card/Card.module.scss +86 -0
  17. package/src/components/Card/index.tsx +79 -0
  18. package/src/components/Checkbox/Checkbox.fragment.tsx +166 -0
  19. package/src/components/Checkbox/Checkbox.module.scss +144 -0
  20. package/src/components/Checkbox/index.tsx +166 -0
  21. package/src/components/Dialog/Dialog.fragment.tsx +179 -0
  22. package/src/components/Dialog/Dialog.module.scss +158 -0
  23. package/src/components/Dialog/index.tsx +230 -0
  24. package/src/components/EmptyState/EmptyState.fragment.tsx +222 -0
  25. package/src/components/EmptyState/EmptyState.module.scss +120 -0
  26. package/src/components/EmptyState/index.tsx +80 -0
  27. package/src/components/Input/Input.fragment.tsx +174 -0
  28. package/src/components/Input/Input.module.scss +64 -0
  29. package/src/components/Input/index.tsx +76 -0
  30. package/src/components/Menu/Menu.fragment.tsx +168 -0
  31. package/src/components/Menu/Menu.module.scss +190 -0
  32. package/src/components/Menu/index.tsx +318 -0
  33. package/src/components/Popover/Popover.fragment.tsx +178 -0
  34. package/src/components/Popover/Popover.module.scss +165 -0
  35. package/src/components/Popover/index.tsx +229 -0
  36. package/src/components/Progress/Progress.fragment.tsx +142 -0
  37. package/src/components/Progress/Progress.module.scss +185 -0
  38. package/src/components/Progress/index.tsx +196 -0
  39. package/src/components/RadioGroup/RadioGroup.fragment.tsx +188 -0
  40. package/src/components/RadioGroup/RadioGroup.module.scss +155 -0
  41. package/src/components/RadioGroup/index.tsx +166 -0
  42. package/src/components/Select/Select.fragment.tsx +173 -0
  43. package/src/components/Select/Select.module.scss +187 -0
  44. package/src/components/Select/index.tsx +233 -0
  45. package/src/components/Separator/Separator.fragment.tsx +148 -0
  46. package/src/components/Separator/Separator.module.scss +92 -0
  47. package/src/components/Separator/index.tsx +89 -0
  48. package/src/components/Skeleton/Skeleton.fragment.tsx +147 -0
  49. package/src/components/Skeleton/Skeleton.module.scss +166 -0
  50. package/src/components/Skeleton/index.tsx +185 -0
  51. package/src/components/Table/Table.fragment.tsx +193 -0
  52. package/src/components/Table/Table.module.scss +152 -0
  53. package/src/components/Table/index.tsx +266 -0
  54. package/src/components/Tabs/Tabs.fragment.tsx +155 -0
  55. package/src/components/Tabs/Tabs.module.scss +142 -0
  56. package/src/components/Tabs/index.tsx +142 -0
  57. package/src/components/Textarea/Textarea.fragment.tsx +171 -0
  58. package/src/components/Textarea/Textarea.module.scss +89 -0
  59. package/src/components/Textarea/index.tsx +128 -0
  60. package/src/components/Toast/Toast.fragment.tsx +210 -0
  61. package/src/components/Toast/Toast.module.scss +227 -0
  62. package/src/components/Toast/index.tsx +315 -0
  63. package/src/components/Toggle/Toggle.fragment.tsx +174 -0
  64. package/src/components/Toggle/Toggle.module.scss +103 -0
  65. package/src/components/Toggle/index.tsx +80 -0
  66. package/src/components/Tooltip/Tooltip.fragment.tsx +158 -0
  67. package/src/components/Tooltip/Tooltip.module.scss +82 -0
  68. package/src/components/Tooltip/index.tsx +135 -0
  69. package/src/index.ts +151 -0
  70. package/src/scss.d.ts +4 -0
  71. package/src/styles/globals.scss +17 -0
  72. package/src/tokens/_mixins.scss +93 -0
  73. 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
+ });