@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,158 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
@use '../../tokens/mixins' as *;
|
|
3
|
+
|
|
4
|
+
// Backdrop overlay
|
|
5
|
+
.backdrop {
|
|
6
|
+
position: fixed;
|
|
7
|
+
inset: 0;
|
|
8
|
+
background-color: var(--fui-backdrop, $fui-backdrop);
|
|
9
|
+
z-index: 50;
|
|
10
|
+
|
|
11
|
+
// Animation
|
|
12
|
+
opacity: 0;
|
|
13
|
+
transition: opacity var(--fui-transition-normal, $fui-transition-normal);
|
|
14
|
+
|
|
15
|
+
&[data-open] {
|
|
16
|
+
opacity: 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
&[data-starting-style],
|
|
20
|
+
&[data-ending-style] {
|
|
21
|
+
opacity: 0;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Positioner centers the popup
|
|
26
|
+
.positioner {
|
|
27
|
+
position: fixed;
|
|
28
|
+
inset: 0;
|
|
29
|
+
z-index: 51;
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
padding: var(--fui-space-4, $fui-space-4);
|
|
34
|
+
overflow-y: auto;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The popup container
|
|
38
|
+
.popup {
|
|
39
|
+
@include surface-elevated;
|
|
40
|
+
@include text-base;
|
|
41
|
+
|
|
42
|
+
position: relative;
|
|
43
|
+
width: 100%;
|
|
44
|
+
max-width: 28rem; // 448px
|
|
45
|
+
max-height: calc(100vh - var(--fui-space-8, $fui-space-8));
|
|
46
|
+
overflow-y: auto;
|
|
47
|
+
box-shadow: var(--fui-shadow-md, $fui-shadow-md);
|
|
48
|
+
|
|
49
|
+
// Animation
|
|
50
|
+
opacity: 0;
|
|
51
|
+
transform: scale(0.95) translateY(-8px);
|
|
52
|
+
transition:
|
|
53
|
+
opacity var(--fui-transition-normal, $fui-transition-normal),
|
|
54
|
+
transform var(--fui-transition-normal, $fui-transition-normal);
|
|
55
|
+
|
|
56
|
+
&[data-open] {
|
|
57
|
+
opacity: 1;
|
|
58
|
+
transform: scale(1) translateY(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
&[data-starting-style] {
|
|
62
|
+
opacity: 0;
|
|
63
|
+
transform: scale(0.95) translateY(-8px);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
&[data-ending-style] {
|
|
67
|
+
opacity: 0;
|
|
68
|
+
transform: scale(0.95) translateY(8px);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Size variants
|
|
73
|
+
.sm {
|
|
74
|
+
max-width: 24rem; // 384px
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.md {
|
|
78
|
+
max-width: 28rem; // 448px - default
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.lg {
|
|
82
|
+
max-width: 36rem; // 576px
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.xl {
|
|
86
|
+
max-width: 48rem; // 768px
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.full {
|
|
90
|
+
max-width: calc(100vw - var(--fui-space-8, $fui-space-8));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Header area
|
|
94
|
+
.header {
|
|
95
|
+
padding: var(--fui-space-5, $fui-space-5) var(--fui-space-6, $fui-space-6);
|
|
96
|
+
padding-bottom: 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Title
|
|
100
|
+
.title {
|
|
101
|
+
margin: 0;
|
|
102
|
+
font-size: var(--fui-font-size-lg, $fui-font-size-lg);
|
|
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
|
+
.description {
|
|
110
|
+
margin: var(--fui-space-1, $fui-space-1) 0 0;
|
|
111
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
112
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
113
|
+
line-height: var(--fui-line-height-normal, $fui-line-height-normal);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Body content
|
|
117
|
+
.body {
|
|
118
|
+
padding: var(--fui-space-5, $fui-space-5) var(--fui-space-6, $fui-space-6);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Footer for actions
|
|
122
|
+
.footer {
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
justify-content: flex-end;
|
|
126
|
+
gap: var(--fui-space-3, $fui-space-3);
|
|
127
|
+
padding: var(--fui-space-4, $fui-space-4) var(--fui-space-6, $fui-space-6);
|
|
128
|
+
border-top: 1px solid var(--fui-border, $fui-border);
|
|
129
|
+
background-color: var(--fui-bg-secondary, $fui-bg-secondary);
|
|
130
|
+
border-radius: 0 0 var(--fui-radius-lg, $fui-radius-lg) var(--fui-radius-lg, $fui-radius-lg);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Close button (X in corner)
|
|
134
|
+
.close {
|
|
135
|
+
@include button-reset;
|
|
136
|
+
@include interactive-base;
|
|
137
|
+
|
|
138
|
+
position: absolute;
|
|
139
|
+
top: var(--fui-space-3, $fui-space-3);
|
|
140
|
+
right: var(--fui-space-3, $fui-space-3);
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: center;
|
|
144
|
+
width: 2rem;
|
|
145
|
+
height: 2rem;
|
|
146
|
+
border-radius: var(--fui-radius-md, $fui-radius-md);
|
|
147
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
148
|
+
|
|
149
|
+
&:hover {
|
|
150
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
151
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
svg {
|
|
155
|
+
width: 1rem;
|
|
156
|
+
height: 1rem;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
|
|
3
|
+
import styles from './Dialog.module.scss';
|
|
4
|
+
// Import globals to ensure CSS variables are defined
|
|
5
|
+
import '../../styles/globals.scss';
|
|
6
|
+
|
|
7
|
+
// ============================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================
|
|
10
|
+
|
|
11
|
+
export interface DialogProps {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
open?: boolean;
|
|
14
|
+
defaultOpen?: boolean;
|
|
15
|
+
onOpenChange?: (open: boolean) => void;
|
|
16
|
+
modal?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DialogContentProps {
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DialogTitleProps {
|
|
26
|
+
children: React.ReactNode;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DialogDescriptionProps {
|
|
31
|
+
children: React.ReactNode;
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DialogHeaderProps {
|
|
36
|
+
children: React.ReactNode;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DialogBodyProps {
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
className?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface DialogFooterProps {
|
|
46
|
+
children: React.ReactNode;
|
|
47
|
+
className?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface DialogTriggerProps {
|
|
51
|
+
children: React.ReactNode;
|
|
52
|
+
asChild?: boolean;
|
|
53
|
+
className?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface DialogCloseProps {
|
|
57
|
+
children?: React.ReactNode;
|
|
58
|
+
asChild?: boolean;
|
|
59
|
+
className?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================
|
|
63
|
+
// Close Icon
|
|
64
|
+
// ============================================
|
|
65
|
+
|
|
66
|
+
function CloseIcon() {
|
|
67
|
+
return (
|
|
68
|
+
<svg
|
|
69
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
70
|
+
width="16"
|
|
71
|
+
height="16"
|
|
72
|
+
viewBox="0 0 24 24"
|
|
73
|
+
fill="none"
|
|
74
|
+
stroke="currentColor"
|
|
75
|
+
strokeWidth="2"
|
|
76
|
+
strokeLinecap="round"
|
|
77
|
+
strokeLinejoin="round"
|
|
78
|
+
aria-hidden="true"
|
|
79
|
+
>
|
|
80
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
81
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
82
|
+
</svg>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ============================================
|
|
87
|
+
// Components
|
|
88
|
+
// ============================================
|
|
89
|
+
|
|
90
|
+
function DialogRoot({
|
|
91
|
+
children,
|
|
92
|
+
open,
|
|
93
|
+
defaultOpen,
|
|
94
|
+
onOpenChange,
|
|
95
|
+
modal = true,
|
|
96
|
+
}: DialogProps) {
|
|
97
|
+
return (
|
|
98
|
+
<BaseDialog.Root
|
|
99
|
+
open={open}
|
|
100
|
+
defaultOpen={defaultOpen}
|
|
101
|
+
onOpenChange={onOpenChange}
|
|
102
|
+
modal={modal}
|
|
103
|
+
>
|
|
104
|
+
{children}
|
|
105
|
+
</BaseDialog.Root>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function DialogTrigger({
|
|
110
|
+
children,
|
|
111
|
+
asChild,
|
|
112
|
+
className,
|
|
113
|
+
}: DialogTriggerProps) {
|
|
114
|
+
if (asChild) {
|
|
115
|
+
return (
|
|
116
|
+
<BaseDialog.Trigger className={className} render={children as React.ReactElement}>
|
|
117
|
+
{null}
|
|
118
|
+
</BaseDialog.Trigger>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<BaseDialog.Trigger className={className}>
|
|
124
|
+
{children}
|
|
125
|
+
</BaseDialog.Trigger>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function DialogContent({
|
|
130
|
+
children,
|
|
131
|
+
size = 'md',
|
|
132
|
+
className,
|
|
133
|
+
}: DialogContentProps) {
|
|
134
|
+
const popupClasses = [styles.popup, styles[size], className]
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
.join(' ');
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<BaseDialog.Portal>
|
|
140
|
+
<BaseDialog.Backdrop className={styles.backdrop} />
|
|
141
|
+
<div className={styles.positioner}>
|
|
142
|
+
<BaseDialog.Popup className={popupClasses}>
|
|
143
|
+
{children}
|
|
144
|
+
</BaseDialog.Popup>
|
|
145
|
+
</div>
|
|
146
|
+
</BaseDialog.Portal>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function DialogHeader({ children, className }: DialogHeaderProps) {
|
|
151
|
+
const classes = [styles.header, className].filter(Boolean).join(' ');
|
|
152
|
+
return <div className={classes}>{children}</div>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function DialogTitle({ children, className }: DialogTitleProps) {
|
|
156
|
+
const classes = [styles.title, className].filter(Boolean).join(' ');
|
|
157
|
+
return <BaseDialog.Title className={classes}>{children}</BaseDialog.Title>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function DialogDescription({ children, className }: DialogDescriptionProps) {
|
|
161
|
+
const classes = [styles.description, className].filter(Boolean).join(' ');
|
|
162
|
+
return (
|
|
163
|
+
<BaseDialog.Description className={classes}>
|
|
164
|
+
{children}
|
|
165
|
+
</BaseDialog.Description>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function DialogBody({ children, className }: DialogBodyProps) {
|
|
170
|
+
const classes = [styles.body, className].filter(Boolean).join(' ');
|
|
171
|
+
return <div className={classes}>{children}</div>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function DialogFooter({ children, className }: DialogFooterProps) {
|
|
175
|
+
const classes = [styles.footer, className].filter(Boolean).join(' ');
|
|
176
|
+
return <div className={classes}>{children}</div>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function DialogClose({ children, asChild, className }: DialogCloseProps) {
|
|
180
|
+
// If no children, render the default X close button
|
|
181
|
+
if (!children) {
|
|
182
|
+
return (
|
|
183
|
+
<BaseDialog.Close className={[styles.close, className].filter(Boolean).join(' ')}>
|
|
184
|
+
<CloseIcon />
|
|
185
|
+
</BaseDialog.Close>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (asChild) {
|
|
190
|
+
return (
|
|
191
|
+
<BaseDialog.Close className={className} render={children as React.ReactElement}>
|
|
192
|
+
{null}
|
|
193
|
+
</BaseDialog.Close>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<BaseDialog.Close className={className}>
|
|
199
|
+
{children}
|
|
200
|
+
</BaseDialog.Close>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ============================================
|
|
205
|
+
// Export compound component
|
|
206
|
+
// ============================================
|
|
207
|
+
|
|
208
|
+
export const Dialog = Object.assign(DialogRoot, {
|
|
209
|
+
Trigger: DialogTrigger,
|
|
210
|
+
Content: DialogContent,
|
|
211
|
+
Header: DialogHeader,
|
|
212
|
+
Title: DialogTitle,
|
|
213
|
+
Description: DialogDescription,
|
|
214
|
+
Body: DialogBody,
|
|
215
|
+
Footer: DialogFooter,
|
|
216
|
+
Close: DialogClose,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Re-export individual components for tree-shaking
|
|
220
|
+
export {
|
|
221
|
+
DialogRoot,
|
|
222
|
+
DialogTrigger,
|
|
223
|
+
DialogContent,
|
|
224
|
+
DialogHeader,
|
|
225
|
+
DialogTitle,
|
|
226
|
+
DialogDescription,
|
|
227
|
+
DialogBody,
|
|
228
|
+
DialogFooter,
|
|
229
|
+
DialogClose,
|
|
230
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineSegment } from '@fragments/core';
|
|
3
|
+
import { EmptyState } from './index.js';
|
|
4
|
+
|
|
5
|
+
// Simple placeholder icon
|
|
6
|
+
const FolderIcon = () => (
|
|
7
|
+
<svg
|
|
8
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
9
|
+
width="48"
|
|
10
|
+
height="48"
|
|
11
|
+
viewBox="0 0 24 24"
|
|
12
|
+
fill="none"
|
|
13
|
+
stroke="currentColor"
|
|
14
|
+
strokeWidth="1.5"
|
|
15
|
+
strokeLinecap="round"
|
|
16
|
+
strokeLinejoin="round"
|
|
17
|
+
>
|
|
18
|
+
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
|
|
19
|
+
</svg>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const SearchIcon = () => (
|
|
23
|
+
<svg
|
|
24
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
25
|
+
width="48"
|
|
26
|
+
height="48"
|
|
27
|
+
viewBox="0 0 24 24"
|
|
28
|
+
fill="none"
|
|
29
|
+
stroke="currentColor"
|
|
30
|
+
strokeWidth="1.5"
|
|
31
|
+
strokeLinecap="round"
|
|
32
|
+
strokeLinejoin="round"
|
|
33
|
+
>
|
|
34
|
+
<circle cx="11" cy="11" r="8" />
|
|
35
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
36
|
+
</svg>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const InboxIcon = () => (
|
|
40
|
+
<svg
|
|
41
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
42
|
+
width="48"
|
|
43
|
+
height="48"
|
|
44
|
+
viewBox="0 0 24 24"
|
|
45
|
+
fill="none"
|
|
46
|
+
stroke="currentColor"
|
|
47
|
+
strokeWidth="1.5"
|
|
48
|
+
strokeLinecap="round"
|
|
49
|
+
strokeLinejoin="round"
|
|
50
|
+
>
|
|
51
|
+
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
|
|
52
|
+
<path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
|
|
53
|
+
</svg>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
export default defineSegment({
|
|
57
|
+
component: EmptyState,
|
|
58
|
+
|
|
59
|
+
meta: {
|
|
60
|
+
name: 'EmptyState',
|
|
61
|
+
description: 'Placeholder for empty content areas. Provides context, guidance, and actions when no data is available.',
|
|
62
|
+
category: 'feedback',
|
|
63
|
+
status: 'stable',
|
|
64
|
+
tags: ['empty', 'placeholder', 'no-data', 'zero-state', 'blank-slate'],
|
|
65
|
+
since: '0.1.0',
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
usage: {
|
|
69
|
+
when: [
|
|
70
|
+
'Empty lists, tables, or search results',
|
|
71
|
+
'New user onboarding (no content yet)',
|
|
72
|
+
'Filtered views with no matches',
|
|
73
|
+
'Error states where content failed to load',
|
|
74
|
+
],
|
|
75
|
+
whenNot: [
|
|
76
|
+
'Loading states (use skeleton or spinner)',
|
|
77
|
+
'Error messages with retry (use Alert)',
|
|
78
|
+
'Temporary messages (use Toast)',
|
|
79
|
+
],
|
|
80
|
+
guidelines: [
|
|
81
|
+
'Always explain why the area is empty',
|
|
82
|
+
'Provide a clear action to resolve the empty state',
|
|
83
|
+
'Use appropriate icons to reinforce the message',
|
|
84
|
+
'Keep messaging positive and actionable',
|
|
85
|
+
],
|
|
86
|
+
accessibility: [
|
|
87
|
+
'Empty state content is accessible to screen readers',
|
|
88
|
+
'Action buttons follow button accessibility guidelines',
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
props: {
|
|
93
|
+
title: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'Main heading text',
|
|
96
|
+
required: true,
|
|
97
|
+
},
|
|
98
|
+
description: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
description: 'Supporting description text',
|
|
101
|
+
},
|
|
102
|
+
icon: {
|
|
103
|
+
type: 'node',
|
|
104
|
+
description: 'Optional icon element',
|
|
105
|
+
},
|
|
106
|
+
action: {
|
|
107
|
+
type: 'object',
|
|
108
|
+
description: 'Primary action button: { label, onClick, variant? }',
|
|
109
|
+
},
|
|
110
|
+
secondaryAction: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
description: 'Secondary action button: { label, onClick, variant? }',
|
|
113
|
+
},
|
|
114
|
+
size: {
|
|
115
|
+
type: 'enum',
|
|
116
|
+
description: 'Size variant',
|
|
117
|
+
values: ['sm', 'md', 'lg'],
|
|
118
|
+
default: 'md',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
relations: [
|
|
123
|
+
{ component: 'Alert', relationship: 'alternative', note: 'Use Alert for error states with retry' },
|
|
124
|
+
{ component: 'Progress', relationship: 'alternative', note: 'Use Progress/Spinner for loading states' },
|
|
125
|
+
],
|
|
126
|
+
|
|
127
|
+
contract: {
|
|
128
|
+
propsSummary: [
|
|
129
|
+
'title: string - main heading (required)',
|
|
130
|
+
'description: string - supporting text',
|
|
131
|
+
'icon: ReactNode - illustrative icon',
|
|
132
|
+
'action: { label, onClick } - primary CTA',
|
|
133
|
+
'secondaryAction: { label, onClick } - secondary CTA',
|
|
134
|
+
],
|
|
135
|
+
scenarioTags: [
|
|
136
|
+
'feedback.empty',
|
|
137
|
+
'onboarding.start',
|
|
138
|
+
'search.no-results',
|
|
139
|
+
],
|
|
140
|
+
a11yRules: ['A11Y_EMPTY_STATE_CONTENT'],
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
variants: [
|
|
144
|
+
{
|
|
145
|
+
name: 'Default',
|
|
146
|
+
description: 'Basic empty state with action',
|
|
147
|
+
render: () => (
|
|
148
|
+
<EmptyState
|
|
149
|
+
icon={<FolderIcon />}
|
|
150
|
+
title="No projects yet"
|
|
151
|
+
description="Get started by creating your first project."
|
|
152
|
+
action={{
|
|
153
|
+
label: 'Create Project',
|
|
154
|
+
onClick: () => {},
|
|
155
|
+
}}
|
|
156
|
+
/>
|
|
157
|
+
),
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'No Results',
|
|
161
|
+
description: 'Empty search results',
|
|
162
|
+
render: () => (
|
|
163
|
+
<EmptyState
|
|
164
|
+
icon={<SearchIcon />}
|
|
165
|
+
title="No results found"
|
|
166
|
+
description="Try adjusting your search terms or filters."
|
|
167
|
+
action={{
|
|
168
|
+
label: 'Clear Filters',
|
|
169
|
+
onClick: () => {},
|
|
170
|
+
variant: 'secondary',
|
|
171
|
+
}}
|
|
172
|
+
/>
|
|
173
|
+
),
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'With Secondary Action',
|
|
177
|
+
description: 'Empty state with two actions',
|
|
178
|
+
render: () => (
|
|
179
|
+
<EmptyState
|
|
180
|
+
icon={<InboxIcon />}
|
|
181
|
+
title="Inbox is empty"
|
|
182
|
+
description="You have no new messages."
|
|
183
|
+
action={{
|
|
184
|
+
label: 'Compose Message',
|
|
185
|
+
onClick: () => {},
|
|
186
|
+
}}
|
|
187
|
+
secondaryAction={{
|
|
188
|
+
label: 'View Archive',
|
|
189
|
+
onClick: () => {},
|
|
190
|
+
}}
|
|
191
|
+
/>
|
|
192
|
+
),
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'Small',
|
|
196
|
+
description: 'Compact empty state for inline use',
|
|
197
|
+
render: () => (
|
|
198
|
+
<EmptyState
|
|
199
|
+
size="sm"
|
|
200
|
+
title="No items"
|
|
201
|
+
description="Add items to see them here."
|
|
202
|
+
/>
|
|
203
|
+
),
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'Large',
|
|
207
|
+
description: 'Prominent empty state for full-page use',
|
|
208
|
+
render: () => (
|
|
209
|
+
<EmptyState
|
|
210
|
+
size="lg"
|
|
211
|
+
icon={<FolderIcon />}
|
|
212
|
+
title="Welcome to your workspace"
|
|
213
|
+
description="This is where your projects will appear. Create your first project to get started."
|
|
214
|
+
action={{
|
|
215
|
+
label: 'Create Your First Project',
|
|
216
|
+
onClick: () => {},
|
|
217
|
+
}}
|
|
218
|
+
/>
|
|
219
|
+
),
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
});
|