@fragments-sdk/ui 0.7.5 → 0.8.1
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 +58 -25
- package/fragments.json +1 -1
- package/package.json +15 -5
- package/src/blocks/AppShell.block.ts +2 -2
- package/src/blocks/InsetDashboardLayout.block.ts +1 -1
- package/src/blocks/LoginForm.block.ts +14 -7
- package/src/components/Accordion/Accordion.fragment.tsx +10 -4
- package/src/components/Alert/Alert.fragment.tsx +2 -2
- package/src/components/Alert/Alert.module.scss +4 -4
- package/src/components/AppShell/AppShell.fragment.tsx +3 -3
- package/src/components/AppShell/index.tsx +2 -0
- package/src/components/Avatar/Avatar.fragment.tsx +7 -3
- package/src/components/Avatar/Avatar.module.scss +1 -1
- package/src/components/Avatar/index.tsx +37 -1
- package/src/components/Badge/Badge.fragment.tsx +5 -5
- package/src/components/Badge/Badge.module.scss +4 -4
- package/src/components/Badge/index.tsx +5 -1
- package/src/components/Box/Box.fragment.tsx +2 -2
- package/src/components/Box/index.tsx +5 -1
- package/src/components/Breadcrumbs/Breadcrumbs.fragment.tsx +2 -2
- package/src/components/Button/Button.fragment.tsx +19 -18
- package/src/components/Button/index.tsx +5 -1
- package/src/components/ButtonGroup/ButtonGroup.fragment.tsx +2 -2
- package/src/components/ButtonGroup/index.tsx +5 -1
- package/src/components/Card/Card.fragment.tsx +7 -7
- package/src/components/Chart/Chart.fragment.tsx +11 -3
- package/src/components/Chart/index.tsx +22 -4
- package/src/components/Checkbox/Checkbox.fragment.tsx +2 -2
- package/src/components/Checkbox/index.tsx +5 -1
- package/src/components/Chip/Chip.fragment.tsx +2 -7
- package/src/components/Chip/Chip.module.scss +2 -2
- package/src/components/CodeBlock/CodeBlock.fragment.tsx +11 -5
- package/src/components/CodeBlock/CodeBlock.module.scss +11 -53
- package/src/components/CodeBlock/index.tsx +13 -24
- package/src/components/Collapsible/Collapsible.fragment.tsx +2 -2
- package/src/components/ColorPicker/ColorPicker.fragment.tsx +2 -2
- package/src/components/ColorPicker/index.tsx +5 -1
- package/src/components/Combobox/Combobox.fragment.tsx +17 -9
- package/src/components/ConversationList/ConversationList.fragment.tsx +5 -5
- package/src/components/ConversationList/ConversationList.module.scss +1 -1
- package/src/components/DatePicker/DatePicker.fragment.tsx +245 -0
- package/src/components/DatePicker/DatePicker.module.scss +394 -0
- package/src/components/DatePicker/DatePicker.test.tsx +264 -0
- package/src/components/DatePicker/index.tsx +535 -0
- package/src/components/Dialog/Dialog.fragment.tsx +2 -2
- package/src/components/EmptyState/EmptyState.fragment.tsx +2 -2
- package/src/components/Field/Field.fragment.tsx +7 -6
- package/src/components/Fieldset/Fieldset.fragment.tsx +7 -6
- package/src/components/Form/Form.fragment.tsx +11 -5
- package/src/components/Form/index.tsx +5 -1
- package/src/components/Grid/Grid.fragment.tsx +6 -2
- package/src/components/Header/Header.fragment.tsx +38 -15
- package/src/components/Header/Header.module.scss +114 -1
- package/src/components/Header/Header.test.tsx +106 -1
- package/src/components/Header/index.tsx +100 -31
- package/src/components/Icon/Icon.fragment.tsx +8 -3
- package/src/components/Icon/index.tsx +5 -1
- package/src/components/Image/Image.fragment.tsx +4 -4
- package/src/components/Image/index.tsx +5 -1
- package/src/components/Input/Input.fragment.tsx +23 -5
- package/src/components/Input/Input.module.scss +1 -1
- package/src/components/Input/index.tsx +5 -1
- package/src/components/Link/Link.fragment.tsx +2 -6
- package/src/components/Link/index.tsx +5 -1
- package/src/components/List/List.fragment.tsx +2 -2
- package/src/components/Listbox/Listbox.fragment.tsx +2 -14
- package/src/components/Loading/Loading.fragment.tsx +2 -2
- package/src/components/Markdown/Markdown.fragment.tsx +2 -2
- package/src/components/Markdown/Markdown.module.scss +11 -3
- package/src/components/Markdown/index.tsx +5 -1
- package/src/components/Menu/Menu.fragment.tsx +2 -2
- package/src/components/Message/Message.fragment.tsx +10 -8
- package/src/components/Message/Message.module.scss +1 -1
- package/src/components/Popover/Popover.fragment.tsx +2 -2
- package/src/components/Progress/Progress.fragment.tsx +16 -2
- package/src/components/Progress/index.tsx +9 -2
- package/src/components/Prompt/Prompt.fragment.tsx +13 -2
- package/src/components/RadioGroup/RadioGroup.fragment.tsx +7 -2
- package/src/components/ScrollArea/ScrollArea.fragment.tsx +185 -0
- package/src/components/ScrollArea/ScrollArea.module.scss +136 -0
- package/src/components/ScrollArea/ScrollArea.test.tsx +38 -0
- package/src/components/ScrollArea/index.tsx +121 -0
- package/src/components/Select/Select.fragment.tsx +15 -7
- package/src/components/Separator/Separator.fragment.tsx +2 -2
- package/src/components/Separator/index.tsx +5 -1
- package/src/components/Sidebar/Sidebar.fragment.tsx +66 -13
- package/src/components/Sidebar/Sidebar.module.scss +69 -21
- package/src/components/Sidebar/Sidebar.test.tsx +31 -2
- package/src/components/Sidebar/index.tsx +69 -45
- package/src/components/Skeleton/Skeleton.fragment.tsx +7 -2
- package/src/components/Slider/Slider.fragment.tsx +2 -2
- package/src/components/Slider/index.tsx +5 -1
- package/src/components/Stack/Stack.fragment.tsx +4 -4
- package/src/components/Stack/index.tsx +5 -1
- package/src/components/Table/Table.fragment.tsx +31 -2
- package/src/components/Table/index.tsx +49 -6
- package/src/components/TableOfContents/TableOfContents.fragment.tsx +149 -0
- package/src/components/TableOfContents/TableOfContents.module.scss +66 -0
- package/src/components/TableOfContents/TableOfContents.test.tsx +126 -0
- package/src/components/TableOfContents/index.tsx +110 -0
- package/src/components/Tabs/Tabs.fragment.tsx +2 -2
- package/src/components/Text/Text.fragment.tsx +2 -2
- package/src/components/Text/Text.module.scss +6 -0
- package/src/components/Text/Text.test.tsx +5 -0
- package/src/components/Text/index.tsx +8 -1
- package/src/components/Textarea/Textarea.fragment.tsx +10 -2
- package/src/components/Textarea/index.tsx +5 -1
- package/src/components/Theme/Theme.fragment.tsx +2 -2
- package/src/components/Theme/ThemeToggle.module.scss +1 -1
- package/src/components/Theme/index.tsx +8 -1
- package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +5 -4
- package/src/components/Toast/Toast.fragment.tsx +14 -2
- package/src/components/Toggle/Toggle.fragment.tsx +2 -2
- package/src/components/Toggle/index.tsx +5 -1
- package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +5 -5
- package/src/components/Tooltip/Tooltip.fragment.tsx +20 -2
- package/src/components/Tooltip/index.tsx +6 -1
- package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +2 -2
- package/src/components/VisuallyHidden/index.tsx +5 -1
- package/src/components/compound-pattern.test.ts +40 -0
- package/src/index.ts +29 -0
- package/src/recipes/AppShell.recipe.ts +2 -2
- package/src/recipes/LoginForm.recipe.ts +14 -7
- package/src/tokens/_computed.scss +12 -0
- package/src/tokens/_derive.scss +71 -0
- package/src/tokens/_mixins.scss +9 -0
- package/src/tokens/_variables.scss +26 -4
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
|
|
3
|
+
// Root nav wrapper
|
|
4
|
+
.root {
|
|
5
|
+
font-family: var(--fui-font-sans, $fui-font-sans);
|
|
6
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
7
|
+
line-height: var(--fui-line-height-normal, $fui-line-height-normal);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Title text ("On This Page")
|
|
11
|
+
.title {
|
|
12
|
+
margin: 0;
|
|
13
|
+
padding: 0 0 var(--fui-space-2, $fui-space-2) 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// List container
|
|
17
|
+
.list {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
list-style: none;
|
|
21
|
+
margin: 0;
|
|
22
|
+
padding: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Individual item
|
|
26
|
+
.item {
|
|
27
|
+
display: block;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Link styling shared by all items
|
|
31
|
+
.link {
|
|
32
|
+
display: block;
|
|
33
|
+
padding: var(--fui-space-1, $fui-space-1) 0 var(--fui-space-1, $fui-space-1) var(--fui-space-3, $fui-space-3);
|
|
34
|
+
border-left: 2px solid transparent;
|
|
35
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
36
|
+
font-size: var(--fui-font-size-xs, $fui-font-size-xs);
|
|
37
|
+
line-height: var(--fui-line-height-normal, $fui-line-height-normal);
|
|
38
|
+
text-decoration: none;
|
|
39
|
+
transition: color var(--fui-transition-fast, $fui-transition-fast),
|
|
40
|
+
border-color var(--fui-transition-fast, $fui-transition-fast);
|
|
41
|
+
|
|
42
|
+
&:hover {
|
|
43
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
&:focus-visible {
|
|
47
|
+
outline: var(--fui-focus-ring-width, $fui-focus-ring-width) solid var(--fui-focus-ring-color, $fui-focus-ring-color);
|
|
48
|
+
outline-offset: var(--fui-focus-ring-offset, $fui-focus-ring-offset);
|
|
49
|
+
border-radius: var(--fui-radius-sm, $fui-radius-sm);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Indent for depth > 2 (h3s)
|
|
54
|
+
.indent {
|
|
55
|
+
padding-left: var(--fui-space-6, $fui-space-6);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Active state
|
|
59
|
+
.active {
|
|
60
|
+
border-left-color: var(--fui-color-accent, $fui-color-accent);
|
|
61
|
+
color: var(--fui-color-accent, $fui-color-accent);
|
|
62
|
+
|
|
63
|
+
&:hover {
|
|
64
|
+
color: var(--fui-color-accent, $fui-color-accent);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen, userEvent, expectNoA11yViolations } from '../../test/utils';
|
|
3
|
+
import { TableOfContents } from './index';
|
|
4
|
+
|
|
5
|
+
describe('TableOfContents', () => {
|
|
6
|
+
it('renders a nav landmark with default aria-label', () => {
|
|
7
|
+
render(
|
|
8
|
+
<TableOfContents>
|
|
9
|
+
<TableOfContents.Item id="intro">Intro</TableOfContents.Item>
|
|
10
|
+
</TableOfContents>
|
|
11
|
+
);
|
|
12
|
+
expect(screen.getByRole('navigation', { name: 'Table of contents' })).toBeInTheDocument();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('renders the default "On This Page" title', () => {
|
|
16
|
+
render(
|
|
17
|
+
<TableOfContents>
|
|
18
|
+
<TableOfContents.Item id="intro">Intro</TableOfContents.Item>
|
|
19
|
+
</TableOfContents>
|
|
20
|
+
);
|
|
21
|
+
expect(screen.getByText('On This Page')).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('renders a custom title', () => {
|
|
25
|
+
render(
|
|
26
|
+
<TableOfContents title="Contents">
|
|
27
|
+
<TableOfContents.Item id="intro">Intro</TableOfContents.Item>
|
|
28
|
+
</TableOfContents>
|
|
29
|
+
);
|
|
30
|
+
expect(screen.getByText('Contents')).toBeInTheDocument();
|
|
31
|
+
expect(screen.queryByText('On This Page')).not.toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('hides the title when hideTitle is true', () => {
|
|
35
|
+
render(
|
|
36
|
+
<TableOfContents hideTitle>
|
|
37
|
+
<TableOfContents.Item id="intro">Intro</TableOfContents.Item>
|
|
38
|
+
</TableOfContents>
|
|
39
|
+
);
|
|
40
|
+
expect(screen.queryByText('On This Page')).not.toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('renders a custom aria-label', () => {
|
|
44
|
+
render(
|
|
45
|
+
<TableOfContents label="Page sections">
|
|
46
|
+
<TableOfContents.Item id="intro">Intro</TableOfContents.Item>
|
|
47
|
+
</TableOfContents>
|
|
48
|
+
);
|
|
49
|
+
expect(screen.getByRole('navigation', { name: 'Page sections' })).toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('renders items as links with correct href', () => {
|
|
53
|
+
render(
|
|
54
|
+
<TableOfContents>
|
|
55
|
+
<TableOfContents.Item id="setup">Setup</TableOfContents.Item>
|
|
56
|
+
<TableOfContents.Item id="props">Props</TableOfContents.Item>
|
|
57
|
+
</TableOfContents>
|
|
58
|
+
);
|
|
59
|
+
expect(screen.getByRole('link', { name: 'Setup' })).toHaveAttribute('href', '#setup');
|
|
60
|
+
expect(screen.getByRole('link', { name: 'Props' })).toHaveAttribute('href', '#props');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('marks active item with aria-current', () => {
|
|
64
|
+
render(
|
|
65
|
+
<TableOfContents>
|
|
66
|
+
<TableOfContents.Item id="setup" active>Setup</TableOfContents.Item>
|
|
67
|
+
<TableOfContents.Item id="props">Props</TableOfContents.Item>
|
|
68
|
+
</TableOfContents>
|
|
69
|
+
);
|
|
70
|
+
expect(screen.getByRole('link', { name: 'Setup' })).toHaveAttribute('aria-current', 'true');
|
|
71
|
+
expect(screen.getByRole('link', { name: 'Props' })).not.toHaveAttribute('aria-current');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('scrolls to heading on click', async () => {
|
|
75
|
+
const scrollIntoViewMock = vi.fn();
|
|
76
|
+
const heading = document.createElement('h2');
|
|
77
|
+
heading.id = 'setup';
|
|
78
|
+
heading.scrollIntoView = scrollIntoViewMock;
|
|
79
|
+
document.body.appendChild(heading);
|
|
80
|
+
|
|
81
|
+
const user = userEvent.setup();
|
|
82
|
+
render(
|
|
83
|
+
<TableOfContents>
|
|
84
|
+
<TableOfContents.Item id="setup">Setup</TableOfContents.Item>
|
|
85
|
+
</TableOfContents>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
await user.click(screen.getByRole('link', { name: 'Setup' }));
|
|
89
|
+
expect(scrollIntoViewMock).toHaveBeenCalledWith({ behavior: 'smooth' });
|
|
90
|
+
|
|
91
|
+
document.body.removeChild(heading);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('renders items in a list', () => {
|
|
95
|
+
render(
|
|
96
|
+
<TableOfContents>
|
|
97
|
+
<TableOfContents.Item id="a">A</TableOfContents.Item>
|
|
98
|
+
<TableOfContents.Item id="b">B</TableOfContents.Item>
|
|
99
|
+
<TableOfContents.Item id="c">C</TableOfContents.Item>
|
|
100
|
+
</TableOfContents>
|
|
101
|
+
);
|
|
102
|
+
expect(screen.getByRole('list')).toBeInTheDocument();
|
|
103
|
+
expect(screen.getAllByRole('listitem')).toHaveLength(3);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('passes additional className to root', () => {
|
|
107
|
+
const { container } = render(
|
|
108
|
+
<TableOfContents className="custom-class">
|
|
109
|
+
<TableOfContents.Item id="a">A</TableOfContents.Item>
|
|
110
|
+
</TableOfContents>
|
|
111
|
+
);
|
|
112
|
+
expect(container.querySelector('.custom-class')).toBeInTheDocument();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('has no accessibility violations', async () => {
|
|
116
|
+
const { container } = render(
|
|
117
|
+
<TableOfContents>
|
|
118
|
+
<TableOfContents.Item id="intro">Introduction</TableOfContents.Item>
|
|
119
|
+
<TableOfContents.Item id="setup" active>Setup</TableOfContents.Item>
|
|
120
|
+
<TableOfContents.Item id="api" indent>API Reference</TableOfContents.Item>
|
|
121
|
+
<TableOfContents.Item id="examples">Examples</TableOfContents.Item>
|
|
122
|
+
</TableOfContents>
|
|
123
|
+
);
|
|
124
|
+
await expectNoA11yViolations(container);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import styles from './TableOfContents.module.scss';
|
|
5
|
+
import { Text } from '../Text';
|
|
6
|
+
import '../../styles/globals.scss';
|
|
7
|
+
|
|
8
|
+
// ============================================
|
|
9
|
+
// Types
|
|
10
|
+
// ============================================
|
|
11
|
+
|
|
12
|
+
export interface TableOfContentsProps extends React.HTMLAttributes<HTMLElement> {
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
/** Label for the nav landmark (default: "Table of contents") */
|
|
15
|
+
label?: string;
|
|
16
|
+
/** Title displayed above the list (default: "On This Page") */
|
|
17
|
+
title?: string;
|
|
18
|
+
/** Hide the title */
|
|
19
|
+
hideTitle?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TableOfContentsItemProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> {
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
/** The heading ID to link to */
|
|
25
|
+
id: string;
|
|
26
|
+
/** Whether this item is currently active/visible */
|
|
27
|
+
active?: boolean;
|
|
28
|
+
/** Indent level — use for sub-headings (h3, h4, etc.) */
|
|
29
|
+
indent?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ============================================
|
|
33
|
+
// Components
|
|
34
|
+
// ============================================
|
|
35
|
+
|
|
36
|
+
function TableOfContentsRoot({
|
|
37
|
+
children,
|
|
38
|
+
label = 'Table of contents',
|
|
39
|
+
title = 'On This Page',
|
|
40
|
+
hideTitle = false,
|
|
41
|
+
className,
|
|
42
|
+
...htmlProps
|
|
43
|
+
}: TableOfContentsProps) {
|
|
44
|
+
const classes = [styles.root, className].filter(Boolean).join(' ');
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<nav aria-label={label} className={classes} {...htmlProps}>
|
|
48
|
+
{!hideTitle && (
|
|
49
|
+
<Text as="p" variant="section-label" className={styles.title}>
|
|
50
|
+
{title}
|
|
51
|
+
</Text>
|
|
52
|
+
)}
|
|
53
|
+
<ul className={styles.list}>
|
|
54
|
+
{children}
|
|
55
|
+
</ul>
|
|
56
|
+
</nav>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function TableOfContentsItem({
|
|
61
|
+
children,
|
|
62
|
+
id,
|
|
63
|
+
active = false,
|
|
64
|
+
indent = false,
|
|
65
|
+
className,
|
|
66
|
+
onClick,
|
|
67
|
+
...htmlProps
|
|
68
|
+
}: TableOfContentsItemProps) {
|
|
69
|
+
const linkClasses = [
|
|
70
|
+
styles.link,
|
|
71
|
+
indent && styles.indent,
|
|
72
|
+
active && styles.active,
|
|
73
|
+
className,
|
|
74
|
+
].filter(Boolean).join(' ');
|
|
75
|
+
|
|
76
|
+
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
const el = document.getElementById(id);
|
|
79
|
+
if (el) {
|
|
80
|
+
el.scrollIntoView({ behavior: 'smooth' });
|
|
81
|
+
history.replaceState(null, '', `#${id}`);
|
|
82
|
+
}
|
|
83
|
+
onClick?.(e);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<li className={styles.item}>
|
|
88
|
+
<a
|
|
89
|
+
href={`#${id}`}
|
|
90
|
+
className={linkClasses}
|
|
91
|
+
onClick={handleClick}
|
|
92
|
+
aria-current={active ? 'true' : undefined}
|
|
93
|
+
{...htmlProps}
|
|
94
|
+
>
|
|
95
|
+
{children}
|
|
96
|
+
</a>
|
|
97
|
+
</li>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ============================================
|
|
102
|
+
// Export compound component
|
|
103
|
+
// ============================================
|
|
104
|
+
|
|
105
|
+
export const TableOfContents = Object.assign(TableOfContentsRoot, {
|
|
106
|
+
Item: TableOfContentsItem,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Re-export individual components
|
|
110
|
+
export { TableOfContentsRoot, TableOfContentsItem };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
@use '../../tokens/variables' as *;
|
|
2
|
+
@use '../../tokens/mixins' as *;
|
|
2
3
|
|
|
3
4
|
.text {
|
|
4
5
|
font-family: var(--fui-font-sans, $fui-font-sans);
|
|
@@ -6,6 +7,11 @@
|
|
|
6
7
|
margin: 0;
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
// Variants
|
|
11
|
+
.variant-section-label {
|
|
12
|
+
@include section-label-text;
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
// Font sizes
|
|
10
16
|
.size-2xs {
|
|
11
17
|
font-size: var(--fui-font-size-2xs, $fui-font-size-2xs);
|
|
@@ -22,6 +22,11 @@ describe('Text', () => {
|
|
|
22
22
|
expect(el).toHaveClass('color-secondary');
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
+
it('applies section-label variant class', () => {
|
|
26
|
+
render(<Text variant="section-label">Label</Text>);
|
|
27
|
+
expect(screen.getByText('Label')).toHaveClass('variant-section-label');
|
|
28
|
+
});
|
|
29
|
+
|
|
25
30
|
it('applies truncate class', () => {
|
|
26
31
|
render(<Text truncate>Long text that should truncate</Text>);
|
|
27
32
|
expect(screen.getByText('Long text that should truncate')).toHaveClass('truncate');
|
|
@@ -5,6 +5,7 @@ import '../../styles/globals.scss';
|
|
|
5
5
|
export interface TextProps extends Omit<React.HTMLAttributes<HTMLElement>, 'color'> {
|
|
6
6
|
children: React.ReactNode;
|
|
7
7
|
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span' | 'label' | 'div' | 'strong' | 'em' | 'small' | 'mark' | 'del' | 'ins' | 'sub' | 'sup' | 'time' | 'address' | 'blockquote' | 'cite' | 'code' | 'abbr';
|
|
8
|
+
variant?: 'section-label';
|
|
8
9
|
size?: '2xs' | 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl';
|
|
9
10
|
weight?: 'normal' | 'medium' | 'semibold';
|
|
10
11
|
color?: 'primary' | 'secondary' | 'tertiary';
|
|
@@ -15,11 +16,12 @@ export interface TextProps extends Omit<React.HTMLAttributes<HTMLElement>, 'colo
|
|
|
15
16
|
lineClamp?: number;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
const TextRoot = React.forwardRef<HTMLElement, TextProps>(
|
|
19
20
|
function Text(
|
|
20
21
|
{
|
|
21
22
|
children,
|
|
22
23
|
as: Component = 'span',
|
|
24
|
+
variant,
|
|
23
25
|
size,
|
|
24
26
|
weight,
|
|
25
27
|
color,
|
|
@@ -34,6 +36,7 @@ export const Text = React.forwardRef<HTMLElement, TextProps>(
|
|
|
34
36
|
) {
|
|
35
37
|
const classes = [
|
|
36
38
|
styles.text,
|
|
39
|
+
variant && styles[`variant-${variant}`],
|
|
37
40
|
size && styles[`size-${size}`],
|
|
38
41
|
weight && styles[`weight-${weight}`],
|
|
39
42
|
color && styles[`color-${color}`],
|
|
@@ -56,3 +59,7 @@ export const Text = React.forwardRef<HTMLElement, TextProps>(
|
|
|
56
59
|
);
|
|
57
60
|
}
|
|
58
61
|
);
|
|
62
|
+
|
|
63
|
+
export const Text = Object.assign(TextRoot, {
|
|
64
|
+
Root: TextRoot,
|
|
65
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { defineFragment } from '@fragments/core';
|
|
3
3
|
import { Textarea } from '.';
|
|
4
4
|
|
|
5
|
-
export default
|
|
5
|
+
export default defineFragment({
|
|
6
6
|
component: Textarea,
|
|
7
7
|
|
|
8
8
|
meta: {
|
|
@@ -52,6 +52,14 @@ export default defineSegment({
|
|
|
52
52
|
default: 3,
|
|
53
53
|
description: 'Number of visible text rows',
|
|
54
54
|
},
|
|
55
|
+
minRows: {
|
|
56
|
+
type: 'number',
|
|
57
|
+
description: 'Minimum number of rows when auto-resizing',
|
|
58
|
+
},
|
|
59
|
+
maxRows: {
|
|
60
|
+
type: 'number',
|
|
61
|
+
description: 'Maximum number of rows when auto-resizing',
|
|
62
|
+
},
|
|
55
63
|
label: {
|
|
56
64
|
type: 'string',
|
|
57
65
|
description: 'Label text above the textarea',
|
|
@@ -49,7 +49,7 @@ function mergeAriaIds(...ids: Array<string | undefined>): string | undefined {
|
|
|
49
49
|
return merged.length > 0 ? merged : undefined;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
const TextareaRoot = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
53
53
|
function Textarea(
|
|
54
54
|
{
|
|
55
55
|
value,
|
|
@@ -143,3 +143,7 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
|
143
143
|
);
|
|
144
144
|
}
|
|
145
145
|
);
|
|
146
|
+
|
|
147
|
+
export const Textarea = Object.assign(TextareaRoot, {
|
|
148
|
+
Root: TextareaRoot,
|
|
149
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { defineFragment } from '@fragments/core';
|
|
3
3
|
import { ThemeProvider, ThemeToggle, useTheme } from '.';
|
|
4
4
|
|
|
5
5
|
// Demo component to show hook usage
|
|
@@ -14,7 +14,7 @@ function ThemeDemo() {
|
|
|
14
14
|
);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export default
|
|
17
|
+
export default defineFragment({
|
|
18
18
|
component: ThemeProvider,
|
|
19
19
|
|
|
20
20
|
meta: {
|
|
@@ -254,7 +254,7 @@ function ThemeProvider({
|
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
/**
|
|
257
|
-
* ThemeToggle -
|
|
257
|
+
* ThemeToggle - Fragmented button group to toggle between light and dark themes
|
|
258
258
|
*
|
|
259
259
|
* Can be used in two modes:
|
|
260
260
|
* 1. Uncontrolled (default): Uses ThemeProvider context to get/set theme
|
|
@@ -340,4 +340,11 @@ function ThemeToggle({
|
|
|
340
340
|
// Exports
|
|
341
341
|
// ============================================
|
|
342
342
|
|
|
343
|
+
export const Theme = Object.assign(ThemeProvider, {
|
|
344
|
+
Root: ThemeProvider,
|
|
345
|
+
Provider: ThemeProvider,
|
|
346
|
+
Toggle: ThemeToggle,
|
|
347
|
+
useTheme,
|
|
348
|
+
});
|
|
349
|
+
|
|
343
350
|
export { ThemeProvider, ThemeToggle, useTheme };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { defineFragment } from '@fragments/core';
|
|
3
3
|
import { ThinkingIndicator } from '.';
|
|
4
4
|
|
|
5
|
-
export default
|
|
5
|
+
export default defineFragment({
|
|
6
6
|
component: ThinkingIndicator,
|
|
7
7
|
|
|
8
8
|
meta: {
|
|
@@ -51,7 +51,8 @@ export default defineSegment({
|
|
|
51
51
|
description: 'Status text',
|
|
52
52
|
},
|
|
53
53
|
variant: {
|
|
54
|
-
type: '
|
|
54
|
+
type: 'enum',
|
|
55
|
+
values: ['dots', 'pulse', 'spinner'],
|
|
55
56
|
default: '"dots"',
|
|
56
57
|
description: 'Animation style',
|
|
57
58
|
},
|
|
@@ -61,7 +62,7 @@ export default defineSegment({
|
|
|
61
62
|
description: 'Show elapsed time',
|
|
62
63
|
},
|
|
63
64
|
steps: {
|
|
64
|
-
type: '
|
|
65
|
+
type: 'array',
|
|
65
66
|
description: 'Multi-step progress array',
|
|
66
67
|
},
|
|
67
68
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { defineFragment } from '@fragments/core';
|
|
3
3
|
import { Toast, ToastProvider, useToast } from '.';
|
|
4
4
|
|
|
5
5
|
// Demo component that triggers toasts
|
|
@@ -45,7 +45,7 @@ function ToastDemoWrapper() {
|
|
|
45
45
|
);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export default
|
|
48
|
+
export default defineFragment({
|
|
49
49
|
component: Toast,
|
|
50
50
|
|
|
51
51
|
meta: {
|
|
@@ -108,6 +108,18 @@ export default defineSegment({
|
|
|
108
108
|
type: 'object',
|
|
109
109
|
description: 'Optional action button { label, onClick }',
|
|
110
110
|
},
|
|
111
|
+
onDismiss: {
|
|
112
|
+
type: 'function',
|
|
113
|
+
description: 'Callback when toast should be dismissed',
|
|
114
|
+
},
|
|
115
|
+
onPause: {
|
|
116
|
+
type: 'function',
|
|
117
|
+
description: 'Callback when auto-dismiss timer should pause',
|
|
118
|
+
},
|
|
119
|
+
onResume: {
|
|
120
|
+
type: 'function',
|
|
121
|
+
description: 'Callback when auto-dismiss timer should resume',
|
|
122
|
+
},
|
|
111
123
|
},
|
|
112
124
|
|
|
113
125
|
relations: [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { defineFragment } from '@fragments/core';
|
|
3
3
|
import { Toggle } from '.';
|
|
4
4
|
|
|
5
5
|
// Stateful wrapper for interactive demos
|
|
@@ -8,7 +8,7 @@ function StatefulToggle(props: React.ComponentProps<typeof Toggle>) {
|
|
|
8
8
|
return <Toggle {...props} checked={checked} onChange={setChecked} />;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export default
|
|
11
|
+
export default defineFragment({
|
|
12
12
|
component: Toggle,
|
|
13
13
|
|
|
14
14
|
meta: {
|
|
@@ -20,7 +20,7 @@ export interface ToggleProps {
|
|
|
20
20
|
'aria-describedby'?: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
const ToggleRoot = React.forwardRef<HTMLButtonElement, ToggleProps>(
|
|
24
24
|
function Toggle(
|
|
25
25
|
{
|
|
26
26
|
checked,
|
|
@@ -90,3 +90,7 @@ export const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
|
|
|
90
90
|
);
|
|
91
91
|
}
|
|
92
92
|
);
|
|
93
|
+
|
|
94
|
+
export const Toggle = Object.assign(ToggleRoot, {
|
|
95
|
+
Root: ToggleRoot,
|
|
96
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { defineFragment } from '@fragments/core';
|
|
3
3
|
import { ToggleGroup } from '.';
|
|
4
4
|
|
|
5
5
|
function DefaultExample() {
|
|
@@ -92,7 +92,7 @@ function DisabledItemExample() {
|
|
|
92
92
|
);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
export default
|
|
95
|
+
export default defineFragment({
|
|
96
96
|
component: ToggleGroup,
|
|
97
97
|
|
|
98
98
|
meta: {
|
|
@@ -100,7 +100,7 @@ export default defineSegment({
|
|
|
100
100
|
description: 'A group of toggle buttons where only one can be selected at a time. Useful for switching between views, modes, or options.',
|
|
101
101
|
category: 'forms',
|
|
102
102
|
status: 'stable',
|
|
103
|
-
tags: ['toggle', 'group', '
|
|
103
|
+
tags: ['toggle', 'group', 'fragmented', 'control', 'tabs', 'switch'],
|
|
104
104
|
since: '0.2.0',
|
|
105
105
|
},
|
|
106
106
|
|
|
@@ -108,7 +108,7 @@ export default defineSegment({
|
|
|
108
108
|
when: [
|
|
109
109
|
'Switching between mutually exclusive views or modes',
|
|
110
110
|
'Selecting one option from a small set (2-5 options)',
|
|
111
|
-
'
|
|
111
|
+
'Fragmented controls like view switchers',
|
|
112
112
|
'Filter or sort options',
|
|
113
113
|
],
|
|
114
114
|
whenNot: [
|
|
@@ -185,7 +185,7 @@ export default defineSegment({
|
|
|
185
185
|
scenarioTags: [
|
|
186
186
|
'forms.selection',
|
|
187
187
|
'input.toggle',
|
|
188
|
-
'control.
|
|
188
|
+
'control.fragmented',
|
|
189
189
|
],
|
|
190
190
|
a11yRules: ['A11Y_GROUP_ROLE', 'A11Y_KEYBOARD_ACCESSIBLE'],
|
|
191
191
|
},
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { defineFragment } from '@fragments/core';
|
|
3
3
|
import { Tooltip } from '.';
|
|
4
4
|
import { Button } from '../Button';
|
|
5
5
|
|
|
6
|
-
export default
|
|
6
|
+
export default defineFragment({
|
|
7
7
|
component: Tooltip,
|
|
8
8
|
|
|
9
9
|
meta: {
|
|
@@ -74,6 +74,11 @@ export default defineSegment({
|
|
|
74
74
|
description: 'Delay before showing (ms)',
|
|
75
75
|
default: '400',
|
|
76
76
|
},
|
|
77
|
+
closeDelay: {
|
|
78
|
+
type: 'number',
|
|
79
|
+
description: 'Delay before hiding (ms)',
|
|
80
|
+
default: '0',
|
|
81
|
+
},
|
|
77
82
|
arrow: {
|
|
78
83
|
type: 'boolean',
|
|
79
84
|
description: 'Show arrow pointing to trigger',
|
|
@@ -84,6 +89,19 @@ export default defineSegment({
|
|
|
84
89
|
description: 'Disable the tooltip',
|
|
85
90
|
default: 'false',
|
|
86
91
|
},
|
|
92
|
+
open: {
|
|
93
|
+
type: 'boolean',
|
|
94
|
+
description: 'Controlled open state',
|
|
95
|
+
},
|
|
96
|
+
defaultOpen: {
|
|
97
|
+
type: 'boolean',
|
|
98
|
+
description: 'Default open state',
|
|
99
|
+
default: 'false',
|
|
100
|
+
},
|
|
101
|
+
onOpenChange: {
|
|
102
|
+
type: 'function',
|
|
103
|
+
description: 'Callback when open state changes',
|
|
104
|
+
},
|
|
87
105
|
},
|
|
88
106
|
|
|
89
107
|
relations: [
|