@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,185 @@
1
+ import * as React from 'react';
2
+ import styles from './Skeleton.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 SkeletonVariant =
11
+ | 'text' // Single line of text, height: 1em
12
+ | 'heading' // Heading text, height: 1.5em
13
+ | 'avatar' // Circular, uses size prop
14
+ | 'button' // Button shape, uses size prop
15
+ | 'input' // Form input height
16
+ | 'rect'; // Rectangle, requires explicit dimensions or fill
17
+
18
+ export type SkeletonSize = 'sm' | 'md' | 'lg';
19
+
20
+ export interface SkeletonProps {
21
+ /**
22
+ * Semantic variant that auto-sizes based on design tokens.
23
+ * @default 'rect'
24
+ */
25
+ variant?: SkeletonVariant;
26
+ /**
27
+ * Size variant for avatar/button. Ignored for text/heading/input.
28
+ * @default 'md'
29
+ */
30
+ size?: SkeletonSize;
31
+ /**
32
+ * Width in pixels or CSS value. Auto-determined for most variants.
33
+ */
34
+ width?: number | string;
35
+ /**
36
+ * Height in pixels or CSS value. Auto-determined for semantic variants.
37
+ */
38
+ height?: number | string;
39
+ /**
40
+ * Fill parent container (100% width and height).
41
+ * Useful when parent has explicit dimensions.
42
+ */
43
+ fill?: boolean;
44
+ /**
45
+ * Border radius override. Auto-determined for most variants.
46
+ */
47
+ radius?: 'none' | 'sm' | 'md' | 'lg' | 'full';
48
+ /**
49
+ * Disable animation for reduced motion preference.
50
+ * @default false
51
+ */
52
+ static?: boolean;
53
+ /** Additional class name */
54
+ className?: string;
55
+ }
56
+
57
+ export interface SkeletonTextProps {
58
+ /** Number of text lines to render */
59
+ lines?: number;
60
+ /**
61
+ * Width of last line as percentage.
62
+ * Creates natural paragraph appearance.
63
+ * @default 80
64
+ */
65
+ lastLineWidth?: number;
66
+ /** Gap between lines. Uses spacing tokens. */
67
+ gap?: 'sm' | 'md';
68
+ /** Additional class name */
69
+ className?: string;
70
+ }
71
+
72
+ // ============================================
73
+ // Component
74
+ // ============================================
75
+
76
+ const SkeletonBase = React.forwardRef<HTMLDivElement, SkeletonProps>(
77
+ function SkeletonBase(
78
+ {
79
+ variant = 'rect',
80
+ size = 'md',
81
+ width,
82
+ height,
83
+ fill = false,
84
+ radius,
85
+ static: isStatic = false,
86
+ className,
87
+ },
88
+ ref
89
+ ) {
90
+ const classes = [
91
+ styles.skeleton,
92
+ styles[variant],
93
+ variant === 'avatar' && styles[`avatar-${size}`],
94
+ variant === 'button' && styles[`button-${size}`],
95
+ fill && styles.fill,
96
+ radius && styles[`radius-${radius}`],
97
+ isStatic && styles.static,
98
+ className,
99
+ ].filter(Boolean).join(' ');
100
+
101
+ const style: React.CSSProperties = {};
102
+
103
+ if (width !== undefined) {
104
+ style.width = typeof width === 'number' ? `${width}px` : width;
105
+ }
106
+ if (height !== undefined) {
107
+ style.height = typeof height === 'number' ? `${height}px` : height;
108
+ }
109
+
110
+ return (
111
+ <div
112
+ ref={ref}
113
+ className={classes}
114
+ style={Object.keys(style).length > 0 ? style : undefined}
115
+ aria-hidden="true"
116
+ />
117
+ );
118
+ }
119
+ );
120
+
121
+ // ============================================
122
+ // Skeleton.Text - Multi-line text skeleton
123
+ // ============================================
124
+
125
+ function SkeletonText({
126
+ lines = 3,
127
+ lastLineWidth = 80,
128
+ gap = 'sm',
129
+ className,
130
+ }: SkeletonTextProps) {
131
+ const containerClasses = [
132
+ styles.textContainer,
133
+ styles[`gap-${gap}`],
134
+ className,
135
+ ].filter(Boolean).join(' ');
136
+
137
+ return (
138
+ <div className={containerClasses} aria-hidden="true">
139
+ {Array.from({ length: lines }, (_, i) => {
140
+ const isLast = i === lines - 1;
141
+ return (
142
+ <div
143
+ key={i}
144
+ className={styles.textLine}
145
+ style={isLast && lines > 1 ? { width: `${lastLineWidth}%` } : undefined}
146
+ />
147
+ );
148
+ })}
149
+ </div>
150
+ );
151
+ }
152
+
153
+ // ============================================
154
+ // Skeleton.Circle - Shorthand for avatar variant
155
+ // ============================================
156
+
157
+ function SkeletonCircle({
158
+ size = 'md',
159
+ className,
160
+ }: {
161
+ size?: SkeletonSize | number;
162
+ className?: string;
163
+ }) {
164
+ if (typeof size === 'number') {
165
+ return (
166
+ <SkeletonBase
167
+ variant="rect"
168
+ width={size}
169
+ height={size}
170
+ radius="full"
171
+ className={className}
172
+ />
173
+ );
174
+ }
175
+ return <SkeletonBase variant="avatar" size={size} className={className} />;
176
+ }
177
+
178
+ // ============================================
179
+ // Compound Component
180
+ // ============================================
181
+
182
+ export const Skeleton = Object.assign(SkeletonBase, {
183
+ Text: SkeletonText,
184
+ Circle: SkeletonCircle,
185
+ });
@@ -0,0 +1,193 @@
1
+ import React from 'react';
2
+ import { defineSegment } from '@fragments/core';
3
+ import { Table, createColumns } from './index.js';
4
+ import { Badge } from '../Badge/index.js';
5
+
6
+ // Sample data types
7
+ interface User {
8
+ id: string;
9
+ name: string;
10
+ email: string;
11
+ status: 'active' | 'inactive' | 'pending';
12
+ role: string;
13
+ }
14
+
15
+ const sampleUsers: User[] = [
16
+ { id: '1', name: 'Alice Johnson', email: 'alice@example.com', status: 'active', role: 'Admin' },
17
+ { id: '2', name: 'Bob Smith', email: 'bob@example.com', status: 'active', role: 'Editor' },
18
+ { id: '3', name: 'Carol Williams', email: 'carol@example.com', status: 'pending', role: 'Viewer' },
19
+ { id: '4', name: 'David Brown', email: 'david@example.com', status: 'inactive', role: 'Editor' },
20
+ ];
21
+
22
+ const columns = createColumns<User>([
23
+ { key: 'name', header: 'Name' },
24
+ { key: 'email', header: 'Email' },
25
+ {
26
+ key: 'status',
27
+ header: 'Status',
28
+ cell: (row) => (
29
+ <Badge
30
+ variant={row.status === 'active' ? 'success' : row.status === 'pending' ? 'warning' : 'default'}
31
+ dot
32
+ >
33
+ {row.status}
34
+ </Badge>
35
+ ),
36
+ },
37
+ { key: 'role', header: 'Role' },
38
+ ]);
39
+
40
+ export default defineSegment({
41
+ component: Table,
42
+
43
+ meta: {
44
+ name: 'Table',
45
+ description: 'Data table with sorting and row selection. Use for displaying structured data that needs to be scanned, compared, or acted upon.',
46
+ category: 'data-display',
47
+ status: 'stable',
48
+ tags: ['table', 'data', 'grid', 'list', 'sorting'],
49
+ since: '0.1.0',
50
+ },
51
+
52
+ usage: {
53
+ when: [
54
+ 'Displaying structured, tabular data',
55
+ 'Data that users need to scan and compare',
56
+ 'Lists with multiple attributes per item',
57
+ 'Data that needs sorting or selection',
58
+ ],
59
+ whenNot: [
60
+ 'Simple lists (use List component)',
61
+ 'Card-based layouts (use CardGrid)',
62
+ 'Heavily interactive data (consider DataGrid)',
63
+ 'Small screens (consider card or list view)',
64
+ ],
65
+ guidelines: [
66
+ 'Keep columns to a reasonable number (5-7 max)',
67
+ 'Use consistent alignment (numbers right, text left)',
68
+ 'Provide meaningful empty states',
69
+ 'Consider mobile responsiveness',
70
+ ],
71
+ accessibility: [
72
+ 'Proper table semantics with headers',
73
+ 'Sortable columns are keyboard accessible',
74
+ 'Row selection is properly announced',
75
+ ],
76
+ },
77
+
78
+ props: {
79
+ columns: {
80
+ type: 'array',
81
+ description: 'Column definitions',
82
+ required: true,
83
+ },
84
+ data: {
85
+ type: 'array',
86
+ description: 'Data rows to display',
87
+ required: true,
88
+ },
89
+ sortable: {
90
+ type: 'boolean',
91
+ description: 'Enable column sorting',
92
+ default: 'false',
93
+ },
94
+ selectable: {
95
+ type: 'boolean',
96
+ description: 'Enable row selection',
97
+ default: 'false',
98
+ },
99
+ onRowClick: {
100
+ type: 'function',
101
+ description: 'Handler for row clicks',
102
+ },
103
+ emptyMessage: {
104
+ type: 'string',
105
+ description: 'Message when no data',
106
+ default: 'No data available',
107
+ },
108
+ size: {
109
+ type: 'enum',
110
+ description: 'Table density',
111
+ values: ['sm', 'md'],
112
+ default: 'md',
113
+ },
114
+ },
115
+
116
+ relations: [
117
+ { component: 'EmptyState', relationship: 'sibling', note: 'Use EmptyState for empty table states' },
118
+ { component: 'Badge', relationship: 'sibling', note: 'Use Badge for status columns' },
119
+ ],
120
+
121
+ contract: {
122
+ propsSummary: [
123
+ 'columns: ColumnDef[] - column definitions',
124
+ 'data: T[] - row data array',
125
+ 'sortable: boolean - enable sorting',
126
+ 'selectable: boolean - enable row selection',
127
+ 'size: sm|md - table density',
128
+ ],
129
+ scenarioTags: [
130
+ 'data.table',
131
+ 'display.list',
132
+ 'data.grid',
133
+ ],
134
+ a11yRules: ['A11Y_TABLE_HEADERS', 'A11Y_TABLE_SORT'],
135
+ },
136
+
137
+ variants: [
138
+ {
139
+ name: 'Default',
140
+ description: 'Basic data table',
141
+ render: () => (
142
+ <Table
143
+ columns={columns}
144
+ data={sampleUsers}
145
+ />
146
+ ),
147
+ },
148
+ {
149
+ name: 'Sortable',
150
+ description: 'Table with sortable columns',
151
+ render: () => (
152
+ <Table
153
+ columns={columns}
154
+ data={sampleUsers}
155
+ sortable
156
+ />
157
+ ),
158
+ },
159
+ {
160
+ name: 'Clickable Rows',
161
+ description: 'Table with clickable rows',
162
+ render: () => (
163
+ <Table
164
+ columns={columns}
165
+ data={sampleUsers}
166
+ onRowClick={(row) => alert(`Clicked: ${row.name}`)}
167
+ />
168
+ ),
169
+ },
170
+ {
171
+ name: 'Compact',
172
+ description: 'Smaller, denser table',
173
+ render: () => (
174
+ <Table
175
+ columns={columns}
176
+ data={sampleUsers}
177
+ size="sm"
178
+ />
179
+ ),
180
+ },
181
+ {
182
+ name: 'Empty State',
183
+ description: 'Table with no data',
184
+ render: () => (
185
+ <Table
186
+ columns={columns}
187
+ data={[]}
188
+ emptyMessage="No users found"
189
+ />
190
+ ),
191
+ },
192
+ ],
193
+ });
@@ -0,0 +1,152 @@
1
+ @use '../../tokens/variables' as *;
2
+ @use '../../tokens/mixins' as *;
3
+
4
+ .wrapper {
5
+ overflow-x: auto;
6
+ -webkit-overflow-scrolling: touch;
7
+ }
8
+
9
+ .table {
10
+ width: 100%;
11
+ border-collapse: collapse;
12
+ font-family: var(--fui-font-sans, $fui-font-sans);
13
+ -webkit-font-smoothing: antialiased;
14
+ -moz-osx-font-smoothing: grayscale;
15
+ }
16
+
17
+ // Size variants
18
+ .sm {
19
+ .th,
20
+ .td {
21
+ padding: var(--fui-space-2, $fui-space-2) var(--fui-space-3, $fui-space-3);
22
+ font-size: var(--fui-font-size-xs, $fui-font-size-xs);
23
+ }
24
+ }
25
+
26
+ .md {
27
+ .th,
28
+ .td {
29
+ padding: var(--fui-space-3, $fui-space-3) var(--fui-space-4, $fui-space-4);
30
+ font-size: var(--fui-font-size-sm, $fui-font-size-sm);
31
+ }
32
+ }
33
+
34
+ // Header
35
+ .thead {
36
+ position: sticky;
37
+ top: 0;
38
+ z-index: 1;
39
+ background-color: var(--fui-bg-secondary, $fui-bg-secondary);
40
+ }
41
+
42
+ .headerRow {
43
+ border-bottom: 1px solid var(--fui-border, $fui-border);
44
+ }
45
+
46
+ .th {
47
+ text-align: left;
48
+ font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
49
+ color: var(--fui-text-secondary, $fui-text-secondary);
50
+ white-space: nowrap;
51
+ vertical-align: middle;
52
+ user-select: none;
53
+
54
+ &:first-child {
55
+ border-top-left-radius: var(--fui-radius-md, $fui-radius-md);
56
+ }
57
+
58
+ &:last-child {
59
+ border-top-right-radius: var(--fui-radius-md, $fui-radius-md);
60
+ }
61
+ }
62
+
63
+ .headerContent {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: var(--fui-space-1, $fui-space-1);
67
+ }
68
+
69
+ .sortable {
70
+ cursor: pointer;
71
+ transition: color var(--fui-transition-fast, $fui-transition-fast);
72
+
73
+ &:hover {
74
+ color: var(--fui-text-primary, $fui-text-primary);
75
+ }
76
+ }
77
+
78
+ .sortIndicator {
79
+ display: inline-flex;
80
+ align-items: center;
81
+ color: var(--fui-text-tertiary, $fui-text-tertiary);
82
+ flex-shrink: 0;
83
+
84
+ .sortable:hover & {
85
+ color: var(--fui-text-secondary, $fui-text-secondary);
86
+ }
87
+ }
88
+
89
+ // Body
90
+ .tbody {
91
+ background-color: var(--fui-bg-primary, $fui-bg-primary);
92
+ }
93
+
94
+ .row {
95
+ border-bottom: 1px solid var(--fui-border, $fui-border);
96
+ transition: background-color var(--fui-transition-fast, $fui-transition-fast);
97
+
98
+ &:last-child {
99
+ border-bottom: none;
100
+ }
101
+ }
102
+
103
+ .clickable {
104
+ cursor: pointer;
105
+
106
+ &:hover {
107
+ background-color: var(--fui-bg-hover, $fui-bg-hover);
108
+ }
109
+
110
+ &:active {
111
+ background-color: var(--fui-bg-active, $fui-bg-active);
112
+ }
113
+ }
114
+
115
+ .selected {
116
+ background-color: var(--fui-color-accent, $fui-color-accent);
117
+ background-color: rgba($fui-color-accent, 0.08);
118
+
119
+ &:hover {
120
+ background-color: rgba($fui-color-accent, 0.12);
121
+ }
122
+ }
123
+
124
+ .td {
125
+ color: var(--fui-text-primary, $fui-text-primary);
126
+ vertical-align: middle;
127
+ line-height: var(--fui-line-height-normal, $fui-line-height-normal);
128
+ }
129
+
130
+ // Empty state
131
+ .emptyState {
132
+ display: flex;
133
+ align-items: center;
134
+ justify-content: center;
135
+ padding: var(--fui-space-12, $fui-space-12) var(--fui-space-6, $fui-space-6);
136
+ }
137
+
138
+ .emptyMessage {
139
+ font-family: var(--fui-font-sans, $fui-font-sans);
140
+ font-size: var(--fui-font-size-sm, $fui-font-size-sm);
141
+ color: var(--fui-text-tertiary, $fui-text-tertiary);
142
+ }
143
+
144
+ // Responsive: allow horizontal scroll on small screens
145
+ @media (max-width: 640px) {
146
+ .wrapper {
147
+ margin-left: calc(-1 * var(--fui-space-4, $fui-space-4));
148
+ margin-right: calc(-1 * var(--fui-space-4, $fui-space-4));
149
+ padding-left: var(--fui-space-4, $fui-space-4);
150
+ padding-right: var(--fui-space-4, $fui-space-4);
151
+ }
152
+ }