@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,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
+ });