@fragments-sdk/ui 0.3.0 → 0.5.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/README.md +98 -2
- package/fragments.json +1 -1
- package/package.json +11 -5
- package/src/components/Accordion/Accordion.fragment.tsx +186 -0
- package/src/components/Accordion/Accordion.module.scss +111 -0
- package/src/components/Accordion/index.tsx +271 -0
- package/src/components/Alert/Alert.fragment.tsx +67 -42
- package/src/components/Alert/Alert.module.scss +31 -21
- package/src/components/Alert/index.tsx +202 -73
- package/src/components/AppShell/AppShell.fragment.tsx +315 -0
- package/src/components/AppShell/AppShell.module.scss +213 -0
- package/src/components/AppShell/index.tsx +398 -0
- package/src/components/Avatar/Avatar.fragment.tsx +2 -2
- package/src/components/Avatar/index.tsx +8 -9
- package/src/components/Badge/Badge.fragment.tsx +2 -2
- package/src/components/Badge/Badge.module.scss +16 -10
- package/src/components/Badge/index.tsx +20 -6
- package/src/components/Box/Box.fragment.tsx +168 -0
- package/src/components/Box/Box.module.scss +84 -0
- package/src/components/Box/index.tsx +78 -0
- package/src/components/Button/Button.fragment.tsx +2 -2
- package/src/components/Button/Button.module.scss +42 -0
- package/src/components/Button/index.tsx +67 -33
- package/src/components/ButtonGroup/ButtonGroup.fragment.tsx +153 -0
- package/src/components/ButtonGroup/ButtonGroup.module.scss +37 -0
- package/src/components/ButtonGroup/index.tsx +40 -0
- package/src/components/Card/Card.fragment.tsx +52 -26
- package/src/components/Card/Card.module.scss +52 -5
- package/src/components/Card/index.tsx +154 -53
- package/src/components/Chart/Chart.fragment.tsx +213 -0
- package/src/components/Chart/Chart.module.scss +123 -0
- package/src/components/Chart/index.tsx +267 -0
- package/src/components/Checkbox/Checkbox.fragment.tsx +1 -1
- package/src/components/Checkbox/Checkbox.module.scss +4 -4
- package/src/components/Checkbox/index.tsx +3 -4
- package/src/components/CodeBlock/CodeBlock.fragment.tsx +460 -0
- package/src/components/CodeBlock/CodeBlock.module.scss +362 -0
- package/src/components/CodeBlock/index.tsx +599 -0
- package/src/components/Collapsible/Collapsible.fragment.tsx +199 -0
- package/src/components/Collapsible/Collapsible.module.scss +117 -0
- package/src/components/Collapsible/index.tsx +219 -0
- package/src/components/ColorPicker/ColorPicker.fragment.tsx +196 -0
- package/src/components/ColorPicker/ColorPicker.module.scss +119 -0
- package/src/components/ColorPicker/index.tsx +129 -0
- package/src/components/ConversationList/ConversationList.fragment.tsx +202 -0
- package/src/components/ConversationList/ConversationList.module.scss +160 -0
- package/src/components/ConversationList/index.tsx +254 -0
- package/src/components/Dialog/Dialog.fragment.tsx +12 -3
- package/src/components/Dialog/Dialog.module.scss +26 -7
- package/src/components/Dialog/index.tsx +12 -15
- package/src/components/EmptyState/EmptyState.fragment.tsx +55 -72
- package/src/components/EmptyState/EmptyState.module.scss +9 -9
- package/src/components/EmptyState/index.tsx +104 -69
- package/src/components/Field/Field.fragment.tsx +165 -0
- package/src/components/Field/Field.module.scss +31 -0
- package/src/components/Field/index.tsx +143 -0
- package/src/components/Fieldset/Fieldset.fragment.tsx +166 -0
- package/src/components/Fieldset/Fieldset.module.scss +22 -0
- package/src/components/Fieldset/index.tsx +47 -0
- package/src/components/Form/Form.fragment.tsx +286 -0
- package/src/components/Form/Form.module.scss +8 -0
- package/src/components/Form/index.tsx +53 -0
- package/src/components/Grid/Grid.fragment.tsx +18 -18
- package/src/components/Grid/index.tsx +6 -1
- package/src/components/Header/Header.fragment.tsx +192 -0
- package/src/components/Header/Header.module.scss +208 -0
- package/src/components/Header/index.tsx +363 -0
- package/src/components/Icon/Icon.fragment.tsx +138 -0
- package/src/components/Icon/Icon.module.scss +38 -0
- package/src/components/Icon/index.tsx +58 -0
- package/src/components/Image/Image.fragment.tsx +195 -0
- package/src/components/Image/Image.module.scss +77 -0
- package/src/components/Image/index.tsx +95 -0
- package/src/components/Input/Input.fragment.tsx +1 -1
- package/src/components/Input/Input.module.scss +75 -2
- package/src/components/Input/index.tsx +60 -21
- package/src/components/Link/Link.fragment.tsx +132 -0
- package/src/components/Link/Link.module.scss +67 -0
- package/src/components/Link/index.tsx +57 -0
- package/src/components/List/List.fragment.tsx +152 -0
- package/src/components/List/List.module.scss +71 -0
- package/src/components/List/index.tsx +106 -0
- package/src/components/Listbox/Listbox.fragment.tsx +191 -0
- package/src/components/Listbox/Listbox.module.scss +97 -0
- package/src/components/Listbox/index.tsx +121 -0
- package/src/components/Loading/Loading.fragment.tsx +153 -0
- package/src/components/Loading/Loading.module.scss +256 -0
- package/src/components/Loading/index.tsx +236 -0
- package/src/components/Menu/Menu.fragment.tsx +12 -3
- package/src/components/Menu/Menu.module.scss +17 -1
- package/src/components/Menu/index.tsx +3 -3
- package/src/components/Message/Message.fragment.tsx +200 -0
- package/src/components/Message/Message.module.scss +224 -0
- package/src/components/Message/index.tsx +278 -0
- package/src/components/Popover/Popover.fragment.tsx +13 -4
- package/src/components/Popover/Popover.module.scss +33 -10
- package/src/components/Popover/index.tsx +9 -11
- package/src/components/Progress/Progress.fragment.tsx +1 -1
- package/src/components/Progress/Progress.module.scss +11 -11
- package/src/components/Progress/index.tsx +34 -7
- package/src/components/Prompt/Prompt.fragment.tsx +231 -0
- package/src/components/Prompt/Prompt.module.scss +243 -0
- package/src/components/Prompt/index.tsx +439 -0
- package/src/components/RadioGroup/RadioGroup.fragment.tsx +1 -1
- package/src/components/RadioGroup/RadioGroup.module.scss +10 -7
- package/src/components/RadioGroup/index.tsx +3 -4
- package/src/components/Select/Select.fragment.tsx +10 -1
- package/src/components/Select/Select.module.scss +8 -0
- package/src/components/Select/index.tsx +91 -12
- package/src/components/Separator/Separator.fragment.tsx +1 -1
- package/src/components/Separator/index.tsx +7 -3
- package/src/components/Sidebar/Sidebar.fragment.tsx +11 -2
- package/src/components/Sidebar/Sidebar.module.scss +91 -47
- package/src/components/Sidebar/index.tsx +57 -14
- package/src/components/Skeleton/Skeleton.fragment.tsx +6 -6
- package/src/components/Skeleton/Skeleton.module.scss +11 -0
- package/src/components/Slider/Slider.fragment.tsx +201 -0
- package/src/components/Slider/Slider.module.scss +87 -0
- package/src/components/Slider/index.tsx +88 -0
- package/src/components/Stack/Stack.fragment.tsx +194 -0
- package/src/components/Stack/Stack.module.scss +120 -0
- package/src/components/Stack/index.tsx +148 -0
- package/src/components/Table/Table.fragment.tsx +10 -3
- package/src/components/Table/Table.module.scss +57 -0
- package/src/components/Table/index.tsx +44 -6
- package/src/components/Tabs/Tabs.fragment.tsx +10 -1
- package/src/components/Tabs/Tabs.module.scss +25 -10
- package/src/components/Tabs/index.tsx +11 -8
- package/src/components/Text/Text.fragment.tsx +188 -0
- package/src/components/Text/Text.module.scss +82 -0
- package/src/components/Text/index.tsx +58 -0
- package/src/components/Textarea/Textarea.fragment.tsx +1 -1
- package/src/components/Textarea/index.tsx +3 -7
- package/src/components/Theme/Theme.fragment.tsx +128 -0
- package/src/components/Theme/ThemeToggle.module.scss +82 -0
- package/src/components/Theme/index.tsx +343 -0
- package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +182 -0
- package/src/components/ThinkingIndicator/ThinkingIndicator.module.scss +226 -0
- package/src/components/ThinkingIndicator/index.tsx +258 -0
- package/src/components/Toast/Toast.fragment.tsx +6 -6
- package/src/components/Toast/Toast.module.scss +16 -1
- package/src/components/Toast/index.tsx +27 -11
- package/src/components/Toggle/Toggle.fragment.tsx +1 -1
- package/src/components/Toggle/Toggle.module.scss +25 -10
- package/src/components/Toggle/index.tsx +12 -0
- package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +207 -0
- package/src/components/ToggleGroup/ToggleGroup.module.scss +134 -0
- package/src/components/ToggleGroup/index.tsx +144 -0
- package/src/components/Tooltip/Tooltip.fragment.tsx +3 -3
- package/src/components/Tooltip/Tooltip.module.scss +4 -4
- package/src/components/Tooltip/index.tsx +4 -2
- package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +134 -0
- package/src/components/VisuallyHidden/VisuallyHidden.module.scss +13 -0
- package/src/components/VisuallyHidden/index.tsx +29 -0
- package/src/index.ts +278 -3
- package/src/recipes/AIChat.recipe.ts +266 -0
- package/src/recipes/AppShell.recipe.ts +175 -0
- package/src/recipes/CardGrid.recipe.ts +6 -2
- package/src/recipes/ChatInterface.recipe.ts +87 -0
- package/src/recipes/CodeExamples.recipe.ts +66 -0
- package/src/recipes/DashboardLayout.recipe.ts +46 -12
- package/src/recipes/DashboardNav.recipe.ts +183 -0
- package/src/recipes/LoginForm.recipe.ts +8 -1
- package/src/recipes/SettingsPage.recipe.ts +37 -20
- package/src/styles/globals.scss +31 -0
- package/src/tokens/_computed.scss +212 -0
- package/src/tokens/_density.scss +171 -0
- package/src/tokens/_derive.scss +287 -0
- package/src/tokens/_index.scss +41 -0
- package/src/tokens/_mixins.scss +95 -1
- package/src/tokens/_palettes.scss +185 -0
- package/src/tokens/_radius.scss +107 -0
- package/src/tokens/_seeds.scss +59 -0
- package/src/tokens/_variables.scss +507 -101
- package/src/utils/a11y.tsx +439 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineSegment } from '@fragments/core';
|
|
3
|
+
import { Link } from '.';
|
|
4
|
+
|
|
5
|
+
export default defineSegment({
|
|
6
|
+
component: Link,
|
|
7
|
+
|
|
8
|
+
meta: {
|
|
9
|
+
name: 'Link',
|
|
10
|
+
description: 'Styled anchor element for navigation. Supports internal and external links with consistent visual treatment.',
|
|
11
|
+
category: 'navigation',
|
|
12
|
+
status: 'stable',
|
|
13
|
+
tags: ['link', 'anchor', 'navigation', 'href', 'url'],
|
|
14
|
+
since: '0.1.0',
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
usage: {
|
|
18
|
+
when: [
|
|
19
|
+
'Inline text links within paragraphs',
|
|
20
|
+
'Navigation links in footers or sidebars',
|
|
21
|
+
'"Forgot password?" or "Sign up" links in forms',
|
|
22
|
+
'External links to documentation or resources',
|
|
23
|
+
],
|
|
24
|
+
whenNot: [
|
|
25
|
+
'Primary call-to-action (use Button instead)',
|
|
26
|
+
'Navigation tabs (use Tabs component)',
|
|
27
|
+
'Menu items in dropdowns (use Menu component)',
|
|
28
|
+
'Cards that link to detail pages (use Card with onClick)',
|
|
29
|
+
],
|
|
30
|
+
guidelines: [
|
|
31
|
+
'Link text should describe the destination, not "click here"',
|
|
32
|
+
'Use external prop for links that open in new tabs',
|
|
33
|
+
'Use subtle variant for secondary/contextual links',
|
|
34
|
+
'Ensure links are distinguishable from regular text',
|
|
35
|
+
],
|
|
36
|
+
accessibility: [
|
|
37
|
+
'Link text must be descriptive of the destination',
|
|
38
|
+
'External links should indicate they open in new window',
|
|
39
|
+
'Links must have visible focus indicators',
|
|
40
|
+
'Avoid using links that look like buttons without button semantics',
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
props: {
|
|
45
|
+
children: {
|
|
46
|
+
type: 'node',
|
|
47
|
+
description: 'Link text content',
|
|
48
|
+
required: true,
|
|
49
|
+
},
|
|
50
|
+
href: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
description: 'URL destination',
|
|
53
|
+
},
|
|
54
|
+
variant: {
|
|
55
|
+
type: 'enum',
|
|
56
|
+
description: 'Visual style variant',
|
|
57
|
+
values: ['default', 'subtle', 'muted'],
|
|
58
|
+
default: 'default',
|
|
59
|
+
},
|
|
60
|
+
underline: {
|
|
61
|
+
type: 'enum',
|
|
62
|
+
description: 'Underline behavior',
|
|
63
|
+
values: ['always', 'hover', 'none'],
|
|
64
|
+
default: 'hover',
|
|
65
|
+
},
|
|
66
|
+
external: {
|
|
67
|
+
type: 'boolean',
|
|
68
|
+
description: 'Opens in new tab with noopener noreferrer',
|
|
69
|
+
default: 'false',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
relations: [
|
|
74
|
+
{ component: 'Button', relationship: 'alternative', note: 'Use Button for primary actions, Link for navigation' },
|
|
75
|
+
{ component: 'Text', relationship: 'parent', note: 'Links often appear within Text components' },
|
|
76
|
+
],
|
|
77
|
+
|
|
78
|
+
contract: {
|
|
79
|
+
propsSummary: [
|
|
80
|
+
'children: ReactNode - link text (required)',
|
|
81
|
+
'href: string - destination URL',
|
|
82
|
+
'variant: default|subtle|muted - visual style',
|
|
83
|
+
'underline: always|hover|none - underline behavior',
|
|
84
|
+
'external: boolean - opens in new tab',
|
|
85
|
+
],
|
|
86
|
+
scenarioTags: [
|
|
87
|
+
'navigation.link',
|
|
88
|
+
'content.inline',
|
|
89
|
+
'action.navigate',
|
|
90
|
+
],
|
|
91
|
+
a11yRules: ['A11Y_LINK_TEXT', 'A11Y_LINK_FOCUS'],
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
variants: [
|
|
95
|
+
{
|
|
96
|
+
name: 'Default',
|
|
97
|
+
description: 'Standard link with hover underline',
|
|
98
|
+
render: () => <Link href="#">Learn more about our services</Link>,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'Variants',
|
|
102
|
+
description: 'Visual style options',
|
|
103
|
+
render: () => (
|
|
104
|
+
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
|
|
105
|
+
<Link href="#" variant="default">Default</Link>
|
|
106
|
+
<Link href="#" variant="subtle">Subtle</Link>
|
|
107
|
+
<Link href="#" variant="muted">Muted</Link>
|
|
108
|
+
</div>
|
|
109
|
+
),
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'Underline Styles',
|
|
113
|
+
description: 'Different underline behaviors',
|
|
114
|
+
render: () => (
|
|
115
|
+
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
|
|
116
|
+
<Link href="#" underline="always">Always underlined</Link>
|
|
117
|
+
<Link href="#" underline="hover">Underline on hover</Link>
|
|
118
|
+
<Link href="#" underline="none">No underline</Link>
|
|
119
|
+
</div>
|
|
120
|
+
),
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'External Link',
|
|
124
|
+
description: 'Link that opens in new tab',
|
|
125
|
+
render: () => (
|
|
126
|
+
<Link href="https://example.com" external>
|
|
127
|
+
View documentation ↗
|
|
128
|
+
</Link>
|
|
129
|
+
),
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
@use '../../tokens/mixins' as *;
|
|
3
|
+
|
|
4
|
+
.link {
|
|
5
|
+
@include interactive-base;
|
|
6
|
+
|
|
7
|
+
display: inline;
|
|
8
|
+
font-family: inherit;
|
|
9
|
+
font-size: inherit;
|
|
10
|
+
font-weight: inherit;
|
|
11
|
+
line-height: inherit;
|
|
12
|
+
text-decoration: none;
|
|
13
|
+
cursor: pointer;
|
|
14
|
+
border-radius: var(--fui-radius-sm, $fui-radius-sm);
|
|
15
|
+
|
|
16
|
+
&:focus-visible {
|
|
17
|
+
@include focus-ring;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Variants
|
|
22
|
+
.default {
|
|
23
|
+
color: var(--fui-color-accent, $fui-color-accent);
|
|
24
|
+
|
|
25
|
+
&:hover {
|
|
26
|
+
color: var(--fui-color-accent-hover, $fui-color-accent-hover);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.subtle {
|
|
31
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
32
|
+
|
|
33
|
+
&:hover {
|
|
34
|
+
color: var(--fui-color-accent, $fui-color-accent);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.muted {
|
|
39
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
40
|
+
|
|
41
|
+
&:hover {
|
|
42
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Underline styles
|
|
47
|
+
.underline-always {
|
|
48
|
+
text-decoration: underline;
|
|
49
|
+
text-underline-offset: 2px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.underline-hover {
|
|
53
|
+
text-decoration: none;
|
|
54
|
+
|
|
55
|
+
&:hover {
|
|
56
|
+
text-decoration: underline;
|
|
57
|
+
text-underline-offset: 2px;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.underline-none {
|
|
62
|
+
text-decoration: none;
|
|
63
|
+
|
|
64
|
+
&:hover {
|
|
65
|
+
text-decoration: none;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import styles from './Link.module.scss';
|
|
3
|
+
import '../../styles/globals.scss';
|
|
4
|
+
|
|
5
|
+
export interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
/** Visual variant */
|
|
8
|
+
variant?: 'default' | 'subtle' | 'muted';
|
|
9
|
+
/** Underline style */
|
|
10
|
+
underline?: 'always' | 'hover' | 'none';
|
|
11
|
+
/** Open in new tab (adds rel="noopener noreferrer") */
|
|
12
|
+
external?: boolean;
|
|
13
|
+
/** Additional class name */
|
|
14
|
+
className?: string;
|
|
15
|
+
/** Inline styles */
|
|
16
|
+
style?: React.CSSProperties;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
|
|
20
|
+
function Link(
|
|
21
|
+
{
|
|
22
|
+
children,
|
|
23
|
+
variant = 'default',
|
|
24
|
+
underline = 'hover',
|
|
25
|
+
external = false,
|
|
26
|
+
className,
|
|
27
|
+
style,
|
|
28
|
+
target,
|
|
29
|
+
rel,
|
|
30
|
+
...props
|
|
31
|
+
},
|
|
32
|
+
ref
|
|
33
|
+
) {
|
|
34
|
+
const classes = [
|
|
35
|
+
styles.link,
|
|
36
|
+
styles[variant],
|
|
37
|
+
styles[`underline-${underline}`],
|
|
38
|
+
className,
|
|
39
|
+
]
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.join(' ');
|
|
42
|
+
|
|
43
|
+
// Handle external links
|
|
44
|
+
const externalProps = external
|
|
45
|
+
? {
|
|
46
|
+
target: target || '_blank',
|
|
47
|
+
rel: rel || 'noopener noreferrer',
|
|
48
|
+
}
|
|
49
|
+
: { target, rel };
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<a ref={ref} className={classes} style={style} {...externalProps} {...props}>
|
|
53
|
+
{children}
|
|
54
|
+
</a>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
);
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineSegment } from '@fragments/core';
|
|
3
|
+
import { List } from '.';
|
|
4
|
+
import { Check, Star, ArrowRight } from '@phosphor-icons/react';
|
|
5
|
+
|
|
6
|
+
export default defineSegment({
|
|
7
|
+
component: List,
|
|
8
|
+
|
|
9
|
+
meta: {
|
|
10
|
+
name: 'List',
|
|
11
|
+
description: 'Compound component for rendering ordered or unordered lists with consistent styling. Supports bullet, numbered, and icon-prefixed items.',
|
|
12
|
+
category: 'display',
|
|
13
|
+
status: 'stable',
|
|
14
|
+
tags: ['list', 'items', 'bullet', 'ordered', 'unordered'],
|
|
15
|
+
since: '0.1.0',
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
usage: {
|
|
19
|
+
when: [
|
|
20
|
+
'Feature lists with checkmarks or icons',
|
|
21
|
+
'Ordered steps or instructions',
|
|
22
|
+
'Navigation lists in sidebars',
|
|
23
|
+
'Pricing plan feature comparisons',
|
|
24
|
+
],
|
|
25
|
+
whenNot: [
|
|
26
|
+
'Interactive selection lists (use Menu or Select)',
|
|
27
|
+
'Data tables with columns (use Table)',
|
|
28
|
+
'Cards in a grid (use Grid with Card)',
|
|
29
|
+
'Navigation tabs (use Tabs)',
|
|
30
|
+
],
|
|
31
|
+
guidelines: [
|
|
32
|
+
'Use as="ol" for sequential or numbered content',
|
|
33
|
+
'Use variant="icon" with meaningful icons for feature lists',
|
|
34
|
+
'Keep list items concise and parallel in structure',
|
|
35
|
+
'Use consistent icons within a single list',
|
|
36
|
+
],
|
|
37
|
+
accessibility: [
|
|
38
|
+
'Use semantic list elements (ul, ol) for screen reader support',
|
|
39
|
+
'List items are automatically announced with count',
|
|
40
|
+
'Icons are decorative; ensure text conveys meaning',
|
|
41
|
+
'Avoid deeply nested lists (3+ levels)',
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
props: {
|
|
46
|
+
children: {
|
|
47
|
+
type: 'node',
|
|
48
|
+
description: 'List.Item components',
|
|
49
|
+
required: true,
|
|
50
|
+
},
|
|
51
|
+
as: {
|
|
52
|
+
type: 'enum',
|
|
53
|
+
description: 'List type',
|
|
54
|
+
values: ['ul', 'ol'],
|
|
55
|
+
default: 'ul',
|
|
56
|
+
},
|
|
57
|
+
variant: {
|
|
58
|
+
type: 'enum',
|
|
59
|
+
description: 'List style variant',
|
|
60
|
+
values: ['none', 'disc', 'decimal', 'icon'],
|
|
61
|
+
default: 'disc',
|
|
62
|
+
},
|
|
63
|
+
gap: {
|
|
64
|
+
type: 'enum',
|
|
65
|
+
description: 'Spacing between items',
|
|
66
|
+
values: ['none', 'xs', 'sm', 'md', 'lg'],
|
|
67
|
+
default: 'sm',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
relations: [
|
|
72
|
+
{ component: 'Icon', relationship: 'child', note: 'Use Icon as List.Item icon prop' },
|
|
73
|
+
{ component: 'Stack', relationship: 'alternative', note: 'Use Stack for non-semantic vertical lists' },
|
|
74
|
+
],
|
|
75
|
+
|
|
76
|
+
contract: {
|
|
77
|
+
propsSummary: [
|
|
78
|
+
'children: List.Item[] - list items (required)',
|
|
79
|
+
'as: ul|ol - list type',
|
|
80
|
+
'variant: none|disc|decimal|icon - list style',
|
|
81
|
+
'gap: none|xs|sm|md|lg - item spacing',
|
|
82
|
+
],
|
|
83
|
+
subComponents: [
|
|
84
|
+
{
|
|
85
|
+
name: 'List.Item',
|
|
86
|
+
props: [
|
|
87
|
+
'children: ReactNode - item content',
|
|
88
|
+
'icon: ReactNode - icon for variant="icon"',
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
scenarioTags: [
|
|
93
|
+
'content.list',
|
|
94
|
+
'layout.vertical',
|
|
95
|
+
'display.items',
|
|
96
|
+
],
|
|
97
|
+
a11yRules: ['A11Y_LIST_SEMANTIC', 'A11Y_LIST_NESTING'],
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
variants: [
|
|
101
|
+
{
|
|
102
|
+
name: 'Bullet List',
|
|
103
|
+
description: 'Default unordered list with bullets',
|
|
104
|
+
render: () => (
|
|
105
|
+
<List variant="disc">
|
|
106
|
+
<List.Item>First item</List.Item>
|
|
107
|
+
<List.Item>Second item</List.Item>
|
|
108
|
+
<List.Item>Third item</List.Item>
|
|
109
|
+
</List>
|
|
110
|
+
),
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'Numbered List',
|
|
114
|
+
description: 'Ordered list with numbers',
|
|
115
|
+
render: () => (
|
|
116
|
+
<List as="ol" variant="decimal">
|
|
117
|
+
<List.Item>Create your account</List.Item>
|
|
118
|
+
<List.Item>Configure your settings</List.Item>
|
|
119
|
+
<List.Item>Start building</List.Item>
|
|
120
|
+
</List>
|
|
121
|
+
),
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'Icon List',
|
|
125
|
+
description: 'List with custom icons per item',
|
|
126
|
+
render: () => (
|
|
127
|
+
<List variant="icon">
|
|
128
|
+
<List.Item icon={<Check weight="bold" color="var(--fui-color-success)" />}>
|
|
129
|
+
Unlimited projects
|
|
130
|
+
</List.Item>
|
|
131
|
+
<List.Item icon={<Check weight="bold" color="var(--fui-color-success)" />}>
|
|
132
|
+
Priority support
|
|
133
|
+
</List.Item>
|
|
134
|
+
<List.Item icon={<Check weight="bold" color="var(--fui-color-success)" />}>
|
|
135
|
+
Advanced analytics
|
|
136
|
+
</List.Item>
|
|
137
|
+
</List>
|
|
138
|
+
),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'No Style',
|
|
142
|
+
description: 'Plain list without markers',
|
|
143
|
+
render: () => (
|
|
144
|
+
<List variant="none" gap="md">
|
|
145
|
+
<List.Item>Dashboard</List.Item>
|
|
146
|
+
<List.Item>Settings</List.Item>
|
|
147
|
+
<List.Item>Profile</List.Item>
|
|
148
|
+
</List>
|
|
149
|
+
),
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
|
|
3
|
+
.list {
|
|
4
|
+
margin: 0;
|
|
5
|
+
padding: 0;
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Variants
|
|
11
|
+
.none {
|
|
12
|
+
list-style: none;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.disc {
|
|
16
|
+
list-style: disc;
|
|
17
|
+
padding-left: var(--fui-space-2, $fui-space-2);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.decimal {
|
|
21
|
+
list-style: decimal;
|
|
22
|
+
padding-left: var(--fui-space-2, $fui-space-2);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.icon {
|
|
26
|
+
list-style: none;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Gap variants
|
|
30
|
+
.gap-none {
|
|
31
|
+
gap: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.gap-xs {
|
|
35
|
+
gap: var(--fui-space-1, $fui-space-1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.gap-sm {
|
|
39
|
+
gap: var(--fui-space-2, $fui-space-2);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.gap-md {
|
|
43
|
+
gap: var(--fui-space-3, $fui-space-3);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.gap-lg {
|
|
47
|
+
gap: var(--fui-space-4, $fui-space-4);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// List item
|
|
51
|
+
.item {
|
|
52
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.iconItem {
|
|
56
|
+
display: flex;
|
|
57
|
+
align-items: flex-start;
|
|
58
|
+
gap: var(--fui-space-2, $fui-space-2);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.iconWrapper {
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
justify-content: center;
|
|
65
|
+
flex-shrink: 0;
|
|
66
|
+
line-height: 1.5;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.itemContent {
|
|
70
|
+
flex: 1;
|
|
71
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import styles from './List.module.scss';
|
|
3
|
+
import '../../styles/globals.scss';
|
|
4
|
+
|
|
5
|
+
// ============================================
|
|
6
|
+
// Types
|
|
7
|
+
// ============================================
|
|
8
|
+
|
|
9
|
+
export interface ListProps extends React.HTMLAttributes<HTMLUListElement | HTMLOListElement> {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
/** List type */
|
|
12
|
+
as?: 'ul' | 'ol';
|
|
13
|
+
/** List style variant */
|
|
14
|
+
variant?: 'none' | 'disc' | 'decimal' | 'icon';
|
|
15
|
+
/** Spacing between items */
|
|
16
|
+
gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ListItemProps extends React.HTMLAttributes<HTMLLIElement> {
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
/** Icon to display (only used with variant="icon") */
|
|
22
|
+
icon?: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ============================================
|
|
26
|
+
// Context
|
|
27
|
+
// ============================================
|
|
28
|
+
|
|
29
|
+
interface ListContextValue {
|
|
30
|
+
variant: NonNullable<ListProps['variant']>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ListContext = React.createContext<ListContextValue>({ variant: 'disc' });
|
|
34
|
+
|
|
35
|
+
function useListContext() {
|
|
36
|
+
return React.useContext(ListContext);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================
|
|
40
|
+
// Components
|
|
41
|
+
// ============================================
|
|
42
|
+
|
|
43
|
+
function ListRoot({
|
|
44
|
+
children,
|
|
45
|
+
as: Component = 'ul',
|
|
46
|
+
variant = 'disc',
|
|
47
|
+
gap = 'sm',
|
|
48
|
+
className,
|
|
49
|
+
style,
|
|
50
|
+
...htmlProps
|
|
51
|
+
}: ListProps) {
|
|
52
|
+
const classes = [
|
|
53
|
+
styles.list,
|
|
54
|
+
styles[variant],
|
|
55
|
+
styles[`gap-${gap}`],
|
|
56
|
+
className,
|
|
57
|
+
]
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.join(' ');
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<ListContext.Provider value={{ variant }}>
|
|
63
|
+
<Component {...htmlProps} className={classes} style={style}>
|
|
64
|
+
{children}
|
|
65
|
+
</Component>
|
|
66
|
+
</ListContext.Provider>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function ListItem({ children, icon, className, style, ...htmlProps }: ListItemProps) {
|
|
71
|
+
const { variant } = useListContext();
|
|
72
|
+
|
|
73
|
+
const classes = [
|
|
74
|
+
styles.item,
|
|
75
|
+
variant === 'icon' && styles.iconItem,
|
|
76
|
+
className,
|
|
77
|
+
]
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.join(' ');
|
|
80
|
+
|
|
81
|
+
if (variant === 'icon' && icon) {
|
|
82
|
+
return (
|
|
83
|
+
<li {...htmlProps} className={classes} style={style}>
|
|
84
|
+
<span className={styles.iconWrapper}>{icon}</span>
|
|
85
|
+
<span className={styles.itemContent}>{children}</span>
|
|
86
|
+
</li>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<li {...htmlProps} className={classes} style={style}>
|
|
92
|
+
{children}
|
|
93
|
+
</li>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============================================
|
|
98
|
+
// Export compound component
|
|
99
|
+
// ============================================
|
|
100
|
+
|
|
101
|
+
export const List = Object.assign(ListRoot, {
|
|
102
|
+
Item: ListItem,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Re-export individual components for tree-shaking
|
|
106
|
+
export { ListRoot, ListItem, useListContext };
|