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