@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,171 @@
1
+ import React from 'react';
2
+ import { defineSegment } from '@fragments/core';
3
+ import { Textarea } from './index.js';
4
+
5
+ export default defineSegment({
6
+ component: Textarea,
7
+
8
+ meta: {
9
+ name: 'Textarea',
10
+ description: 'Multi-line text input for longer form content',
11
+ category: 'forms',
12
+ status: 'stable',
13
+ tags: ['input', 'text', 'form', 'multiline'],
14
+ },
15
+
16
+ usage: {
17
+ when: [
18
+ 'Collecting multi-line text (comments, descriptions)',
19
+ 'Free-form text input that may span multiple lines',
20
+ 'Message composition fields',
21
+ 'Code or content editing',
22
+ ],
23
+ whenNot: [
24
+ 'Single-line input (use Input)',
25
+ 'Rich text editing (use rich text editor)',
26
+ 'Selecting from predefined options (use Select)',
27
+ ],
28
+ guidelines: [
29
+ 'Set appropriate rows for expected content length',
30
+ 'Use placeholder to show example format',
31
+ 'Show character count when maxLength is set',
32
+ 'Consider auto-resize for better UX',
33
+ ],
34
+ accessibility: [
35
+ 'Always provide a visible label',
36
+ 'Use helperText for format hints',
37
+ 'Error messages should be descriptive',
38
+ ],
39
+ },
40
+
41
+ props: {
42
+ value: {
43
+ type: 'string',
44
+ description: 'Controlled value',
45
+ },
46
+ placeholder: {
47
+ type: 'string',
48
+ description: 'Placeholder text',
49
+ },
50
+ rows: {
51
+ type: 'number',
52
+ default: 3,
53
+ description: 'Number of visible text rows',
54
+ },
55
+ label: {
56
+ type: 'string',
57
+ description: 'Label text above the textarea',
58
+ },
59
+ helperText: {
60
+ type: 'string',
61
+ description: 'Helper text below the textarea',
62
+ },
63
+ error: {
64
+ type: 'boolean',
65
+ default: false,
66
+ description: 'Error state',
67
+ },
68
+ disabled: {
69
+ type: 'boolean',
70
+ default: false,
71
+ description: 'Disabled state',
72
+ },
73
+ resize: {
74
+ type: 'enum',
75
+ values: ['none', 'vertical', 'horizontal', 'both'],
76
+ default: 'vertical',
77
+ description: 'Resize behavior',
78
+ },
79
+ maxLength: {
80
+ type: 'number',
81
+ description: 'Maximum character length',
82
+ },
83
+ },
84
+
85
+ relations: [
86
+ {
87
+ component: 'Input',
88
+ relationship: 'alternative',
89
+ note: 'Use Input for single-line text',
90
+ },
91
+ ],
92
+
93
+ contract: {
94
+ propsSummary: [
95
+ 'value: string - controlled value',
96
+ 'rows: number - visible rows (default: 3)',
97
+ 'label: string - label text',
98
+ 'error: boolean - error state',
99
+ 'disabled: boolean - disabled state',
100
+ 'resize: none|vertical|horizontal|both',
101
+ ],
102
+ scenarioTags: [
103
+ 'form.textarea',
104
+ 'form.comment',
105
+ 'form.description',
106
+ ],
107
+ a11yRules: [
108
+ 'A11Y_LABEL_REQUIRED',
109
+ ],
110
+ bans: [],
111
+ },
112
+
113
+ variants: [
114
+ {
115
+ name: 'Default',
116
+ description: 'Basic textarea with label',
117
+ render: () => (
118
+ <Textarea
119
+ label="Description"
120
+ placeholder="Enter a description..."
121
+ />
122
+ ),
123
+ },
124
+ {
125
+ name: 'With Helper Text',
126
+ description: 'Textarea with additional guidance',
127
+ render: () => (
128
+ <Textarea
129
+ label="Bio"
130
+ placeholder="Tell us about yourself..."
131
+ helperText="Max 500 characters"
132
+ maxLength={500}
133
+ />
134
+ ),
135
+ },
136
+ {
137
+ name: 'Error State',
138
+ description: 'Textarea showing validation error',
139
+ render: () => (
140
+ <Textarea
141
+ label="Comments"
142
+ placeholder="Add your comments..."
143
+ error
144
+ helperText="This field is required"
145
+ />
146
+ ),
147
+ },
148
+ {
149
+ name: 'Disabled',
150
+ description: 'Non-interactive textarea',
151
+ render: () => (
152
+ <Textarea
153
+ label="Notes"
154
+ placeholder="Cannot edit..."
155
+ disabled
156
+ />
157
+ ),
158
+ },
159
+ {
160
+ name: 'Custom Rows',
161
+ description: 'Textarea with more visible rows',
162
+ render: () => (
163
+ <Textarea
164
+ label="Long Description"
165
+ placeholder="Enter detailed information..."
166
+ rows={6}
167
+ />
168
+ ),
169
+ },
170
+ ],
171
+ });
@@ -0,0 +1,89 @@
1
+ @use '../../tokens/variables' as *;
2
+ @use '../../tokens/mixins' as *;
3
+
4
+ .wrapper {
5
+ display: flex;
6
+ flex-direction: column;
7
+ }
8
+
9
+ .label {
10
+ @include label-text;
11
+ margin-bottom: var(--fui-space-1, $fui-space-1);
12
+ }
13
+
14
+ .required {
15
+ color: var(--fui-color-danger, $fui-color-danger);
16
+ margin-left: 2px;
17
+ }
18
+
19
+ .textarea {
20
+ @include text-base;
21
+ @include interactive-base;
22
+
23
+ display: block;
24
+ width: 100%;
25
+ min-height: var(--fui-input-height, $fui-input-height);
26
+ padding: var(--fui-space-2, $fui-space-2) var(--fui-space-3, $fui-space-3);
27
+ background-color: var(--fui-bg-elevated, $fui-bg-elevated);
28
+ border: 1px solid var(--fui-border-strong, $fui-border-strong);
29
+ border-radius: var(--fui-radius-md, $fui-radius-md);
30
+ line-height: $fui-line-height-normal;
31
+ font-family: inherit;
32
+
33
+ &::placeholder {
34
+ color: var(--fui-text-tertiary, $fui-text-tertiary);
35
+ }
36
+
37
+ &:hover:not(:disabled):not(:focus) {
38
+ border-color: var(--fui-text-tertiary, $fui-text-tertiary);
39
+ }
40
+
41
+ &:focus {
42
+ @include focus-ring;
43
+ border-color: var(--fui-color-accent, $fui-color-accent);
44
+ }
45
+
46
+ &:disabled,
47
+ &[data-disabled] {
48
+ background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
49
+ color: var(--fui-text-tertiary, $fui-text-tertiary);
50
+ }
51
+ }
52
+
53
+ // Resize variants
54
+ .resize-none {
55
+ resize: none;
56
+ }
57
+
58
+ .resize-vertical {
59
+ resize: vertical;
60
+ }
61
+
62
+ .resize-horizontal {
63
+ resize: horizontal;
64
+ }
65
+
66
+ .resize-both {
67
+ resize: both;
68
+ }
69
+
70
+ // Error state
71
+ .error {
72
+ border-color: var(--fui-color-danger, $fui-color-danger);
73
+
74
+ &:focus {
75
+ border-color: var(--fui-color-danger, $fui-color-danger);
76
+ box-shadow:
77
+ 0 0 0 var(--fui-focus-ring-offset, $fui-focus-ring-offset) var(--fui-bg-primary, $fui-bg-primary),
78
+ 0 0 0 calc(var(--fui-focus-ring-offset, $fui-focus-ring-offset) + var(--fui-focus-ring-width, $fui-focus-ring-width)) var(--fui-color-danger, $fui-color-danger);
79
+ }
80
+ }
81
+
82
+ .helper {
83
+ @include helper-text;
84
+ margin-top: var(--fui-space-1, $fui-space-1);
85
+ }
86
+
87
+ .helperError {
88
+ color: var(--fui-color-danger, $fui-color-danger);
89
+ }
@@ -0,0 +1,128 @@
1
+ import * as React from 'react';
2
+ import styles from './Textarea.module.scss';
3
+ // Import globals to ensure CSS variables are defined
4
+ import '../../styles/globals.scss';
5
+
6
+ export interface TextareaProps {
7
+ /** Controlled value */
8
+ value?: string;
9
+ /** Default value for uncontrolled usage */
10
+ defaultValue?: string;
11
+ /** Placeholder text */
12
+ placeholder?: string;
13
+ /** Number of visible text rows */
14
+ rows?: number;
15
+ /** Minimum number of rows (for auto-resize) */
16
+ minRows?: number;
17
+ /** Maximum number of rows (for auto-resize) */
18
+ maxRows?: number;
19
+ /** Allow user to resize the textarea */
20
+ resize?: 'none' | 'vertical' | 'horizontal' | 'both';
21
+ /** Disabled state */
22
+ disabled?: boolean;
23
+ /** Error state */
24
+ error?: boolean;
25
+ /** Label text above the textarea */
26
+ label?: string;
27
+ /** Helper text below the textarea */
28
+ helperText?: string;
29
+ /** Called when value changes */
30
+ onChange?: (value: string) => void;
31
+ /** Called when textarea loses focus */
32
+ onBlur?: () => void;
33
+ /** Additional class name */
34
+ className?: string;
35
+ /** Form field name */
36
+ name?: string;
37
+ /** Element ID */
38
+ id?: string;
39
+ /** Maximum character length */
40
+ maxLength?: number;
41
+ /** Required field */
42
+ required?: boolean;
43
+ }
44
+
45
+ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
46
+ function Textarea(
47
+ {
48
+ value,
49
+ defaultValue,
50
+ placeholder,
51
+ rows = 3,
52
+ minRows,
53
+ maxRows,
54
+ resize = 'vertical',
55
+ disabled = false,
56
+ error = false,
57
+ label,
58
+ helperText,
59
+ onChange,
60
+ onBlur,
61
+ className,
62
+ name,
63
+ id,
64
+ maxLength,
65
+ required = false,
66
+ },
67
+ ref
68
+ ) {
69
+ const textareaId = id || React.useId();
70
+ const helperId = `${textareaId}-helper`;
71
+
72
+ const textareaClasses = [
73
+ styles.textarea,
74
+ error && styles.error,
75
+ styles[`resize-${resize}`],
76
+ className,
77
+ ]
78
+ .filter(Boolean)
79
+ .join(' ');
80
+
81
+ const helperClasses = [styles.helper, error && styles.helperError]
82
+ .filter(Boolean)
83
+ .join(' ');
84
+
85
+ // Calculate min/max height based on rows
86
+ const style: React.CSSProperties = {};
87
+ if (minRows) {
88
+ style.minHeight = `calc(${minRows} * 1.5em + 1.5rem)`;
89
+ }
90
+ if (maxRows) {
91
+ style.maxHeight = `calc(${maxRows} * 1.5em + 1.5rem)`;
92
+ }
93
+
94
+ return (
95
+ <div className={styles.wrapper}>
96
+ {label && (
97
+ <label htmlFor={textareaId} className={styles.label}>
98
+ {label}
99
+ {required && <span className={styles.required}>*</span>}
100
+ </label>
101
+ )}
102
+ <textarea
103
+ ref={ref}
104
+ id={textareaId}
105
+ value={value}
106
+ defaultValue={defaultValue}
107
+ placeholder={placeholder}
108
+ rows={rows}
109
+ name={name}
110
+ maxLength={maxLength}
111
+ disabled={disabled}
112
+ required={required}
113
+ aria-invalid={error || undefined}
114
+ aria-describedby={helperText ? helperId : undefined}
115
+ onChange={(e) => onChange?.(e.target.value)}
116
+ onBlur={onBlur}
117
+ className={textareaClasses}
118
+ style={Object.keys(style).length > 0 ? style : undefined}
119
+ />
120
+ {helperText && (
121
+ <span id={helperId} className={helperClasses}>
122
+ {helperText}
123
+ </span>
124
+ )}
125
+ </div>
126
+ );
127
+ }
128
+ );
@@ -0,0 +1,210 @@
1
+ import React from 'react';
2
+ import { defineSegment } from '@fragments/core';
3
+ import { Toast, ToastProvider, useToast } from './index.js';
4
+
5
+ // Demo component that triggers toasts
6
+ function ToastDemo() {
7
+ const { success, error, warning, info, toast } = useToast();
8
+
9
+ return (
10
+ <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
11
+ <button
12
+ onClick={() => success('Success!', 'Your changes have been saved.')}
13
+ style={{ padding: '8px 16px', borderRadius: '6px', border: '1px solid #10b981', background: '#ecfdf5', color: '#065f46', cursor: 'pointer' }}
14
+ >
15
+ Success Toast
16
+ </button>
17
+ <button
18
+ onClick={() => error('Error', 'Something went wrong. Please try again.')}
19
+ style={{ padding: '8px 16px', borderRadius: '6px', border: '1px solid #ef4444', background: '#fef2f2', color: '#991b1b', cursor: 'pointer' }}
20
+ >
21
+ Error Toast
22
+ </button>
23
+ <button
24
+ onClick={() => warning('Warning', 'This action cannot be undone.')}
25
+ style={{ padding: '8px 16px', borderRadius: '6px', border: '1px solid #f59e0b', background: '#fffbeb', color: '#92400e', cursor: 'pointer' }}
26
+ >
27
+ Warning Toast
28
+ </button>
29
+ <button
30
+ onClick={() => info('Info', 'New features are available.')}
31
+ style={{ padding: '8px 16px', borderRadius: '6px', border: '1px solid #3b82f6', background: '#eff6ff', color: '#1e40af', cursor: 'pointer' }}
32
+ >
33
+ Info Toast
34
+ </button>
35
+ </div>
36
+ );
37
+ }
38
+
39
+ // Wrapped demo with provider
40
+ function ToastDemoWrapper() {
41
+ return (
42
+ <ToastProvider position="bottom-right" duration={5000}>
43
+ <ToastDemo />
44
+ </ToastProvider>
45
+ );
46
+ }
47
+
48
+ export default defineSegment({
49
+ component: Toast,
50
+
51
+ meta: {
52
+ name: 'Toast',
53
+ description: 'Brief, non-blocking notification messages',
54
+ category: 'feedback',
55
+ status: 'stable',
56
+ tags: ['notification', 'alert', 'message', 'feedback'],
57
+ },
58
+
59
+ usage: {
60
+ when: [
61
+ 'Providing feedback after an action',
62
+ 'Showing success/error status of operations',
63
+ 'Non-critical information that doesn\'t require action',
64
+ 'Temporary messages that auto-dismiss',
65
+ ],
66
+ whenNot: [
67
+ 'Critical errors requiring user action (use Dialog)',
68
+ 'Persistent information (use Alert)',
69
+ 'Inline validation (use form error states)',
70
+ 'System-wide announcements (use Banner)',
71
+ ],
72
+ guidelines: [
73
+ 'Keep messages brief and actionable',
74
+ 'Use appropriate variant for the message type',
75
+ 'Auto-dismiss after reasonable duration (3-5s)',
76
+ 'Allow manual dismissal for longer messages',
77
+ 'Limit number of simultaneous toasts',
78
+ ],
79
+ accessibility: [
80
+ 'Use role="alert" for important messages',
81
+ 'Ensure sufficient display time for reading',
82
+ 'Don\'t rely solely on color for meaning',
83
+ 'Provide dismiss button with accessible label',
84
+ ],
85
+ },
86
+
87
+ props: {
88
+ title: {
89
+ type: 'string',
90
+ description: 'Toast title',
91
+ },
92
+ description: {
93
+ type: 'string',
94
+ description: 'Additional message content',
95
+ },
96
+ variant: {
97
+ type: 'enum',
98
+ values: ['default', 'success', 'error', 'warning', 'info'],
99
+ default: 'default',
100
+ description: 'Visual variant indicating message type',
101
+ },
102
+ duration: {
103
+ type: 'number',
104
+ default: 5000,
105
+ description: 'Auto-dismiss duration in ms (0 = no auto-dismiss)',
106
+ },
107
+ action: {
108
+ type: 'object',
109
+ description: 'Optional action button { label, onClick }',
110
+ },
111
+ },
112
+
113
+ relations: [
114
+ {
115
+ component: 'Alert',
116
+ relationship: 'alternative',
117
+ note: 'Use Alert for persistent inline messages',
118
+ },
119
+ {
120
+ component: 'Dialog',
121
+ relationship: 'alternative',
122
+ note: 'Use Dialog for messages requiring user action',
123
+ },
124
+ ],
125
+
126
+ contract: {
127
+ propsSummary: [
128
+ 'title: string - toast title',
129
+ 'description: string - additional message',
130
+ 'variant: default|success|error|warning|info',
131
+ 'duration: number - ms before auto-dismiss',
132
+ 'action: { label, onClick } - optional action',
133
+ ],
134
+ scenarioTags: [
135
+ 'feedback.success',
136
+ 'feedback.error',
137
+ 'feedback.notification',
138
+ ],
139
+ a11yRules: [
140
+ 'A11Y_ALERT_ROLE',
141
+ ],
142
+ bans: [],
143
+ },
144
+
145
+ variants: [
146
+ {
147
+ name: 'Default',
148
+ description: 'Interactive toast demo - click buttons to trigger toasts',
149
+ render: () => <ToastDemoWrapper />,
150
+ },
151
+ {
152
+ name: 'Success',
153
+ description: 'Success message variant',
154
+ render: () => (
155
+ <Toast
156
+ title="Success!"
157
+ description="Your changes have been saved."
158
+ variant="success"
159
+ />
160
+ ),
161
+ },
162
+ {
163
+ name: 'Error',
164
+ description: 'Error message variant',
165
+ render: () => (
166
+ <Toast
167
+ title="Error"
168
+ description="Failed to save changes. Please try again."
169
+ variant="error"
170
+ />
171
+ ),
172
+ },
173
+ {
174
+ name: 'Warning',
175
+ description: 'Warning message variant',
176
+ render: () => (
177
+ <Toast
178
+ title="Warning"
179
+ description="This action cannot be undone."
180
+ variant="warning"
181
+ />
182
+ ),
183
+ },
184
+ {
185
+ name: 'Info',
186
+ description: 'Informational message variant',
187
+ render: () => (
188
+ <Toast
189
+ title="New Update"
190
+ description="Version 2.0 is now available."
191
+ variant="info"
192
+ />
193
+ ),
194
+ },
195
+ {
196
+ name: 'With Action',
197
+ description: 'Toast with an action button',
198
+ render: () => (
199
+ <Toast
200
+ title="File deleted"
201
+ description="The file has been moved to trash."
202
+ action={{
203
+ label: 'Undo',
204
+ onClick: () => console.log('Undo clicked'),
205
+ }}
206
+ />
207
+ ),
208
+ },
209
+ ],
210
+ });