@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,120 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
@use '../../tokens/mixins' as *;
|
|
3
|
+
|
|
4
|
+
.emptyState {
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
align-items: center;
|
|
8
|
+
text-align: center;
|
|
9
|
+
font-family: var(--fui-font-sans, $fui-font-sans);
|
|
10
|
+
-webkit-font-smoothing: antialiased;
|
|
11
|
+
-moz-osx-font-smoothing: grayscale;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Size variants
|
|
15
|
+
.sm {
|
|
16
|
+
padding: var(--fui-space-6, $fui-space-6) var(--fui-space-4, $fui-space-4);
|
|
17
|
+
|
|
18
|
+
.title {
|
|
19
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.description {
|
|
23
|
+
font-size: var(--fui-font-size-xs, $fui-font-size-xs);
|
|
24
|
+
max-width: 240px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.icon {
|
|
28
|
+
margin-bottom: var(--fui-space-2, $fui-space-2);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.actions {
|
|
32
|
+
margin-top: var(--fui-space-3, $fui-space-3);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.md {
|
|
37
|
+
padding: var(--fui-space-10, $fui-space-10) var(--fui-space-6, $fui-space-6);
|
|
38
|
+
|
|
39
|
+
.title {
|
|
40
|
+
font-size: var(--fui-font-size-base, $fui-font-size-base);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.description {
|
|
44
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
45
|
+
max-width: 320px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.icon {
|
|
49
|
+
margin-bottom: var(--fui-space-3, $fui-space-3);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.actions {
|
|
53
|
+
margin-top: var(--fui-space-4, $fui-space-4);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.lg {
|
|
58
|
+
padding: var(--fui-space-12, $fui-space-12) var(--fui-space-8, $fui-space-8);
|
|
59
|
+
|
|
60
|
+
.title {
|
|
61
|
+
font-size: var(--fui-font-size-lg, $fui-font-size-lg);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.description {
|
|
65
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
66
|
+
max-width: 400px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.icon {
|
|
70
|
+
margin-bottom: var(--fui-space-4, $fui-space-4);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.actions {
|
|
74
|
+
margin-top: var(--fui-space-6, $fui-space-6);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.icon {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
justify-content: center;
|
|
82
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
83
|
+
|
|
84
|
+
// Icon sizing based on parent size variant
|
|
85
|
+
svg {
|
|
86
|
+
width: 32px;
|
|
87
|
+
height: 32px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.lg & svg {
|
|
91
|
+
width: 40px;
|
|
92
|
+
height: 40px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.sm & svg {
|
|
96
|
+
width: 24px;
|
|
97
|
+
height: 24px;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.title {
|
|
102
|
+
margin: 0;
|
|
103
|
+
font-weight: var(--fui-font-weight-semibold, $fui-font-weight-semibold);
|
|
104
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
105
|
+
line-height: var(--fui-line-height-tight, $fui-line-height-tight);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.description {
|
|
109
|
+
margin: var(--fui-space-2, $fui-space-2) 0 0;
|
|
110
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
111
|
+
line-height: var(--fui-line-height-normal, $fui-line-height-normal);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.actions {
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
gap: var(--fui-space-2, $fui-space-2);
|
|
118
|
+
flex-wrap: wrap;
|
|
119
|
+
justify-content: center;
|
|
120
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Button } from '../Button';
|
|
3
|
+
import styles from './EmptyState.module.scss';
|
|
4
|
+
// Import globals to ensure CSS variables are defined
|
|
5
|
+
import '../../styles/globals.scss';
|
|
6
|
+
|
|
7
|
+
export interface EmptyStateAction {
|
|
8
|
+
/** Button label */
|
|
9
|
+
label: string;
|
|
10
|
+
/** Click handler */
|
|
11
|
+
onClick: () => void;
|
|
12
|
+
/** Button variant */
|
|
13
|
+
variant?: 'primary' | 'secondary';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface EmptyStateProps {
|
|
17
|
+
/** Main heading */
|
|
18
|
+
title: string;
|
|
19
|
+
/** Supporting description */
|
|
20
|
+
description?: string;
|
|
21
|
+
/** Optional icon element */
|
|
22
|
+
icon?: React.ReactNode;
|
|
23
|
+
/** Primary action */
|
|
24
|
+
action?: EmptyStateAction;
|
|
25
|
+
/** Secondary action */
|
|
26
|
+
secondaryAction?: EmptyStateAction;
|
|
27
|
+
/** Size variant */
|
|
28
|
+
size?: 'sm' | 'md' | 'lg';
|
|
29
|
+
/** Additional class name */
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
|
|
34
|
+
function EmptyState(
|
|
35
|
+
{
|
|
36
|
+
title,
|
|
37
|
+
description,
|
|
38
|
+
icon,
|
|
39
|
+
action,
|
|
40
|
+
secondaryAction,
|
|
41
|
+
size = 'md',
|
|
42
|
+
className,
|
|
43
|
+
},
|
|
44
|
+
ref
|
|
45
|
+
) {
|
|
46
|
+
const classes = [styles.emptyState, styles[size], className]
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.join(' ');
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div ref={ref} className={classes}>
|
|
52
|
+
{icon && <div className={styles.icon}>{icon}</div>}
|
|
53
|
+
<h3 className={styles.title}>{title}</h3>
|
|
54
|
+
{description && <p className={styles.description}>{description}</p>}
|
|
55
|
+
{(action || secondaryAction) && (
|
|
56
|
+
<div className={styles.actions}>
|
|
57
|
+
{action && (
|
|
58
|
+
<Button
|
|
59
|
+
variant={action.variant ?? 'primary'}
|
|
60
|
+
onClick={action.onClick}
|
|
61
|
+
size={size === 'sm' ? 'sm' : 'md'}
|
|
62
|
+
>
|
|
63
|
+
{action.label}
|
|
64
|
+
</Button>
|
|
65
|
+
)}
|
|
66
|
+
{secondaryAction && (
|
|
67
|
+
<Button
|
|
68
|
+
variant={secondaryAction.variant ?? 'secondary'}
|
|
69
|
+
onClick={secondaryAction.onClick}
|
|
70
|
+
size={size === 'sm' ? 'sm' : 'md'}
|
|
71
|
+
>
|
|
72
|
+
{secondaryAction.label}
|
|
73
|
+
</Button>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
);
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineSegment } from '@fragments/core';
|
|
3
|
+
import { Input } from './index.js';
|
|
4
|
+
|
|
5
|
+
export default defineSegment({
|
|
6
|
+
component: Input,
|
|
7
|
+
|
|
8
|
+
meta: {
|
|
9
|
+
name: 'Input',
|
|
10
|
+
description: 'Text input field for single-line user data entry',
|
|
11
|
+
category: 'forms',
|
|
12
|
+
status: 'stable',
|
|
13
|
+
tags: ['form', 'input', 'text'],
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
usage: {
|
|
17
|
+
when: [
|
|
18
|
+
'Collecting single-line text data from users',
|
|
19
|
+
'Email, password, phone number, or URL input',
|
|
20
|
+
'Search fields',
|
|
21
|
+
'Short form fields (name, title, etc.)',
|
|
22
|
+
],
|
|
23
|
+
whenNot: [
|
|
24
|
+
'Multi-line text (use Textarea)',
|
|
25
|
+
'Selecting from predefined options (use Select)',
|
|
26
|
+
'Boolean input (use Checkbox or Switch)',
|
|
27
|
+
'Date/time input (use DatePicker)',
|
|
28
|
+
],
|
|
29
|
+
guidelines: [
|
|
30
|
+
'Always provide a label for accessibility',
|
|
31
|
+
'Use appropriate input type for data validation',
|
|
32
|
+
'Show validation errors with error prop and helperText',
|
|
33
|
+
'Use placeholder for format hints, not labels',
|
|
34
|
+
],
|
|
35
|
+
accessibility: [
|
|
36
|
+
'Labels must be associated with inputs',
|
|
37
|
+
'Error messages should be announced to screen readers',
|
|
38
|
+
'Required fields should be indicated',
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
props: {
|
|
43
|
+
value: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
description: 'Current input value (controlled)',
|
|
46
|
+
},
|
|
47
|
+
placeholder: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: 'Placeholder text shown when empty',
|
|
50
|
+
constraints: ['Use for format hints only, not as a replacement for labels'],
|
|
51
|
+
},
|
|
52
|
+
type: {
|
|
53
|
+
type: 'enum',
|
|
54
|
+
values: ['text', 'email', 'password', 'number', 'tel', 'url'],
|
|
55
|
+
default: 'text',
|
|
56
|
+
description: 'HTML input type for validation and keyboard',
|
|
57
|
+
},
|
|
58
|
+
disabled: {
|
|
59
|
+
type: 'boolean',
|
|
60
|
+
default: false,
|
|
61
|
+
description: 'Whether the input is interactive',
|
|
62
|
+
},
|
|
63
|
+
error: {
|
|
64
|
+
type: 'boolean',
|
|
65
|
+
default: false,
|
|
66
|
+
description: 'Whether to show error styling',
|
|
67
|
+
},
|
|
68
|
+
label: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: 'Label text displayed above input',
|
|
71
|
+
},
|
|
72
|
+
helperText: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: 'Helper or error message below input',
|
|
75
|
+
},
|
|
76
|
+
onChange: {
|
|
77
|
+
type: 'function',
|
|
78
|
+
description: 'Called with new value on change',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
relations: [
|
|
83
|
+
{
|
|
84
|
+
component: 'Textarea',
|
|
85
|
+
relationship: 'alternative',
|
|
86
|
+
note: 'Use Textarea for multi-line text input',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
component: 'Select',
|
|
90
|
+
relationship: 'alternative',
|
|
91
|
+
note: 'Use Select when choosing from predefined options',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
component: 'FormField',
|
|
95
|
+
relationship: 'parent',
|
|
96
|
+
note: 'Wrap in FormField for consistent form layout',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
|
|
100
|
+
contract: {
|
|
101
|
+
propsSummary: [
|
|
102
|
+
'type: text|email|password|number|tel|url (default: text)',
|
|
103
|
+
'value: string - controlled input value',
|
|
104
|
+
'label: string - accessible label text',
|
|
105
|
+
'placeholder: string - format hint only',
|
|
106
|
+
'disabled: boolean - disables interaction',
|
|
107
|
+
'error: boolean - shows error styling',
|
|
108
|
+
'helperText: string - helper/error message',
|
|
109
|
+
],
|
|
110
|
+
scenarioTags: [
|
|
111
|
+
'form.input',
|
|
112
|
+
'form.text',
|
|
113
|
+
'form.email',
|
|
114
|
+
'form.password',
|
|
115
|
+
'form.search',
|
|
116
|
+
],
|
|
117
|
+
a11yRules: [
|
|
118
|
+
'A11Y_INPUT_LABEL',
|
|
119
|
+
'A11Y_INPUT_ERROR',
|
|
120
|
+
'A11Y_INPUT_REQUIRED',
|
|
121
|
+
],
|
|
122
|
+
bans: [
|
|
123
|
+
{
|
|
124
|
+
pattern: 'placeholder=.*label',
|
|
125
|
+
message: 'Use label prop for labels, not placeholder',
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
variants: [
|
|
131
|
+
{
|
|
132
|
+
name: 'Default',
|
|
133
|
+
description: 'Basic text input',
|
|
134
|
+
render: () => <Input label="Name" placeholder="Enter your name" />,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'With Value',
|
|
138
|
+
description: 'Input with pre-filled value',
|
|
139
|
+
render: () => <Input label="Email" type="email" value="user@example.com" />,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'With Helper',
|
|
143
|
+
description: 'Input with helper text',
|
|
144
|
+
render: () => (
|
|
145
|
+
<Input
|
|
146
|
+
label="Password"
|
|
147
|
+
type="password"
|
|
148
|
+
placeholder="Create a password"
|
|
149
|
+
helperText="Must be at least 8 characters"
|
|
150
|
+
/>
|
|
151
|
+
),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'Error State',
|
|
155
|
+
description: 'Input showing validation error',
|
|
156
|
+
render: () => (
|
|
157
|
+
<Input
|
|
158
|
+
label="Email"
|
|
159
|
+
type="email"
|
|
160
|
+
value="invalid-email"
|
|
161
|
+
error
|
|
162
|
+
helperText="Please enter a valid email address"
|
|
163
|
+
/>
|
|
164
|
+
),
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'Disabled',
|
|
168
|
+
description: 'Non-interactive input',
|
|
169
|
+
render: () => (
|
|
170
|
+
<Input label="Username" value="readonly-user" disabled />
|
|
171
|
+
),
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
.input {
|
|
15
|
+
@include text-base;
|
|
16
|
+
@include interactive-base;
|
|
17
|
+
|
|
18
|
+
display: block;
|
|
19
|
+
width: 100%;
|
|
20
|
+
height: var(--fui-input-height, $fui-input-height);
|
|
21
|
+
padding: 0 var(--fui-space-3, $fui-space-3);
|
|
22
|
+
background-color: var(--fui-bg-elevated, $fui-bg-elevated);
|
|
23
|
+
border: 1px solid var(--fui-border-strong, $fui-border-strong);
|
|
24
|
+
border-radius: var(--fui-radius-md, $fui-radius-md);
|
|
25
|
+
|
|
26
|
+
&::placeholder {
|
|
27
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
&:hover:not(:disabled):not(:focus) {
|
|
31
|
+
border-color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
&:focus {
|
|
35
|
+
@include focus-ring;
|
|
36
|
+
border-color: var(--fui-color-accent, $fui-color-accent);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
&:disabled,
|
|
40
|
+
&[data-disabled] {
|
|
41
|
+
background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
|
|
42
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.error {
|
|
47
|
+
border-color: var(--fui-color-danger, $fui-color-danger);
|
|
48
|
+
|
|
49
|
+
&:focus {
|
|
50
|
+
border-color: var(--fui-color-danger, $fui-color-danger);
|
|
51
|
+
box-shadow:
|
|
52
|
+
0 0 0 var(--fui-focus-ring-offset, $fui-focus-ring-offset) var(--fui-bg-primary, $fui-bg-primary),
|
|
53
|
+
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);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.helper {
|
|
58
|
+
@include helper-text;
|
|
59
|
+
margin-top: var(--fui-space-1, $fui-space-1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.helperError {
|
|
63
|
+
color: var(--fui-color-danger, $fui-color-danger);
|
|
64
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Field } from '@base-ui/react/field';
|
|
3
|
+
import styles from './Input.module.scss';
|
|
4
|
+
// Import globals to ensure CSS variables are defined
|
|
5
|
+
import '../../styles/globals.scss';
|
|
6
|
+
|
|
7
|
+
export interface InputProps {
|
|
8
|
+
value?: string;
|
|
9
|
+
defaultValue?: string;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url';
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
error?: boolean;
|
|
14
|
+
label?: string;
|
|
15
|
+
helperText?: string;
|
|
16
|
+
onChange?: (value: string) => void;
|
|
17
|
+
onBlur?: () => void;
|
|
18
|
+
className?: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
id?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
24
|
+
function Input(
|
|
25
|
+
{
|
|
26
|
+
value,
|
|
27
|
+
defaultValue,
|
|
28
|
+
placeholder,
|
|
29
|
+
type = 'text',
|
|
30
|
+
disabled = false,
|
|
31
|
+
error = false,
|
|
32
|
+
label,
|
|
33
|
+
helperText,
|
|
34
|
+
onChange,
|
|
35
|
+
onBlur,
|
|
36
|
+
className,
|
|
37
|
+
name,
|
|
38
|
+
id,
|
|
39
|
+
},
|
|
40
|
+
ref
|
|
41
|
+
) {
|
|
42
|
+
const inputClasses = [styles.input, error && styles.error, className]
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.join(' ');
|
|
45
|
+
|
|
46
|
+
const helperClasses = [styles.helper, error && styles.helperError]
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.join(' ');
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Field.Root disabled={disabled} invalid={error} className={styles.wrapper}>
|
|
52
|
+
{label && <Field.Label className={styles.label}>{label}</Field.Label>}
|
|
53
|
+
<Field.Control
|
|
54
|
+
ref={ref}
|
|
55
|
+
type={type}
|
|
56
|
+
value={value}
|
|
57
|
+
defaultValue={defaultValue}
|
|
58
|
+
placeholder={placeholder}
|
|
59
|
+
name={name}
|
|
60
|
+
id={id}
|
|
61
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
62
|
+
onChange?.(e.target.value)
|
|
63
|
+
}
|
|
64
|
+
onBlur={onBlur}
|
|
65
|
+
className={inputClasses}
|
|
66
|
+
render={<input />}
|
|
67
|
+
/>
|
|
68
|
+
{helperText && (
|
|
69
|
+
<Field.Description className={helperClasses}>
|
|
70
|
+
{helperText}
|
|
71
|
+
</Field.Description>
|
|
72
|
+
)}
|
|
73
|
+
</Field.Root>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
);
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineSegment } from '@fragments/core';
|
|
3
|
+
import { Menu } from './index.js';
|
|
4
|
+
import { Button } from '../Button/index.js';
|
|
5
|
+
|
|
6
|
+
export default defineSegment({
|
|
7
|
+
component: Menu,
|
|
8
|
+
|
|
9
|
+
meta: {
|
|
10
|
+
name: 'Menu',
|
|
11
|
+
description: 'Dropdown menu for actions and commands. Use for contextual actions, overflow menus, or grouped commands.',
|
|
12
|
+
category: 'overlays',
|
|
13
|
+
status: 'stable',
|
|
14
|
+
tags: ['menu', 'dropdown', 'actions', 'context-menu', 'commands'],
|
|
15
|
+
since: '0.1.0',
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
usage: {
|
|
19
|
+
when: [
|
|
20
|
+
'Overflow actions that dont fit in the toolbar',
|
|
21
|
+
'Context menus (right-click)',
|
|
22
|
+
'User account menus',
|
|
23
|
+
'Grouped actions with separators',
|
|
24
|
+
],
|
|
25
|
+
whenNot: [
|
|
26
|
+
'Selecting from options (use Select)',
|
|
27
|
+
'Navigation (use Tabs or navigation components)',
|
|
28
|
+
'Form selection (use Select or RadioGroup)',
|
|
29
|
+
],
|
|
30
|
+
guidelines: [
|
|
31
|
+
'Group related actions with Menu.Group',
|
|
32
|
+
'Use separators to divide action categories',
|
|
33
|
+
'Include keyboard shortcuts where applicable',
|
|
34
|
+
'Use danger variant for destructive actions',
|
|
35
|
+
'Keep menu items under 10-12 for usability',
|
|
36
|
+
],
|
|
37
|
+
accessibility: [
|
|
38
|
+
'Full keyboard navigation with arrow keys',
|
|
39
|
+
'Type-ahead search for items',
|
|
40
|
+
'Focus returns to trigger on close',
|
|
41
|
+
'Proper ARIA menu roles',
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
props: {
|
|
46
|
+
children: {
|
|
47
|
+
type: 'node',
|
|
48
|
+
description: 'Menu trigger and content',
|
|
49
|
+
required: true,
|
|
50
|
+
},
|
|
51
|
+
open: {
|
|
52
|
+
type: 'boolean',
|
|
53
|
+
description: 'Controlled open state',
|
|
54
|
+
},
|
|
55
|
+
defaultOpen: {
|
|
56
|
+
type: 'boolean',
|
|
57
|
+
description: 'Default open state (uncontrolled)',
|
|
58
|
+
default: 'false',
|
|
59
|
+
},
|
|
60
|
+
onOpenChange: {
|
|
61
|
+
type: 'function',
|
|
62
|
+
description: 'Called when open state changes',
|
|
63
|
+
},
|
|
64
|
+
modal: {
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
description: 'Whether menu is modal',
|
|
67
|
+
default: 'true',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
relations: [
|
|
72
|
+
{ component: 'Select', relationship: 'alternative', note: 'Use Select for form field selection' },
|
|
73
|
+
{ component: 'Popover', relationship: 'alternative', note: 'Use Popover for rich non-action content' },
|
|
74
|
+
],
|
|
75
|
+
|
|
76
|
+
contract: {
|
|
77
|
+
propsSummary: [
|
|
78
|
+
'open: boolean - controlled open state',
|
|
79
|
+
'onOpenChange: (open) => void - state handler',
|
|
80
|
+
'Menu.Item danger: boolean - destructive styling',
|
|
81
|
+
'Menu.Item shortcut: string - keyboard shortcut text',
|
|
82
|
+
],
|
|
83
|
+
scenarioTags: [
|
|
84
|
+
'action.menu',
|
|
85
|
+
'action.overflow',
|
|
86
|
+
'navigation.context',
|
|
87
|
+
],
|
|
88
|
+
a11yRules: ['A11Y_MENU_KEYBOARD', 'A11Y_MENU_ROLE'],
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
variants: [
|
|
92
|
+
{
|
|
93
|
+
name: 'Default',
|
|
94
|
+
description: 'Basic dropdown menu',
|
|
95
|
+
render: () => (
|
|
96
|
+
<Menu>
|
|
97
|
+
<Menu.Trigger asChild>
|
|
98
|
+
<Button variant="secondary">Actions</Button>
|
|
99
|
+
</Menu.Trigger>
|
|
100
|
+
<Menu.Content>
|
|
101
|
+
<Menu.Item onSelect={() => {}}>Edit</Menu.Item>
|
|
102
|
+
<Menu.Item onSelect={() => {}}>Duplicate</Menu.Item>
|
|
103
|
+
<Menu.Separator />
|
|
104
|
+
<Menu.Item danger onSelect={() => {}}>Delete</Menu.Item>
|
|
105
|
+
</Menu.Content>
|
|
106
|
+
</Menu>
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'With Shortcuts',
|
|
111
|
+
description: 'Menu items with keyboard shortcuts',
|
|
112
|
+
render: () => (
|
|
113
|
+
<Menu>
|
|
114
|
+
<Menu.Trigger asChild>
|
|
115
|
+
<Button variant="secondary">Edit</Button>
|
|
116
|
+
</Menu.Trigger>
|
|
117
|
+
<Menu.Content>
|
|
118
|
+
<Menu.Item shortcut="Ctrl+Z" onSelect={() => {}}>Undo</Menu.Item>
|
|
119
|
+
<Menu.Item shortcut="Ctrl+Y" onSelect={() => {}}>Redo</Menu.Item>
|
|
120
|
+
<Menu.Separator />
|
|
121
|
+
<Menu.Item shortcut="Ctrl+C" onSelect={() => {}}>Copy</Menu.Item>
|
|
122
|
+
<Menu.Item shortcut="Ctrl+V" onSelect={() => {}}>Paste</Menu.Item>
|
|
123
|
+
</Menu.Content>
|
|
124
|
+
</Menu>
|
|
125
|
+
),
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'With Groups',
|
|
129
|
+
description: 'Menu with labeled groups',
|
|
130
|
+
render: () => (
|
|
131
|
+
<Menu>
|
|
132
|
+
<Menu.Trigger asChild>
|
|
133
|
+
<Button variant="secondary">Options</Button>
|
|
134
|
+
</Menu.Trigger>
|
|
135
|
+
<Menu.Content>
|
|
136
|
+
<Menu.Group>
|
|
137
|
+
<Menu.GroupLabel>View</Menu.GroupLabel>
|
|
138
|
+
<Menu.Item onSelect={() => {}}>Zoom In</Menu.Item>
|
|
139
|
+
<Menu.Item onSelect={() => {}}>Zoom Out</Menu.Item>
|
|
140
|
+
</Menu.Group>
|
|
141
|
+
<Menu.Separator />
|
|
142
|
+
<Menu.Group>
|
|
143
|
+
<Menu.GroupLabel>Layout</Menu.GroupLabel>
|
|
144
|
+
<Menu.Item onSelect={() => {}}>Grid View</Menu.Item>
|
|
145
|
+
<Menu.Item onSelect={() => {}}>List View</Menu.Item>
|
|
146
|
+
</Menu.Group>
|
|
147
|
+
</Menu.Content>
|
|
148
|
+
</Menu>
|
|
149
|
+
),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'With Checkboxes',
|
|
153
|
+
description: 'Menu with toggleable options',
|
|
154
|
+
render: () => (
|
|
155
|
+
<Menu>
|
|
156
|
+
<Menu.Trigger asChild>
|
|
157
|
+
<Button variant="secondary">Display</Button>
|
|
158
|
+
</Menu.Trigger>
|
|
159
|
+
<Menu.Content>
|
|
160
|
+
<Menu.CheckboxItem defaultChecked>Show Grid</Menu.CheckboxItem>
|
|
161
|
+
<Menu.CheckboxItem defaultChecked>Show Rulers</Menu.CheckboxItem>
|
|
162
|
+
<Menu.CheckboxItem>Show Guides</Menu.CheckboxItem>
|
|
163
|
+
</Menu.Content>
|
|
164
|
+
</Menu>
|
|
165
|
+
),
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
});
|