@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,136 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
|
|
3
|
+
// ============================================
|
|
4
|
+
// Avatar Component Styles
|
|
5
|
+
// ============================================
|
|
6
|
+
|
|
7
|
+
.avatar {
|
|
8
|
+
position: relative;
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
flex-shrink: 0;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
border-radius: var(--fui-radius-full, $fui-radius-full);
|
|
15
|
+
background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
|
|
16
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
17
|
+
font-family: var(--fui-font-sans, $fui-font-sans);
|
|
18
|
+
user-select: none;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Shape variant
|
|
22
|
+
.square {
|
|
23
|
+
border-radius: var(--fui-radius-md, $fui-radius-md);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ============================================
|
|
27
|
+
// Size Variants
|
|
28
|
+
// ============================================
|
|
29
|
+
|
|
30
|
+
.xs {
|
|
31
|
+
width: 1.5rem; // 24px
|
|
32
|
+
height: 1.5rem;
|
|
33
|
+
font-size: var(--fui-font-size-2xs, $fui-font-size-2xs);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.sm {
|
|
37
|
+
width: 2rem; // 32px
|
|
38
|
+
height: 2rem;
|
|
39
|
+
font-size: var(--fui-font-size-xs, $fui-font-size-xs);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.md {
|
|
43
|
+
width: 2.5rem; // 40px
|
|
44
|
+
height: 2.5rem;
|
|
45
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.lg {
|
|
49
|
+
width: 3rem; // 48px
|
|
50
|
+
height: 3rem;
|
|
51
|
+
font-size: var(--fui-font-size-base, $fui-font-size-base);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.xl {
|
|
55
|
+
width: 4rem; // 64px
|
|
56
|
+
height: 4rem;
|
|
57
|
+
font-size: var(--fui-font-size-lg, $fui-font-size-lg);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ============================================
|
|
61
|
+
// Image
|
|
62
|
+
// ============================================
|
|
63
|
+
|
|
64
|
+
.image {
|
|
65
|
+
width: 100%;
|
|
66
|
+
height: 100%;
|
|
67
|
+
object-fit: cover;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ============================================
|
|
71
|
+
// Initials / Fallback
|
|
72
|
+
// ============================================
|
|
73
|
+
|
|
74
|
+
.initials {
|
|
75
|
+
font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
|
|
76
|
+
color: var(--fui-text-inverse, $fui-text-inverse);
|
|
77
|
+
text-transform: uppercase;
|
|
78
|
+
line-height: 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.fallbackIcon {
|
|
82
|
+
width: 60%;
|
|
83
|
+
height: 60%;
|
|
84
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================
|
|
88
|
+
// Avatar Group
|
|
89
|
+
// ============================================
|
|
90
|
+
|
|
91
|
+
.group {
|
|
92
|
+
display: inline-flex;
|
|
93
|
+
flex-direction: row-reverse;
|
|
94
|
+
justify-content: flex-end;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.groupItem {
|
|
98
|
+
margin-left: -0.5rem;
|
|
99
|
+
border: 2px solid var(--fui-bg-primary, $fui-bg-primary);
|
|
100
|
+
|
|
101
|
+
&:last-child {
|
|
102
|
+
margin-left: 0;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Adjust overlap for different sizes
|
|
107
|
+
.group .xs.groupItem {
|
|
108
|
+
margin-left: -0.375rem;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.group .sm.groupItem {
|
|
112
|
+
margin-left: -0.5rem;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.group .md.groupItem {
|
|
116
|
+
margin-left: -0.625rem;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.group .lg.groupItem {
|
|
120
|
+
margin-left: -0.75rem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.group .xl.groupItem {
|
|
124
|
+
margin-left: -1rem;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Overflow count indicator
|
|
128
|
+
.overflow {
|
|
129
|
+
margin-left: -0.625rem;
|
|
130
|
+
background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
|
|
131
|
+
border: 2px solid var(--fui-bg-primary, $fui-bg-primary);
|
|
132
|
+
|
|
133
|
+
.initials {
|
|
134
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import styles from './Avatar.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 AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
11
|
+
|
|
12
|
+
export interface AvatarProps {
|
|
13
|
+
/** Image source URL */
|
|
14
|
+
src?: string;
|
|
15
|
+
/** Alt text for the image */
|
|
16
|
+
alt?: string;
|
|
17
|
+
/** Fallback initials (1-2 characters recommended) */
|
|
18
|
+
initials?: string;
|
|
19
|
+
/** Full name - used to generate initials if not provided */
|
|
20
|
+
name?: string;
|
|
21
|
+
/** Size variant */
|
|
22
|
+
size?: AvatarSize;
|
|
23
|
+
/** Shape variant */
|
|
24
|
+
shape?: 'circle' | 'square';
|
|
25
|
+
/** Custom background color for fallback */
|
|
26
|
+
color?: string;
|
|
27
|
+
/** Additional class name */
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AvatarGroupProps {
|
|
32
|
+
/** Maximum number of avatars to display */
|
|
33
|
+
max?: number;
|
|
34
|
+
/** Size for all avatars in the group */
|
|
35
|
+
size?: AvatarSize;
|
|
36
|
+
/** Children (Avatar components) */
|
|
37
|
+
children: React.ReactNode;
|
|
38
|
+
/** Additional class name */
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ============================================
|
|
43
|
+
// Helper Functions
|
|
44
|
+
// ============================================
|
|
45
|
+
|
|
46
|
+
function getInitials(name: string): string {
|
|
47
|
+
const parts = name.trim().split(/\s+/);
|
|
48
|
+
if (parts.length === 1) {
|
|
49
|
+
return parts[0].charAt(0).toUpperCase();
|
|
50
|
+
}
|
|
51
|
+
return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stringToColor(str: string): string {
|
|
55
|
+
// Generate a consistent color from a string
|
|
56
|
+
let hash = 0;
|
|
57
|
+
for (let i = 0; i < str.length; i++) {
|
|
58
|
+
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
59
|
+
}
|
|
60
|
+
const hue = Math.abs(hash % 360);
|
|
61
|
+
return `hsl(${hue}, 65%, 50%)`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============================================
|
|
65
|
+
// Avatar Component
|
|
66
|
+
// ============================================
|
|
67
|
+
|
|
68
|
+
const AvatarBase = React.forwardRef<HTMLDivElement, AvatarProps>(
|
|
69
|
+
function AvatarBase(
|
|
70
|
+
{
|
|
71
|
+
src,
|
|
72
|
+
alt = '',
|
|
73
|
+
initials,
|
|
74
|
+
name,
|
|
75
|
+
size = 'md',
|
|
76
|
+
shape = 'circle',
|
|
77
|
+
color,
|
|
78
|
+
className,
|
|
79
|
+
},
|
|
80
|
+
ref
|
|
81
|
+
) {
|
|
82
|
+
const [imageError, setImageError] = React.useState(false);
|
|
83
|
+
|
|
84
|
+
// Reset error state when src changes
|
|
85
|
+
React.useEffect(() => {
|
|
86
|
+
setImageError(false);
|
|
87
|
+
}, [src]);
|
|
88
|
+
|
|
89
|
+
const showFallback = !src || imageError;
|
|
90
|
+
const displayInitials = initials || (name ? getInitials(name) : '');
|
|
91
|
+
const fallbackColor = color || (name ? stringToColor(name) : undefined);
|
|
92
|
+
|
|
93
|
+
const avatarClasses = [
|
|
94
|
+
styles.avatar,
|
|
95
|
+
styles[size],
|
|
96
|
+
shape === 'square' && styles.square,
|
|
97
|
+
className,
|
|
98
|
+
].filter(Boolean).join(' ');
|
|
99
|
+
|
|
100
|
+
const style: React.CSSProperties = {};
|
|
101
|
+
if (showFallback && fallbackColor) {
|
|
102
|
+
style.backgroundColor = fallbackColor;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div ref={ref} className={avatarClasses} style={style}>
|
|
107
|
+
{!showFallback && (
|
|
108
|
+
<img
|
|
109
|
+
src={src}
|
|
110
|
+
alt={alt}
|
|
111
|
+
className={styles.image}
|
|
112
|
+
onError={() => setImageError(true)}
|
|
113
|
+
/>
|
|
114
|
+
)}
|
|
115
|
+
{showFallback && displayInitials && (
|
|
116
|
+
<span className={styles.initials}>{displayInitials}</span>
|
|
117
|
+
)}
|
|
118
|
+
{showFallback && !displayInitials && (
|
|
119
|
+
<svg
|
|
120
|
+
className={styles.fallbackIcon}
|
|
121
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
122
|
+
viewBox="0 0 24 24"
|
|
123
|
+
fill="currentColor"
|
|
124
|
+
aria-hidden="true"
|
|
125
|
+
>
|
|
126
|
+
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
|
|
127
|
+
</svg>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// ============================================
|
|
135
|
+
// Avatar Group Component
|
|
136
|
+
// ============================================
|
|
137
|
+
|
|
138
|
+
function AvatarGroup({
|
|
139
|
+
max,
|
|
140
|
+
size = 'md',
|
|
141
|
+
children,
|
|
142
|
+
className,
|
|
143
|
+
}: AvatarGroupProps) {
|
|
144
|
+
const childArray = React.Children.toArray(children);
|
|
145
|
+
const displayCount = max && max < childArray.length ? max : childArray.length;
|
|
146
|
+
const overflowCount = max && childArray.length > max ? childArray.length - max : 0;
|
|
147
|
+
|
|
148
|
+
const groupClasses = [styles.group, className].filter(Boolean).join(' ');
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div className={groupClasses}>
|
|
152
|
+
{childArray.slice(0, displayCount).map((child, index) => {
|
|
153
|
+
if (React.isValidElement<AvatarProps>(child)) {
|
|
154
|
+
return React.cloneElement(child, {
|
|
155
|
+
key: index,
|
|
156
|
+
size: child.props.size || size,
|
|
157
|
+
className: [styles.groupItem, child.props.className].filter(Boolean).join(' '),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return child;
|
|
161
|
+
})}
|
|
162
|
+
{overflowCount > 0 && (
|
|
163
|
+
<div className={[styles.avatar, styles[size], styles.overflow].join(' ')}>
|
|
164
|
+
<span className={styles.initials}>+{overflowCount}</span>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ============================================
|
|
172
|
+
// Compound Component Export
|
|
173
|
+
// ============================================
|
|
174
|
+
|
|
175
|
+
export const Avatar = Object.assign(AvatarBase, {
|
|
176
|
+
Group: AvatarGroup,
|
|
177
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineSegment } from '@fragments/core';
|
|
3
|
+
import { Badge } from './index.js';
|
|
4
|
+
|
|
5
|
+
export default defineSegment({
|
|
6
|
+
component: Badge,
|
|
7
|
+
|
|
8
|
+
meta: {
|
|
9
|
+
name: 'Badge',
|
|
10
|
+
description: 'Compact label for status, counts, or categorization. Draws attention to metadata without dominating the layout.',
|
|
11
|
+
category: 'feedback',
|
|
12
|
+
status: 'stable',
|
|
13
|
+
tags: ['status', 'label', 'tag', 'count', 'chip'],
|
|
14
|
+
since: '0.1.0',
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
usage: {
|
|
18
|
+
when: [
|
|
19
|
+
'Showing item status (active, pending, archived)',
|
|
20
|
+
'Displaying counts or quantities inline',
|
|
21
|
+
'Categorizing or tagging content',
|
|
22
|
+
'Highlighting new or updated items',
|
|
23
|
+
],
|
|
24
|
+
whenNot: [
|
|
25
|
+
'Conveying critical errors (use Alert instead)',
|
|
26
|
+
'Long-form status messages (use Alert)',
|
|
27
|
+
'Interactive filtering (use chip/toggle group)',
|
|
28
|
+
'Navigation labels (use tabs or links)',
|
|
29
|
+
],
|
|
30
|
+
guidelines: [
|
|
31
|
+
'Keep badge text under 20 characters',
|
|
32
|
+
'Use dot variant for live status indicators',
|
|
33
|
+
'Pair success/error variants with meaningful labels, not just colors',
|
|
34
|
+
'Use onRemove for user-created tags only, not system-generated badges',
|
|
35
|
+
],
|
|
36
|
+
accessibility: [
|
|
37
|
+
'Badge text must be meaningful without relying on color alone',
|
|
38
|
+
'Removable badges must have accessible dismiss button labels',
|
|
39
|
+
'Avoid using badges as the sole indicator of important state changes',
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
props: {
|
|
44
|
+
children: {
|
|
45
|
+
type: 'node',
|
|
46
|
+
description: 'Badge label text',
|
|
47
|
+
required: true,
|
|
48
|
+
},
|
|
49
|
+
variant: {
|
|
50
|
+
type: 'enum',
|
|
51
|
+
description: 'Visual style indicating severity or category',
|
|
52
|
+
values: ['default', 'success', 'warning', 'error', 'info'],
|
|
53
|
+
default: 'default',
|
|
54
|
+
},
|
|
55
|
+
size: {
|
|
56
|
+
type: 'enum',
|
|
57
|
+
description: 'Badge size',
|
|
58
|
+
values: ['sm', 'md'],
|
|
59
|
+
default: 'md',
|
|
60
|
+
},
|
|
61
|
+
dot: {
|
|
62
|
+
type: 'boolean',
|
|
63
|
+
description: 'Show a colored dot indicator before the label',
|
|
64
|
+
default: 'false',
|
|
65
|
+
},
|
|
66
|
+
icon: {
|
|
67
|
+
type: 'node',
|
|
68
|
+
description: 'Optional icon element before the text',
|
|
69
|
+
},
|
|
70
|
+
onRemove: {
|
|
71
|
+
type: 'function',
|
|
72
|
+
description: 'Makes the badge removable. Called when X is clicked.',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
relations: [
|
|
77
|
+
{ component: 'Alert', relationship: 'alternative', note: 'Use Alert for prominent, longer messages with actions' },
|
|
78
|
+
{ component: 'Tag', relationship: 'sibling', note: 'Tag is interactive (clickable/filterable); Badge is display-only' },
|
|
79
|
+
],
|
|
80
|
+
|
|
81
|
+
contract: {
|
|
82
|
+
propsSummary: [
|
|
83
|
+
'children: ReactNode - badge label (required)',
|
|
84
|
+
'variant: default|success|warning|error|info - visual style',
|
|
85
|
+
'size: sm|md - badge size',
|
|
86
|
+
'dot: boolean - show status dot indicator',
|
|
87
|
+
'onRemove: () => void - makes badge removable',
|
|
88
|
+
],
|
|
89
|
+
scenarioTags: [
|
|
90
|
+
'feedback.status',
|
|
91
|
+
'display.label',
|
|
92
|
+
'display.count',
|
|
93
|
+
'content.tag',
|
|
94
|
+
],
|
|
95
|
+
a11yRules: ['A11Y_BADGE_CONTRAST', 'A11Y_BADGE_DISMISS'],
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
variants: [
|
|
99
|
+
{
|
|
100
|
+
name: 'Default',
|
|
101
|
+
description: 'Neutral badge for general labels',
|
|
102
|
+
render: () => <Badge>Default</Badge>,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'Status Variants',
|
|
106
|
+
description: 'All severity variants for different contexts',
|
|
107
|
+
render: () => (
|
|
108
|
+
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
|
109
|
+
<Badge variant="default">Default</Badge>
|
|
110
|
+
<Badge variant="success">Active</Badge>
|
|
111
|
+
<Badge variant="warning">Pending</Badge>
|
|
112
|
+
<Badge variant="error">Failed</Badge>
|
|
113
|
+
<Badge variant="info">New</Badge>
|
|
114
|
+
</div>
|
|
115
|
+
),
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'With Dot',
|
|
119
|
+
description: 'Live status indicators using dot prefix',
|
|
120
|
+
render: () => (
|
|
121
|
+
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
|
122
|
+
<Badge variant="success" dot>Online</Badge>
|
|
123
|
+
<Badge variant="warning" dot>Away</Badge>
|
|
124
|
+
<Badge variant="error" dot>Offline</Badge>
|
|
125
|
+
</div>
|
|
126
|
+
),
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'Small Size',
|
|
130
|
+
description: 'Compact badges for dense UIs',
|
|
131
|
+
render: () => (
|
|
132
|
+
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
|
133
|
+
<Badge size="sm" variant="info">v2.1</Badge>
|
|
134
|
+
<Badge size="sm" variant="success">Stable</Badge>
|
|
135
|
+
<Badge size="md" variant="info">Standard</Badge>
|
|
136
|
+
</div>
|
|
137
|
+
),
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'Removable',
|
|
141
|
+
description: 'User-created tags that can be dismissed',
|
|
142
|
+
render: () => (
|
|
143
|
+
<div style={{ display: 'flex', gap: '8px' }}>
|
|
144
|
+
<Badge variant="info" onRemove={() => {}}>React</Badge>
|
|
145
|
+
<Badge variant="info" onRemove={() => {}}>TypeScript</Badge>
|
|
146
|
+
<Badge variant="info" onRemove={() => {}}>CSS</Badge>
|
|
147
|
+
</div>
|
|
148
|
+
),
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
@use '../../tokens/mixins' as *;
|
|
3
|
+
|
|
4
|
+
.badge {
|
|
5
|
+
display: inline-flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
gap: var(--fui-space-1, $fui-space-1);
|
|
8
|
+
border-radius: var(--fui-radius-full, $fui-radius-full);
|
|
9
|
+
font-family: var(--fui-font-sans, $fui-font-sans);
|
|
10
|
+
font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
|
|
11
|
+
line-height: 1;
|
|
12
|
+
white-space: nowrap;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Sizes
|
|
16
|
+
.sm {
|
|
17
|
+
padding: 2px var(--fui-space-2, $fui-space-2);
|
|
18
|
+
font-size: var(--fui-font-size-2xs, $fui-font-size-2xs);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.md {
|
|
22
|
+
padding: var(--fui-space-1, $fui-space-1) 10px;
|
|
23
|
+
font-size: var(--fui-font-size-xs, $fui-font-size-xs);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Variants
|
|
27
|
+
.default {
|
|
28
|
+
background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
|
|
29
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.success {
|
|
33
|
+
background-color: var(--fui-color-success-bg, $fui-color-success-bg);
|
|
34
|
+
color: var(--fui-color-success, $fui-color-success);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.warning {
|
|
38
|
+
background-color: var(--fui-color-warning-bg, $fui-color-warning-bg);
|
|
39
|
+
color: var(--fui-color-warning, $fui-color-warning);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.error {
|
|
43
|
+
background-color: var(--fui-color-danger-bg, $fui-color-danger-bg);
|
|
44
|
+
color: var(--fui-color-danger, $fui-color-danger);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.info {
|
|
48
|
+
background-color: var(--fui-color-info-bg, $fui-color-info-bg);
|
|
49
|
+
color: var(--fui-color-info, $fui-color-info);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.dot {
|
|
53
|
+
width: 6px;
|
|
54
|
+
height: 6px;
|
|
55
|
+
border-radius: 50%;
|
|
56
|
+
background-color: currentColor;
|
|
57
|
+
|
|
58
|
+
.sm & {
|
|
59
|
+
width: 5px;
|
|
60
|
+
height: 5px;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.icon {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.remove {
|
|
70
|
+
@include button-reset;
|
|
71
|
+
@include interactive-base;
|
|
72
|
+
|
|
73
|
+
padding: 0 2px;
|
|
74
|
+
font-size: 14px;
|
|
75
|
+
color: inherit;
|
|
76
|
+
opacity: 0.6;
|
|
77
|
+
line-height: 1;
|
|
78
|
+
border-radius: 2px;
|
|
79
|
+
|
|
80
|
+
.sm & {
|
|
81
|
+
font-size: 12px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
&:hover {
|
|
85
|
+
opacity: 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Button as BaseButton } from '@base-ui/react/button';
|
|
3
|
+
import styles from './Badge.module.scss';
|
|
4
|
+
// Import globals to ensure CSS variables are defined
|
|
5
|
+
import '../../styles/globals.scss';
|
|
6
|
+
|
|
7
|
+
export interface BadgeProps {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
variant?: 'default' | 'success' | 'warning' | 'error' | 'info';
|
|
10
|
+
size?: 'sm' | 'md';
|
|
11
|
+
dot?: boolean;
|
|
12
|
+
icon?: React.ReactNode;
|
|
13
|
+
onRemove?: () => void;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
|
|
18
|
+
function Badge(
|
|
19
|
+
{
|
|
20
|
+
children,
|
|
21
|
+
variant = 'default',
|
|
22
|
+
size = 'md',
|
|
23
|
+
dot = false,
|
|
24
|
+
icon,
|
|
25
|
+
onRemove,
|
|
26
|
+
className,
|
|
27
|
+
},
|
|
28
|
+
ref
|
|
29
|
+
) {
|
|
30
|
+
const classes = [styles.badge, styles[size], styles[variant], className]
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.join(' ');
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<span ref={ref} className={classes}>
|
|
36
|
+
{dot && <span className={styles.dot} aria-hidden="true" />}
|
|
37
|
+
{icon && (
|
|
38
|
+
<span className={styles.icon} aria-hidden="true">
|
|
39
|
+
{icon}
|
|
40
|
+
</span>
|
|
41
|
+
)}
|
|
42
|
+
{children}
|
|
43
|
+
{onRemove && (
|
|
44
|
+
<BaseButton
|
|
45
|
+
onClick={onRemove}
|
|
46
|
+
aria-label="Remove"
|
|
47
|
+
className={styles.remove}
|
|
48
|
+
>
|
|
49
|
+
×
|
|
50
|
+
</BaseButton>
|
|
51
|
+
)}
|
|
52
|
+
</span>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
);
|