@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,159 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineSegment } from '@fragments/core';
|
|
3
|
+
import { Button } from './index.js';
|
|
4
|
+
|
|
5
|
+
export default defineSegment({
|
|
6
|
+
component: Button,
|
|
7
|
+
|
|
8
|
+
meta: {
|
|
9
|
+
name: 'Button',
|
|
10
|
+
description: 'Interactive element for user actions and form submissions',
|
|
11
|
+
category: 'actions',
|
|
12
|
+
status: 'stable',
|
|
13
|
+
tags: ['action', 'button', 'form', 'interactive'],
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
usage: {
|
|
17
|
+
when: [
|
|
18
|
+
'Triggering an action (save, submit, delete, etc.)',
|
|
19
|
+
'Form submission',
|
|
20
|
+
'Opening dialogs or menus',
|
|
21
|
+
'Navigation when action context is needed',
|
|
22
|
+
],
|
|
23
|
+
whenNot: [
|
|
24
|
+
'Simple navigation (use Link)',
|
|
25
|
+
'Toggling state (use Switch or Checkbox)',
|
|
26
|
+
'Selecting from options (use Select or RadioGroup)',
|
|
27
|
+
],
|
|
28
|
+
guidelines: [
|
|
29
|
+
'Use Primary for the main action in a context',
|
|
30
|
+
'Only one Primary button per section/form',
|
|
31
|
+
'Use Danger variant for destructive actions',
|
|
32
|
+
'Loading state should disable the button',
|
|
33
|
+
],
|
|
34
|
+
accessibility: [
|
|
35
|
+
'Button text should describe the action',
|
|
36
|
+
'Avoid generic labels like "Click here"',
|
|
37
|
+
'Icon-only buttons need aria-label',
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
props: {
|
|
42
|
+
children: {
|
|
43
|
+
type: 'node',
|
|
44
|
+
required: true,
|
|
45
|
+
description: 'Button label content',
|
|
46
|
+
},
|
|
47
|
+
variant: {
|
|
48
|
+
type: 'enum',
|
|
49
|
+
values: ['primary', 'secondary', 'ghost', 'danger'],
|
|
50
|
+
default: 'primary',
|
|
51
|
+
description: 'Visual style variant',
|
|
52
|
+
constraints: ['Only one primary button per context'],
|
|
53
|
+
},
|
|
54
|
+
size: {
|
|
55
|
+
type: 'enum',
|
|
56
|
+
values: ['sm', 'md', 'lg'],
|
|
57
|
+
default: 'md',
|
|
58
|
+
description: 'Button size',
|
|
59
|
+
},
|
|
60
|
+
disabled: {
|
|
61
|
+
type: 'boolean',
|
|
62
|
+
default: false,
|
|
63
|
+
description: 'Whether the button is disabled',
|
|
64
|
+
},
|
|
65
|
+
onClick: {
|
|
66
|
+
type: 'function',
|
|
67
|
+
description: 'Click handler',
|
|
68
|
+
},
|
|
69
|
+
type: {
|
|
70
|
+
type: 'enum',
|
|
71
|
+
values: ['button', 'submit', 'reset'],
|
|
72
|
+
default: 'button',
|
|
73
|
+
description: 'HTML button type attribute',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
relations: [
|
|
78
|
+
{
|
|
79
|
+
component: 'Link',
|
|
80
|
+
relationship: 'alternative',
|
|
81
|
+
note: 'Use Link for navigation without action context',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
component: 'IconButton',
|
|
85
|
+
relationship: 'alternative',
|
|
86
|
+
note: 'Use IconButton for icon-only actions',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
component: 'ButtonGroup',
|
|
90
|
+
relationship: 'parent',
|
|
91
|
+
note: 'Use ButtonGroup for related action sets',
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
|
|
95
|
+
contract: {
|
|
96
|
+
propsSummary: [
|
|
97
|
+
'variant: primary|secondary|ghost|danger (default: primary)',
|
|
98
|
+
'size: sm|md|lg (default: md)',
|
|
99
|
+
'disabled: boolean - disables interaction',
|
|
100
|
+
'type: button|submit|reset (default: button)',
|
|
101
|
+
'onClick: () => void - action handler',
|
|
102
|
+
],
|
|
103
|
+
scenarioTags: [
|
|
104
|
+
'form.submit',
|
|
105
|
+
'action.primary',
|
|
106
|
+
'action.secondary',
|
|
107
|
+
'action.destructive',
|
|
108
|
+
],
|
|
109
|
+
a11yRules: [
|
|
110
|
+
'A11Y_BTN_LABEL',
|
|
111
|
+
'A11Y_BTN_FOCUS',
|
|
112
|
+
],
|
|
113
|
+
bans: [
|
|
114
|
+
{
|
|
115
|
+
pattern: '<button.*click here',
|
|
116
|
+
message: 'Use descriptive button text instead of "click here"',
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
variants: [
|
|
122
|
+
{
|
|
123
|
+
name: 'Primary',
|
|
124
|
+
description: 'Default action button for primary actions',
|
|
125
|
+
render: () => <Button variant="primary">Save Changes</Button>,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'Secondary',
|
|
129
|
+
description: 'Less prominent action button',
|
|
130
|
+
render: () => <Button variant="secondary">Cancel</Button>,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'Ghost',
|
|
134
|
+
description: 'Minimal visual weight for subtle actions',
|
|
135
|
+
render: () => <Button variant="ghost">Learn More</Button>,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'Danger',
|
|
139
|
+
description: 'Destructive action requiring attention',
|
|
140
|
+
render: () => <Button variant="danger">Delete Item</Button>,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'Sizes',
|
|
144
|
+
description: 'Available size options',
|
|
145
|
+
render: () => (
|
|
146
|
+
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
|
147
|
+
<Button size="sm">Small</Button>
|
|
148
|
+
<Button size="md">Medium</Button>
|
|
149
|
+
<Button size="lg">Large</Button>
|
|
150
|
+
</div>
|
|
151
|
+
),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'Disabled',
|
|
155
|
+
description: 'Non-interactive state',
|
|
156
|
+
render: () => <Button disabled>Cannot Click</Button>,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
@use '../../tokens/mixins' as *;
|
|
3
|
+
|
|
4
|
+
.button {
|
|
5
|
+
@include button-reset;
|
|
6
|
+
@include interactive-base;
|
|
7
|
+
@include text-base;
|
|
8
|
+
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
gap: var(--fui-space-2, $fui-space-2);
|
|
13
|
+
border-radius: var(--fui-radius-md, $fui-radius-md);
|
|
14
|
+
font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
|
|
15
|
+
white-space: nowrap;
|
|
16
|
+
user-select: none;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Sizes
|
|
20
|
+
.sm {
|
|
21
|
+
min-height: var(--fui-button-height-sm, $fui-button-height-sm);
|
|
22
|
+
padding: 0 var(--fui-space-3, $fui-space-3);
|
|
23
|
+
font-size: var(--fui-font-size-xs, $fui-font-size-xs);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.md {
|
|
27
|
+
min-height: var(--fui-button-height-md, $fui-button-height-md);
|
|
28
|
+
padding: 0 var(--fui-space-4, $fui-space-4);
|
|
29
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.lg {
|
|
33
|
+
min-height: var(--fui-button-height-lg, $fui-button-height-lg);
|
|
34
|
+
padding: 0 var(--fui-space-6, $fui-space-6);
|
|
35
|
+
font-size: var(--fui-font-size-base, $fui-font-size-base);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Variants
|
|
39
|
+
.primary {
|
|
40
|
+
background-color: var(--fui-color-accent, $fui-color-accent);
|
|
41
|
+
color: var(--fui-text-inverse, $fui-text-inverse);
|
|
42
|
+
border: 1px solid transparent;
|
|
43
|
+
|
|
44
|
+
&:hover:not(:disabled) {
|
|
45
|
+
background-color: var(--fui-color-accent-hover, $fui-color-accent-hover);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
&:active:not(:disabled),
|
|
49
|
+
&[data-pressed] {
|
|
50
|
+
background-color: var(--fui-color-accent-active, $fui-color-accent-active);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.secondary {
|
|
55
|
+
background-color: var(--fui-bg-secondary, $fui-bg-secondary);
|
|
56
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
57
|
+
border: 1px solid var(--fui-border-strong, $fui-border-strong);
|
|
58
|
+
|
|
59
|
+
&:hover:not(:disabled) {
|
|
60
|
+
background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
&:active:not(:disabled),
|
|
64
|
+
&[data-pressed] {
|
|
65
|
+
background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.ghost {
|
|
70
|
+
background-color: transparent;
|
|
71
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
72
|
+
border: 1px solid transparent;
|
|
73
|
+
|
|
74
|
+
&:hover:not(:disabled) {
|
|
75
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
&:active:not(:disabled),
|
|
79
|
+
&[data-pressed] {
|
|
80
|
+
background-color: var(--fui-bg-active, $fui-bg-active);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.danger {
|
|
85
|
+
background-color: var(--fui-color-danger, $fui-color-danger);
|
|
86
|
+
color: var(--fui-text-inverse, $fui-text-inverse);
|
|
87
|
+
border: 1px solid transparent;
|
|
88
|
+
|
|
89
|
+
&:hover:not(:disabled) {
|
|
90
|
+
background-color: var(--fui-color-danger-hover, $fui-color-danger-hover);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
&:active:not(:disabled),
|
|
94
|
+
&[data-pressed] {
|
|
95
|
+
background-color: var(--fui-color-danger-hover, $fui-color-danger-hover);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Button as BaseButton } from '@base-ui/react/button';
|
|
3
|
+
import styles from './Button.module.scss';
|
|
4
|
+
// Import globals to ensure CSS variables are defined
|
|
5
|
+
import '../../styles/globals.scss';
|
|
6
|
+
|
|
7
|
+
export interface ButtonProps {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
|
10
|
+
size?: 'sm' | 'md' | 'lg';
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
|
13
|
+
type?: 'button' | 'submit' | 'reset';
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
18
|
+
function Button(
|
|
19
|
+
{
|
|
20
|
+
children,
|
|
21
|
+
variant = 'primary',
|
|
22
|
+
size = 'md',
|
|
23
|
+
disabled = false,
|
|
24
|
+
onClick,
|
|
25
|
+
type = 'button',
|
|
26
|
+
className,
|
|
27
|
+
},
|
|
28
|
+
ref
|
|
29
|
+
) {
|
|
30
|
+
const classNames = [
|
|
31
|
+
styles.button,
|
|
32
|
+
styles[size],
|
|
33
|
+
styles[variant],
|
|
34
|
+
className,
|
|
35
|
+
]
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.join(' ');
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<BaseButton
|
|
41
|
+
ref={ref}
|
|
42
|
+
type={type}
|
|
43
|
+
disabled={disabled}
|
|
44
|
+
onClick={onClick}
|
|
45
|
+
className={classNames}
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
</BaseButton>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineSegment } from '@fragments/core';
|
|
3
|
+
import { Card } from './index.js';
|
|
4
|
+
|
|
5
|
+
export default defineSegment({
|
|
6
|
+
component: Card,
|
|
7
|
+
|
|
8
|
+
meta: {
|
|
9
|
+
name: 'Card',
|
|
10
|
+
description: 'Container for grouping related content',
|
|
11
|
+
category: 'layout',
|
|
12
|
+
status: 'stable',
|
|
13
|
+
tags: ['container', 'layout', 'surface'],
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
usage: {
|
|
17
|
+
when: [
|
|
18
|
+
'Grouping related pieces of content together',
|
|
19
|
+
'Creating visual separation between content sections',
|
|
20
|
+
'Displaying a preview or summary of an item',
|
|
21
|
+
'Building dashboard widgets or tiles',
|
|
22
|
+
],
|
|
23
|
+
whenNot: [
|
|
24
|
+
'Simple text content that does not need grouping',
|
|
25
|
+
'Modal or dialog content (use Dialog component)',
|
|
26
|
+
'Navigation items (use NavItem or similar)',
|
|
27
|
+
],
|
|
28
|
+
guidelines: [
|
|
29
|
+
'Use consistent card variants within the same context',
|
|
30
|
+
'Cards in a grid should have uniform sizing',
|
|
31
|
+
'Use elevated variant sparingly for emphasis',
|
|
32
|
+
'Interactive cards should have clear hover states',
|
|
33
|
+
],
|
|
34
|
+
accessibility: [
|
|
35
|
+
'Interactive cards should use button or link semantics',
|
|
36
|
+
'Card titles should be appropriate heading levels',
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
props: {
|
|
41
|
+
children: {
|
|
42
|
+
type: 'node',
|
|
43
|
+
description: 'Card content',
|
|
44
|
+
},
|
|
45
|
+
title: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
description: 'Card header title',
|
|
48
|
+
},
|
|
49
|
+
description: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
description: 'Card header description/subtitle',
|
|
52
|
+
},
|
|
53
|
+
variant: {
|
|
54
|
+
type: 'enum',
|
|
55
|
+
values: ['default', 'outlined', 'elevated'],
|
|
56
|
+
default: 'default',
|
|
57
|
+
description: 'Visual style of the card surface',
|
|
58
|
+
constraints: ['Use "elevated" sparingly to maintain visual hierarchy'],
|
|
59
|
+
},
|
|
60
|
+
padding: {
|
|
61
|
+
type: 'enum',
|
|
62
|
+
values: ['none', 'sm', 'md', 'lg'],
|
|
63
|
+
default: 'md',
|
|
64
|
+
description: 'Internal padding size',
|
|
65
|
+
},
|
|
66
|
+
onClick: {
|
|
67
|
+
type: 'function',
|
|
68
|
+
description: 'Click handler - makes card interactive',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
relations: [
|
|
73
|
+
{
|
|
74
|
+
component: 'CardGrid',
|
|
75
|
+
relationship: 'parent',
|
|
76
|
+
note: 'Use CardGrid for responsive card layouts',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
component: 'ListItem',
|
|
80
|
+
relationship: 'alternative',
|
|
81
|
+
note: 'Use ListItem for linear list layouts',
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
|
|
85
|
+
contract: {
|
|
86
|
+
propsSummary: [
|
|
87
|
+
'variant: default|outlined|elevated (default: default)',
|
|
88
|
+
'padding: none|sm|md|lg (default: md)',
|
|
89
|
+
'title: string - header title',
|
|
90
|
+
'description: string - header subtitle',
|
|
91
|
+
'onClick: () => void - makes card interactive',
|
|
92
|
+
],
|
|
93
|
+
scenarioTags: [
|
|
94
|
+
'layout.container',
|
|
95
|
+
'content.group',
|
|
96
|
+
'pattern.widget',
|
|
97
|
+
'pattern.tile',
|
|
98
|
+
],
|
|
99
|
+
a11yRules: [
|
|
100
|
+
'A11Y_CARD_HEADING',
|
|
101
|
+
'A11Y_CARD_INTERACTIVE',
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
variants: [
|
|
106
|
+
{
|
|
107
|
+
name: 'Default',
|
|
108
|
+
description: 'Standard card with subtle shadow',
|
|
109
|
+
render: () => (
|
|
110
|
+
<Card title="Card Title" description="A brief description">
|
|
111
|
+
Card content goes here.
|
|
112
|
+
</Card>
|
|
113
|
+
),
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'Outlined',
|
|
117
|
+
description: 'Card with border instead of shadow',
|
|
118
|
+
render: () => (
|
|
119
|
+
<Card variant="outlined" title="Outlined Card">
|
|
120
|
+
Content with border.
|
|
121
|
+
</Card>
|
|
122
|
+
),
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'Elevated',
|
|
126
|
+
description: 'Card with prominent shadow for emphasis',
|
|
127
|
+
render: () => (
|
|
128
|
+
<Card variant="elevated" title="Featured Item">
|
|
129
|
+
Important content.
|
|
130
|
+
</Card>
|
|
131
|
+
),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'Interactive',
|
|
135
|
+
description: 'Clickable card',
|
|
136
|
+
render: () => (
|
|
137
|
+
<Card
|
|
138
|
+
title="Click Me"
|
|
139
|
+
description="This card is interactive"
|
|
140
|
+
onClick={() => alert('Card clicked!')}
|
|
141
|
+
>
|
|
142
|
+
Click anywhere on this card.
|
|
143
|
+
</Card>
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'Content Only',
|
|
148
|
+
description: 'Card without header',
|
|
149
|
+
render: () => (
|
|
150
|
+
<Card>
|
|
151
|
+
Just content, no title or description.
|
|
152
|
+
</Card>
|
|
153
|
+
),
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
@use '../../tokens/mixins' as *;
|
|
3
|
+
|
|
4
|
+
.card {
|
|
5
|
+
@include surface-elevated;
|
|
6
|
+
font-family: var(--fui-font-sans, $fui-font-sans);
|
|
7
|
+
transition:
|
|
8
|
+
box-shadow var(--fui-transition-fast, $fui-transition-fast),
|
|
9
|
+
border-color var(--fui-transition-fast, $fui-transition-fast);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Variants
|
|
13
|
+
.default {
|
|
14
|
+
box-shadow: var(--fui-shadow-sm, $fui-shadow-sm);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.outlined {
|
|
18
|
+
border-color: var(--fui-border-strong, $fui-border-strong);
|
|
19
|
+
box-shadow: none;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.elevated {
|
|
23
|
+
box-shadow: var(--fui-shadow-md, $fui-shadow-md);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Padding
|
|
27
|
+
.paddingNone {
|
|
28
|
+
padding: 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.paddingSm {
|
|
32
|
+
padding: var(--fui-space-3, $fui-space-3);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.paddingMd {
|
|
36
|
+
padding: var(--fui-space-4, $fui-space-4);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.paddingLg {
|
|
40
|
+
padding: var(--fui-space-6, $fui-space-6);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Interactive card (when onClick is provided)
|
|
44
|
+
.interactive {
|
|
45
|
+
@include button-reset;
|
|
46
|
+
@include interactive-base;
|
|
47
|
+
|
|
48
|
+
display: block;
|
|
49
|
+
width: 100%;
|
|
50
|
+
text-align: left;
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
|
|
53
|
+
&:hover {
|
|
54
|
+
border-color: var(--fui-border-strong, $fui-border-strong);
|
|
55
|
+
box-shadow: var(--fui-shadow-md, $fui-shadow-md);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
&:active {
|
|
59
|
+
box-shadow: var(--fui-shadow-sm, $fui-shadow-sm);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.header {
|
|
64
|
+
margin-bottom: var(--fui-space-3, $fui-space-3);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.title {
|
|
68
|
+
margin: 0;
|
|
69
|
+
font-size: var(--fui-font-size-base, $fui-font-size-base);
|
|
70
|
+
font-weight: var(--fui-font-weight-semibold, $fui-font-weight-semibold);
|
|
71
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
72
|
+
line-height: var(--fui-line-height-tight, $fui-line-height-tight);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.description {
|
|
76
|
+
margin: var(--fui-space-1, $fui-space-1) 0 0;
|
|
77
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
78
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
79
|
+
line-height: var(--fui-line-height-normal, $fui-line-height-normal);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.content {
|
|
83
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
84
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
85
|
+
line-height: var(--fui-line-height-normal, $fui-line-height-normal);
|
|
86
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import styles from './Card.module.scss';
|
|
3
|
+
// Import globals to ensure CSS variables are defined
|
|
4
|
+
import '../../styles/globals.scss';
|
|
5
|
+
|
|
6
|
+
export interface CardProps {
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
title?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
variant?: 'default' | 'outlined' | 'elevated';
|
|
11
|
+
padding?: 'none' | 'sm' | 'md' | 'lg';
|
|
12
|
+
onClick?: () => void;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const paddingMap = {
|
|
17
|
+
none: styles.paddingNone,
|
|
18
|
+
sm: styles.paddingSm,
|
|
19
|
+
md: styles.paddingMd,
|
|
20
|
+
lg: styles.paddingLg,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
|
24
|
+
function Card(
|
|
25
|
+
{
|
|
26
|
+
children,
|
|
27
|
+
title,
|
|
28
|
+
description,
|
|
29
|
+
variant = 'default',
|
|
30
|
+
padding = 'md',
|
|
31
|
+
onClick,
|
|
32
|
+
className,
|
|
33
|
+
},
|
|
34
|
+
ref
|
|
35
|
+
) {
|
|
36
|
+
const isInteractive = !!onClick;
|
|
37
|
+
|
|
38
|
+
const classes = [
|
|
39
|
+
styles.card,
|
|
40
|
+
styles[variant],
|
|
41
|
+
paddingMap[padding],
|
|
42
|
+
isInteractive && styles.interactive,
|
|
43
|
+
className,
|
|
44
|
+
]
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(' ');
|
|
47
|
+
|
|
48
|
+
const content = (
|
|
49
|
+
<>
|
|
50
|
+
{(title || description) && (
|
|
51
|
+
<div className={children ? styles.header : undefined}>
|
|
52
|
+
{title && <h3 className={styles.title}>{title}</h3>}
|
|
53
|
+
{description && <p className={styles.description}>{description}</p>}
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
{children && <div className={styles.content}>{children}</div>}
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (isInteractive) {
|
|
61
|
+
return (
|
|
62
|
+
<button
|
|
63
|
+
ref={ref as React.Ref<HTMLButtonElement>}
|
|
64
|
+
type="button"
|
|
65
|
+
onClick={onClick}
|
|
66
|
+
className={classes}
|
|
67
|
+
>
|
|
68
|
+
{content}
|
|
69
|
+
</button>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div ref={ref} className={classes}>
|
|
75
|
+
{content}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
);
|