@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
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@fragments-sdk/ui",
3
+ "version": "0.1.0",
4
+ "description": "Customizable UI components built on Base UI headless primitives",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts",
10
+ "./styles": "./src/styles/globals.scss",
11
+ "./tokens": "./src/tokens/_variables.scss",
12
+ "./brand": "./src/brand.ts"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "scripts": {
18
+ "dev": "fragments dev",
19
+ "build": "fragments build",
20
+ "validate": "fragments validate"
21
+ },
22
+ "peerDependencies": {
23
+ "react": "^18.0.0 || ^19.0.0",
24
+ "react-dom": "^18.0.0 || ^19.0.0"
25
+ },
26
+ "dependencies": {
27
+ "@base-ui/react": "^1.0.0",
28
+ "@tanstack/react-table": "^8.21.3"
29
+ },
30
+ "devDependencies": {
31
+ "@fragments/cli": "workspace:*",
32
+ "@fragments/core": "workspace:*",
33
+ "@fragments/viewer": "workspace:*",
34
+ "@types/react": "^19.0.0",
35
+ "@types/react-dom": "^19.0.0",
36
+ "react": "^19.0.0",
37
+ "react-dom": "^19.0.0",
38
+ "sass": "^1.83.0",
39
+ "typescript": "^5.7.0"
40
+ },
41
+ "files": [
42
+ "src"
43
+ ]
44
+ }
package/src/brand.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Shared brand constants for the Fragments ecosystem
3
+ */
4
+ export const BRAND = {
5
+ name: 'Fragments',
6
+ tagline: 'AI-native design system tooling',
7
+ url: 'https://fragments.cloud',
8
+ support: 'support@fragments.cloud',
9
+ social: {
10
+ github: 'https://github.com/anthropics/fragments',
11
+ twitter: '@fragmentscloud',
12
+ },
13
+ } as const;
14
+
15
+ export type Brand = typeof BRAND;
@@ -0,0 +1,163 @@
1
+ import React from 'react';
2
+ import { defineSegment } from '@fragments/core';
3
+ import { Alert } from './index.js';
4
+
5
+ export default defineSegment({
6
+ component: Alert,
7
+
8
+ meta: {
9
+ name: 'Alert',
10
+ description: 'Contextual feedback messages for user actions or system status. Supports multiple severity levels with optional actions and dismissibility.',
11
+ category: 'feedback',
12
+ status: 'stable',
13
+ tags: ['notification', 'message', 'feedback', 'banner', 'toast'],
14
+ since: '0.1.0',
15
+ },
16
+
17
+ usage: {
18
+ when: [
19
+ 'Communicating the result of a user action (success, error)',
20
+ 'Warning about potential issues before they occur',
21
+ 'Providing important contextual information inline',
22
+ 'System status notifications that require attention',
23
+ ],
24
+ whenNot: [
25
+ 'Brief status labels (use Badge instead)',
26
+ 'Transient notifications (use Toast/Snackbar)',
27
+ 'Form-field-level errors (use Input error prop)',
28
+ 'Confirmation before destructive actions (use Dialog)',
29
+ ],
30
+ guidelines: [
31
+ 'Match severity to the actual importance: info for context, warning for potential issues, error for failures',
32
+ 'Always provide actionable guidance in error alerts',
33
+ 'Use titles for complex messages; skip titles for brief one-liners',
34
+ 'Limit to one action per alert to avoid decision paralysis',
35
+ 'Dismissible alerts should only be used for non-critical information',
36
+ ],
37
+ accessibility: [
38
+ 'Uses role="alert" for screen reader announcement',
39
+ 'Error and warning alerts are announced immediately by assistive technology',
40
+ 'Dismiss button must have an accessible label',
41
+ 'Color alone must not convey meaning - icons and text reinforce severity',
42
+ ],
43
+ },
44
+
45
+ props: {
46
+ children: {
47
+ type: 'node',
48
+ description: 'Alert message content',
49
+ required: true,
50
+ },
51
+ title: {
52
+ type: 'string',
53
+ description: 'Optional bold title displayed above the message',
54
+ },
55
+ severity: {
56
+ type: 'enum',
57
+ description: 'Visual severity level',
58
+ values: ['info', 'success', 'warning', 'error'],
59
+ default: 'info',
60
+ },
61
+ dismissible: {
62
+ type: 'boolean',
63
+ description: 'Whether the alert shows a dismiss button',
64
+ default: 'false',
65
+ },
66
+ onDismiss: {
67
+ type: 'function',
68
+ description: 'Callback when the alert is dismissed',
69
+ },
70
+ action: {
71
+ type: 'custom',
72
+ description: 'Optional action button: { label: string, onClick: () => void }',
73
+ },
74
+ },
75
+
76
+ relations: [
77
+ { component: 'Badge', relationship: 'alternative', note: 'Use Badge for compact, inline status labels' },
78
+ { component: 'Toast', relationship: 'alternative', note: 'Use Toast for transient notifications that auto-dismiss' },
79
+ { component: 'Dialog', relationship: 'sibling', note: 'Use Dialog for blocking confirmations' },
80
+ ],
81
+
82
+ contract: {
83
+ propsSummary: [
84
+ 'children: ReactNode - message content (required)',
85
+ 'title: string - optional bold heading',
86
+ 'severity: info|success|warning|error - visual severity',
87
+ 'dismissible: boolean - shows dismiss button',
88
+ 'action: { label, onClick } - optional action button',
89
+ ],
90
+ scenarioTags: [
91
+ 'feedback.message',
92
+ 'feedback.error',
93
+ 'feedback.success',
94
+ 'feedback.warning',
95
+ 'content.notification',
96
+ ],
97
+ a11yRules: ['A11Y_ALERT_ROLE', 'A11Y_ALERT_DISMISS', 'A11Y_ALERT_CONTRAST'],
98
+ validationBans: [
99
+ { pattern: 'severity="error"(?!.*children)', reason: 'Error alerts must include helpful message text' },
100
+ ],
101
+ },
102
+
103
+ variants: [
104
+ {
105
+ name: 'Info',
106
+ description: 'Informational context for the user',
107
+ render: () => (
108
+ <Alert severity="info">
109
+ Your session will expire in 15 minutes. Save your work to avoid losing changes.
110
+ </Alert>
111
+ ),
112
+ },
113
+ {
114
+ name: 'Success',
115
+ description: 'Positive confirmation of completed action',
116
+ render: () => (
117
+ <Alert severity="success" title="Payment processed">
118
+ Your order #12345 has been confirmed. You will receive a confirmation email shortly.
119
+ </Alert>
120
+ ),
121
+ },
122
+ {
123
+ name: 'Warning',
124
+ description: 'Caution about potential issues',
125
+ render: () => (
126
+ <Alert severity="warning" title="Storage almost full">
127
+ You have used 90% of your storage quota. Consider deleting unused files.
128
+ </Alert>
129
+ ),
130
+ },
131
+ {
132
+ name: 'Error',
133
+ description: 'Error state requiring user attention',
134
+ render: () => (
135
+ <Alert severity="error" title="Upload failed">
136
+ The file could not be uploaded. Check your connection and try again.
137
+ </Alert>
138
+ ),
139
+ },
140
+ {
141
+ name: 'With Action',
142
+ description: 'Alert with an actionable button',
143
+ render: () => (
144
+ <Alert
145
+ severity="warning"
146
+ title="Update available"
147
+ action={{ label: 'Update now', onClick: () => {} }}
148
+ >
149
+ A new version is available with important security fixes.
150
+ </Alert>
151
+ ),
152
+ },
153
+ {
154
+ name: 'Dismissible',
155
+ description: 'Alert that can be closed by the user',
156
+ render: () => (
157
+ <Alert severity="info" dismissible>
158
+ You can customize your notification preferences in Settings.
159
+ </Alert>
160
+ ),
161
+ },
162
+ ],
163
+ });
@@ -0,0 +1,116 @@
1
+ @use '../../tokens/variables' as *;
2
+ @use '../../tokens/mixins' as *;
3
+
4
+ .alert {
5
+ display: flex;
6
+ align-items: flex-start;
7
+ gap: var(--fui-space-3, $fui-space-3);
8
+ padding: var(--fui-space-3, $fui-space-3) var(--fui-space-4, $fui-space-4);
9
+ border-radius: var(--fui-radius-lg, $fui-radius-lg);
10
+ font-family: var(--fui-font-sans, $fui-font-sans);
11
+ font-size: var(--fui-font-size-sm, $fui-font-size-sm);
12
+ line-height: var(--fui-line-height-normal, $fui-line-height-normal);
13
+ }
14
+
15
+ // Severity variants
16
+ .info {
17
+ background-color: var(--fui-color-info-bg, $fui-color-info-bg);
18
+ border: 1px solid var(--fui-color-info, $fui-color-info);
19
+ color: var(--fui-color-info, $fui-color-info);
20
+ }
21
+
22
+ .success {
23
+ background-color: var(--fui-color-success-bg, $fui-color-success-bg);
24
+ border: 1px solid var(--fui-color-success, $fui-color-success);
25
+ color: var(--fui-color-success, $fui-color-success);
26
+ }
27
+
28
+ .warning {
29
+ background-color: var(--fui-color-warning-bg, $fui-color-warning-bg);
30
+ border: 1px solid var(--fui-color-warning, $fui-color-warning);
31
+ color: var(--fui-color-warning, $fui-color-warning);
32
+ }
33
+
34
+ .error {
35
+ background-color: var(--fui-color-danger-bg, $fui-color-danger-bg);
36
+ border: 1px solid var(--fui-color-danger, $fui-color-danger);
37
+ color: var(--fui-color-danger, $fui-color-danger);
38
+ }
39
+
40
+ .icon {
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ width: 20px;
45
+ height: 20px;
46
+ border-radius: 50%;
47
+ font-size: var(--fui-font-size-xs, $fui-font-size-xs);
48
+ font-weight: var(--fui-font-weight-semibold, $fui-font-weight-semibold);
49
+ flex-shrink: 0;
50
+ margin-top: 1px;
51
+ color: var(--fui-text-inverse, $fui-text-inverse);
52
+ }
53
+
54
+ .info .icon {
55
+ background-color: var(--fui-color-info, $fui-color-info);
56
+ }
57
+
58
+ .success .icon {
59
+ background-color: var(--fui-color-success, $fui-color-success);
60
+ }
61
+
62
+ .warning .icon {
63
+ background-color: var(--fui-color-warning, $fui-color-warning);
64
+ }
65
+
66
+ .error .icon {
67
+ background-color: var(--fui-color-danger, $fui-color-danger);
68
+ }
69
+
70
+ .body {
71
+ flex: 1;
72
+ min-width: 0;
73
+ }
74
+
75
+ .title {
76
+ font-weight: var(--fui-font-weight-semibold, $fui-font-weight-semibold);
77
+ margin-bottom: 2px;
78
+ }
79
+
80
+ .message {
81
+ opacity: 0.9;
82
+ }
83
+
84
+ .action {
85
+ @include button-reset;
86
+
87
+ margin-top: var(--fui-space-2, $fui-space-2);
88
+ font-weight: var(--fui-font-weight-semibold, $fui-font-weight-semibold);
89
+ font-size: var(--fui-font-size-xs, $fui-font-size-xs);
90
+ color: inherit;
91
+ text-decoration: underline;
92
+
93
+ &:hover {
94
+ opacity: 0.8;
95
+ }
96
+
97
+ &:focus-visible {
98
+ @include focus-ring;
99
+ }
100
+ }
101
+
102
+ .dismiss {
103
+ @include button-reset;
104
+ @include interactive-base;
105
+
106
+ padding: 0 var(--fui-space-1, $fui-space-1);
107
+ font-size: 18px;
108
+ color: inherit;
109
+ opacity: 0.6;
110
+ line-height: 1;
111
+ border-radius: var(--fui-radius-sm, $fui-radius-sm);
112
+
113
+ &:hover {
114
+ opacity: 1;
115
+ }
116
+ }
@@ -0,0 +1,95 @@
1
+ import * as React from 'react';
2
+ import { Button as BaseButton } from '@base-ui/react/button';
3
+ import styles from './Alert.module.scss';
4
+ // Import globals to ensure CSS variables are defined
5
+ import '../../styles/globals.scss';
6
+
7
+ export interface AlertProps {
8
+ children: React.ReactNode;
9
+ title?: string;
10
+ severity?: 'info' | 'success' | 'warning' | 'error';
11
+ dismissible?: boolean;
12
+ onDismiss?: () => void;
13
+ action?: {
14
+ label: string;
15
+ onClick: () => void;
16
+ };
17
+ className?: string;
18
+ }
19
+
20
+ const severityIcons: Record<string, string> = {
21
+ info: 'i',
22
+ success: '\u2713',
23
+ warning: '!',
24
+ error: '\u2717',
25
+ };
26
+
27
+ export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
28
+ function Alert(
29
+ {
30
+ children,
31
+ title,
32
+ severity = 'info',
33
+ dismissible = false,
34
+ onDismiss,
35
+ action,
36
+ className,
37
+ },
38
+ ref
39
+ ) {
40
+ const [dismissed, setDismissed] = React.useState(false);
41
+ const titleId = React.useId();
42
+ const descId = React.useId();
43
+
44
+ if (dismissed) return null;
45
+
46
+ const handleDismiss = () => {
47
+ setDismissed(true);
48
+ onDismiss?.();
49
+ };
50
+
51
+ const classes = [styles.alert, styles[severity], className]
52
+ .filter(Boolean)
53
+ .join(' ');
54
+
55
+ return (
56
+ <div
57
+ ref={ref}
58
+ role="alert"
59
+ aria-labelledby={title ? titleId : undefined}
60
+ aria-describedby={descId}
61
+ className={classes}
62
+ >
63
+ <span className={styles.icon} aria-hidden="true">
64
+ {severityIcons[severity]}
65
+ </span>
66
+
67
+ <div className={styles.body}>
68
+ {title && (
69
+ <div id={titleId} className={styles.title}>
70
+ {title}
71
+ </div>
72
+ )}
73
+ <div id={descId} className={styles.message}>
74
+ {children}
75
+ </div>
76
+ {action && (
77
+ <BaseButton onClick={action.onClick} className={styles.action}>
78
+ {action.label}
79
+ </BaseButton>
80
+ )}
81
+ </div>
82
+
83
+ {dismissible && (
84
+ <BaseButton
85
+ onClick={handleDismiss}
86
+ aria-label="Dismiss alert"
87
+ className={styles.dismiss}
88
+ >
89
+ &times;
90
+ </BaseButton>
91
+ )}
92
+ </div>
93
+ );
94
+ }
95
+ );
@@ -0,0 +1,147 @@
1
+ import React from 'react';
2
+ import { defineSegment } from '@fragments/core';
3
+ import { Avatar } from './index.js';
4
+
5
+ export default defineSegment({
6
+ component: Avatar,
7
+
8
+ meta: {
9
+ name: 'Avatar',
10
+ description: 'Visual representation of a user or entity',
11
+ category: 'data-display',
12
+ status: 'stable',
13
+ tags: ['user', 'profile', 'image', 'identity'],
14
+ },
15
+
16
+ usage: {
17
+ when: [
18
+ 'Displaying user profile pictures',
19
+ 'Showing team member lists',
20
+ 'Representing entities in lists or cards',
21
+ 'User identification in comments or messages',
22
+ ],
23
+ whenNot: [
24
+ 'Decorative images (use Image)',
25
+ 'Logo display (use Logo component)',
26
+ 'Large profile headers (use custom layout)',
27
+ ],
28
+ guidelines: [
29
+ 'Always provide alt text or name for accessibility',
30
+ 'Use consistent sizes within the same context',
31
+ 'Provide fallback initials when image may not load',
32
+ 'Use Avatar.Group for multiple avatars in a row',
33
+ ],
34
+ accessibility: [
35
+ 'Include meaningful alt text describing the user',
36
+ 'Initials should be derived from name for screen readers',
37
+ 'Decorative avatars should have empty alt',
38
+ ],
39
+ },
40
+
41
+ props: {
42
+ src: {
43
+ type: 'string',
44
+ description: 'Image source URL',
45
+ },
46
+ alt: {
47
+ type: 'string',
48
+ description: 'Alt text for the image',
49
+ },
50
+ name: {
51
+ type: 'string',
52
+ description: 'Full name - used to generate initials',
53
+ },
54
+ initials: {
55
+ type: 'string',
56
+ description: 'Fallback initials (1-2 characters)',
57
+ },
58
+ size: {
59
+ type: 'enum',
60
+ values: ['xs', 'sm', 'md', 'lg', 'xl'],
61
+ default: 'md',
62
+ description: 'Size variant',
63
+ },
64
+ shape: {
65
+ type: 'enum',
66
+ values: ['circle', 'square'],
67
+ default: 'circle',
68
+ description: 'Shape variant',
69
+ },
70
+ },
71
+
72
+ relations: [
73
+ {
74
+ component: 'AvatarGroup',
75
+ relationship: 'parent',
76
+ note: 'Use Avatar.Group for stacked avatar displays',
77
+ },
78
+ ],
79
+
80
+ contract: {
81
+ propsSummary: [
82
+ 'src: string - image URL',
83
+ 'name: string - used for initials fallback',
84
+ 'size: xs|sm|md|lg|xl (default: md)',
85
+ 'shape: circle|square (default: circle)',
86
+ ],
87
+ scenarioTags: [
88
+ 'profile.display',
89
+ 'list.user',
90
+ 'comment.author',
91
+ ],
92
+ a11yRules: [
93
+ 'A11Y_IMG_ALT',
94
+ ],
95
+ bans: [],
96
+ },
97
+
98
+ variants: [
99
+ {
100
+ name: 'Default',
101
+ description: 'Avatar with image',
102
+ render: () => (
103
+ <Avatar
104
+ src="https://i.pravatar.cc/150?u=jane"
105
+ alt="Jane Doe"
106
+ name="Jane Doe"
107
+ />
108
+ ),
109
+ },
110
+ {
111
+ name: 'With Initials',
112
+ description: 'Fallback when no image is provided',
113
+ render: () => <Avatar name="John Smith" />,
114
+ },
115
+ {
116
+ name: 'Sizes',
117
+ description: 'Available size options',
118
+ render: () => (
119
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
120
+ <Avatar name="XS" size="xs" />
121
+ <Avatar name="SM" size="sm" />
122
+ <Avatar name="MD" size="md" />
123
+ <Avatar name="LG" size="lg" />
124
+ <Avatar name="XL" size="xl" />
125
+ </div>
126
+ ),
127
+ },
128
+ {
129
+ name: 'Square Shape',
130
+ description: 'Square variant for app icons or brands',
131
+ render: () => <Avatar name="App" shape="square" />,
132
+ },
133
+ {
134
+ name: 'Group',
135
+ description: 'Multiple avatars stacked together',
136
+ render: () => (
137
+ <Avatar.Group max={3} size="md">
138
+ <Avatar name="Alice Johnson" />
139
+ <Avatar name="Bob Smith" />
140
+ <Avatar name="Carol Williams" />
141
+ <Avatar name="David Brown" />
142
+ <Avatar name="Eve Davis" />
143
+ </Avatar.Group>
144
+ ),
145
+ },
146
+ ],
147
+ });