@fragments-sdk/ui 0.8.7 → 0.8.8
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/fragments.json +1 -1
- package/package.json +2 -2
- package/src/assets/fragments-logo.tsx +9 -8
- package/src/blocks/CommandPalette.block.ts +34 -0
- package/src/blocks/PaginatedTable.block.ts +36 -0
- package/src/blocks/SettingsDrawer.block.ts +47 -0
- package/src/components/Command/Command.fragment.tsx +237 -0
- package/src/components/Command/Command.module.scss +153 -0
- package/src/components/Command/Command.test.tsx +363 -0
- package/src/components/Command/index.tsx +502 -0
- package/src/components/Drawer/Drawer.fragment.tsx +206 -0
- package/src/components/Drawer/Drawer.module.scss +215 -0
- package/src/components/Drawer/Drawer.test.tsx +227 -0
- package/src/components/Drawer/index.tsx +239 -0
- package/src/components/Pagination/Pagination.fragment.tsx +152 -0
- package/src/components/Pagination/Pagination.module.scss +109 -0
- package/src/components/Pagination/Pagination.test.tsx +171 -0
- package/src/components/Pagination/index.tsx +360 -0
- package/src/components/Tooltip/index.tsx +25 -1
- package/src/index.ts +34 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
@use '../../tokens/mixins' as *;
|
|
3
|
+
|
|
4
|
+
// Backdrop overlay
|
|
5
|
+
.backdrop {
|
|
6
|
+
position: fixed;
|
|
7
|
+
inset: 0;
|
|
8
|
+
background-color: var(--fui-backdrop, $fui-backdrop);
|
|
9
|
+
z-index: 50;
|
|
10
|
+
|
|
11
|
+
// Animation
|
|
12
|
+
opacity: 0;
|
|
13
|
+
transition: opacity var(--fui-transition-normal, $fui-transition-normal);
|
|
14
|
+
|
|
15
|
+
&[data-open] {
|
|
16
|
+
opacity: 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
&[data-starting-style],
|
|
20
|
+
&[data-ending-style] {
|
|
21
|
+
opacity: 0;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The popup panel
|
|
26
|
+
.popup {
|
|
27
|
+
@include surface-elevated;
|
|
28
|
+
@include text-base;
|
|
29
|
+
|
|
30
|
+
position: fixed;
|
|
31
|
+
z-index: 51;
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
box-shadow: var(--fui-shadow-md, $fui-shadow-md);
|
|
35
|
+
overflow-y: auto;
|
|
36
|
+
|
|
37
|
+
// Animation
|
|
38
|
+
transition:
|
|
39
|
+
opacity var(--fui-transition-normal, $fui-transition-normal),
|
|
40
|
+
transform var(--fui-transition-normal, $fui-transition-normal);
|
|
41
|
+
opacity: 0;
|
|
42
|
+
|
|
43
|
+
&[data-open] {
|
|
44
|
+
opacity: 1;
|
|
45
|
+
transform: translate(0, 0);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================
|
|
50
|
+
// Side positioning
|
|
51
|
+
// ============================================
|
|
52
|
+
|
|
53
|
+
.side-right {
|
|
54
|
+
top: 0;
|
|
55
|
+
right: 0;
|
|
56
|
+
bottom: 0;
|
|
57
|
+
transform: translateX(100%);
|
|
58
|
+
|
|
59
|
+
&[data-starting-style],
|
|
60
|
+
&[data-ending-style] {
|
|
61
|
+
transform: translateX(100%);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.side-left {
|
|
66
|
+
top: 0;
|
|
67
|
+
left: 0;
|
|
68
|
+
bottom: 0;
|
|
69
|
+
transform: translateX(-100%);
|
|
70
|
+
|
|
71
|
+
&[data-starting-style],
|
|
72
|
+
&[data-ending-style] {
|
|
73
|
+
transform: translateX(-100%);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.side-top {
|
|
78
|
+
top: 0;
|
|
79
|
+
left: 0;
|
|
80
|
+
right: 0;
|
|
81
|
+
transform: translateY(-100%);
|
|
82
|
+
|
|
83
|
+
&[data-starting-style],
|
|
84
|
+
&[data-ending-style] {
|
|
85
|
+
transform: translateY(-100%);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.side-bottom {
|
|
90
|
+
bottom: 0;
|
|
91
|
+
left: 0;
|
|
92
|
+
right: 0;
|
|
93
|
+
transform: translateY(100%);
|
|
94
|
+
|
|
95
|
+
&[data-starting-style],
|
|
96
|
+
&[data-ending-style] {
|
|
97
|
+
transform: translateY(100%);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ============================================
|
|
102
|
+
// Size variants — width for left/right, height for top/bottom
|
|
103
|
+
// ============================================
|
|
104
|
+
|
|
105
|
+
// Left/Right: size maps to width
|
|
106
|
+
.side-left,
|
|
107
|
+
.side-right {
|
|
108
|
+
&.size-sm { width: 20rem; }
|
|
109
|
+
&.size-md { width: 24rem; }
|
|
110
|
+
&.size-lg { width: 32rem; }
|
|
111
|
+
&.size-xl { width: 48rem; }
|
|
112
|
+
&.size-full { width: 100vw; }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Top/Bottom: size maps to height
|
|
116
|
+
.side-top,
|
|
117
|
+
.side-bottom {
|
|
118
|
+
&.size-sm { height: 25vh; }
|
|
119
|
+
&.size-md { height: 40vh; }
|
|
120
|
+
&.size-lg { height: 60vh; }
|
|
121
|
+
&.size-xl { height: 80vh; }
|
|
122
|
+
&.size-full { height: 100vh; }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============================================
|
|
126
|
+
// Content sections (mirror Dialog)
|
|
127
|
+
// ============================================
|
|
128
|
+
|
|
129
|
+
// Header area
|
|
130
|
+
.header {
|
|
131
|
+
padding: var(--fui-padding-container-sm, $fui-padding-container-sm) var(--fui-padding-container-md, $fui-padding-container-md);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Title
|
|
135
|
+
.title {
|
|
136
|
+
margin: 0;
|
|
137
|
+
font-size: var(--fui-font-size-lg, $fui-font-size-lg);
|
|
138
|
+
font-weight: var(--fui-font-weight-semibold, $fui-font-weight-semibold);
|
|
139
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
140
|
+
line-height: var(--fui-line-height-tight, $fui-line-height-tight);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Description
|
|
144
|
+
.description {
|
|
145
|
+
margin: var(--fui-space-1, $fui-space-1) 0 0;
|
|
146
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
147
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
148
|
+
line-height: var(--fui-line-height-normal, $fui-line-height-normal);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Body content
|
|
152
|
+
.body {
|
|
153
|
+
flex: 1;
|
|
154
|
+
padding: 0 var(--fui-padding-container-md, $fui-padding-container-md) var(--fui-padding-container-sm, $fui-padding-container-sm);
|
|
155
|
+
overflow-y: auto;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Footer for actions
|
|
159
|
+
.footer {
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
justify-content: flex-end;
|
|
163
|
+
gap: var(--fui-space-1, $fui-space-1);
|
|
164
|
+
padding: var(--fui-padding-inline-sm, $fui-padding-inline-sm) var(--fui-padding-container-md, $fui-padding-container-md);
|
|
165
|
+
border-top: 1px solid var(--fui-border, $fui-border);
|
|
166
|
+
background-color: var(--fui-bg-secondary, $fui-bg-secondary);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Close button (X in corner)
|
|
170
|
+
.close {
|
|
171
|
+
@include button-reset;
|
|
172
|
+
@include interactive-base;
|
|
173
|
+
|
|
174
|
+
position: absolute;
|
|
175
|
+
top: var(--fui-space-3, $fui-space-3);
|
|
176
|
+
right: var(--fui-space-3, $fui-space-3);
|
|
177
|
+
display: flex;
|
|
178
|
+
align-items: center;
|
|
179
|
+
justify-content: center;
|
|
180
|
+
width: 2rem;
|
|
181
|
+
height: 2rem;
|
|
182
|
+
border-radius: var(--fui-radius-md, $fui-radius-md);
|
|
183
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
184
|
+
z-index: 1;
|
|
185
|
+
|
|
186
|
+
&:hover {
|
|
187
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
188
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
svg {
|
|
192
|
+
width: 1rem;
|
|
193
|
+
height: 1rem;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ============================================
|
|
198
|
+
// Accessibility: Reduced Motion
|
|
199
|
+
// ============================================
|
|
200
|
+
|
|
201
|
+
@media (prefers-reduced-motion: reduce) {
|
|
202
|
+
.backdrop {
|
|
203
|
+
transition: none;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.popup {
|
|
207
|
+
transition: none;
|
|
208
|
+
transform: none;
|
|
209
|
+
|
|
210
|
+
&[data-starting-style],
|
|
211
|
+
&[data-ending-style] {
|
|
212
|
+
transform: none;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render, screen, userEvent, waitFor, expectNoA11yViolations } from '../../test/utils';
|
|
3
|
+
import { Drawer } from './index';
|
|
4
|
+
|
|
5
|
+
function renderDrawer(
|
|
6
|
+
props: Partial<React.ComponentProps<typeof Drawer>> = {},
|
|
7
|
+
contentProps: Partial<React.ComponentProps<typeof Drawer.Content>> = {},
|
|
8
|
+
) {
|
|
9
|
+
return render(
|
|
10
|
+
<Drawer {...props}>
|
|
11
|
+
<Drawer.Trigger>Open Drawer</Drawer.Trigger>
|
|
12
|
+
<Drawer.Content {...contentProps}>
|
|
13
|
+
<Drawer.Header>
|
|
14
|
+
<Drawer.Title>Drawer Title</Drawer.Title>
|
|
15
|
+
<Drawer.Close />
|
|
16
|
+
</Drawer.Header>
|
|
17
|
+
<Drawer.Body>
|
|
18
|
+
<Drawer.Description>Drawer description text</Drawer.Description>
|
|
19
|
+
<p>Body content</p>
|
|
20
|
+
</Drawer.Body>
|
|
21
|
+
<Drawer.Footer>
|
|
22
|
+
<Drawer.Close asChild>
|
|
23
|
+
<button>Cancel</button>
|
|
24
|
+
</Drawer.Close>
|
|
25
|
+
</Drawer.Footer>
|
|
26
|
+
</Drawer.Content>
|
|
27
|
+
</Drawer>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('Drawer', () => {
|
|
32
|
+
it('opens when trigger is clicked', async () => {
|
|
33
|
+
const user = userEvent.setup();
|
|
34
|
+
renderDrawer();
|
|
35
|
+
|
|
36
|
+
expect(screen.queryByText('Drawer Title')).not.toBeInTheDocument();
|
|
37
|
+
|
|
38
|
+
await user.click(screen.getByRole('button', { name: /open drawer/i }));
|
|
39
|
+
await waitFor(() => {
|
|
40
|
+
expect(screen.getByText('Drawer Title')).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('closes when close button is clicked', async () => {
|
|
45
|
+
const user = userEvent.setup();
|
|
46
|
+
renderDrawer({ defaultOpen: true });
|
|
47
|
+
|
|
48
|
+
await waitFor(() => {
|
|
49
|
+
expect(screen.getByText('Drawer Title')).toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const closeButton = screen.getByRole('button', { name: /close drawer/i });
|
|
53
|
+
await user.click(closeButton);
|
|
54
|
+
|
|
55
|
+
await waitFor(() => {
|
|
56
|
+
expect(screen.queryByText('Drawer Title')).not.toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('renders title and description', async () => {
|
|
61
|
+
renderDrawer({ defaultOpen: true });
|
|
62
|
+
|
|
63
|
+
await waitFor(() => {
|
|
64
|
+
expect(screen.getByText('Drawer Title')).toBeInTheDocument();
|
|
65
|
+
expect(screen.getByText('Drawer description text')).toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('renders compound sub-components (Header, Body, Footer)', async () => {
|
|
70
|
+
renderDrawer({ defaultOpen: true });
|
|
71
|
+
|
|
72
|
+
await waitFor(() => {
|
|
73
|
+
expect(screen.getByText('Body content')).toBeInTheDocument();
|
|
74
|
+
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('supports side prop', async () => {
|
|
79
|
+
renderDrawer({ defaultOpen: true }, { side: 'left' });
|
|
80
|
+
|
|
81
|
+
await waitFor(() => {
|
|
82
|
+
expect(screen.getByText('Drawer Title')).toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('supports size prop', async () => {
|
|
87
|
+
renderDrawer({ defaultOpen: true }, { size: 'lg' });
|
|
88
|
+
|
|
89
|
+
await waitFor(() => {
|
|
90
|
+
expect(screen.getByText('Drawer Title')).toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('has no accessibility violations when open', async () => {
|
|
95
|
+
const { container } = renderDrawer({ defaultOpen: true });
|
|
96
|
+
|
|
97
|
+
await waitFor(() => {
|
|
98
|
+
expect(screen.getByText('Drawer Title')).toBeInTheDocument();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await expectNoA11yViolations(container, {
|
|
102
|
+
// Base UI focus guard spans have role="button" without labels.
|
|
103
|
+
disabledRules: ['aria-command-name'],
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('keyboard & focus', () => {
|
|
108
|
+
it('Escape closes drawer', async () => {
|
|
109
|
+
const user = userEvent.setup();
|
|
110
|
+
renderDrawer({ defaultOpen: true });
|
|
111
|
+
|
|
112
|
+
await waitFor(() => {
|
|
113
|
+
expect(screen.getByText('Drawer Title')).toBeInTheDocument();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await user.keyboard('{Escape}');
|
|
117
|
+
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
expect(screen.queryByText('Drawer Title')).not.toBeInTheDocument();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('focus moves into drawer on open', async () => {
|
|
124
|
+
const user = userEvent.setup();
|
|
125
|
+
renderDrawer();
|
|
126
|
+
|
|
127
|
+
await user.click(screen.getByRole('button', { name: /open drawer/i }));
|
|
128
|
+
|
|
129
|
+
await waitFor(() => {
|
|
130
|
+
expect(screen.getByText('Drawer Title')).toBeInTheDocument();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await waitFor(() => {
|
|
134
|
+
const dialog = screen.getByRole('dialog');
|
|
135
|
+
expect(dialog.contains(document.activeElement)).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('focus returns to trigger on close', async () => {
|
|
140
|
+
const user = userEvent.setup();
|
|
141
|
+
renderDrawer();
|
|
142
|
+
|
|
143
|
+
const trigger = screen.getByRole('button', { name: /open drawer/i });
|
|
144
|
+
await user.click(trigger);
|
|
145
|
+
|
|
146
|
+
await waitFor(() => {
|
|
147
|
+
expect(screen.getByText('Drawer Title')).toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await user.keyboard('{Escape}');
|
|
151
|
+
|
|
152
|
+
await waitFor(() => {
|
|
153
|
+
expect(screen.queryByText('Drawer Title')).not.toBeInTheDocument();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await waitFor(() => {
|
|
157
|
+
expect(trigger).toHaveFocus();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('Tab cycles within drawer (focus trap)', async () => {
|
|
162
|
+
const user = userEvent.setup();
|
|
163
|
+
renderDrawer({ defaultOpen: true });
|
|
164
|
+
|
|
165
|
+
await waitFor(() => {
|
|
166
|
+
expect(screen.getByText('Drawer Title')).toBeInTheDocument();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const dialog = screen.getByRole('dialog');
|
|
170
|
+
|
|
171
|
+
// Tab through focusable elements — focus should stay inside drawer
|
|
172
|
+
await user.tab();
|
|
173
|
+
await user.tab();
|
|
174
|
+
await user.tab();
|
|
175
|
+
|
|
176
|
+
await waitFor(() => {
|
|
177
|
+
expect(dialog.contains(document.activeElement)).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('Shift+Tab cycles backward', async () => {
|
|
182
|
+
const user = userEvent.setup();
|
|
183
|
+
renderDrawer({ defaultOpen: true });
|
|
184
|
+
|
|
185
|
+
await waitFor(() => {
|
|
186
|
+
expect(screen.getByText('Drawer Title')).toBeInTheDocument();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const dialog = screen.getByRole('dialog');
|
|
190
|
+
|
|
191
|
+
await user.keyboard('{Shift>}{Tab}{/Shift}');
|
|
192
|
+
await user.keyboard('{Shift>}{Tab}{/Shift}');
|
|
193
|
+
await user.keyboard('{Shift>}{Tab}{/Shift}');
|
|
194
|
+
|
|
195
|
+
await waitFor(() => {
|
|
196
|
+
expect(dialog.contains(document.activeElement)).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('role="dialog" is present when open', async () => {
|
|
201
|
+
renderDrawer({ defaultOpen: true });
|
|
202
|
+
|
|
203
|
+
await waitFor(() => {
|
|
204
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('backdrop click closes drawer', async () => {
|
|
209
|
+
const user = userEvent.setup();
|
|
210
|
+
renderDrawer({ defaultOpen: true });
|
|
211
|
+
|
|
212
|
+
await waitFor(() => {
|
|
213
|
+
expect(screen.getByText('Drawer Title')).toBeInTheDocument();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Click the backdrop (outside the drawer content)
|
|
217
|
+
const backdrop = document.querySelector('[data-open]');
|
|
218
|
+
if (backdrop) {
|
|
219
|
+
await user.click(backdrop as HTMLElement);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await waitFor(() => {
|
|
223
|
+
expect(screen.queryByText('Drawer Title')).not.toBeInTheDocument();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
|
|
3
|
+
import styles from './Drawer.module.scss';
|
|
4
|
+
// Import globals to ensure CSS variables are defined
|
|
5
|
+
import '../../styles/globals.scss';
|
|
6
|
+
|
|
7
|
+
// ============================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================
|
|
10
|
+
|
|
11
|
+
export interface DrawerProps {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
open?: boolean;
|
|
14
|
+
defaultOpen?: boolean;
|
|
15
|
+
onOpenChange?: (open: boolean) => void;
|
|
16
|
+
modal?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DrawerContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
side?: 'left' | 'right' | 'top' | 'bottom';
|
|
22
|
+
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DrawerTriggerProps {
|
|
26
|
+
children: React.ReactNode;
|
|
27
|
+
asChild?: boolean;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DrawerHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
32
|
+
children: React.ReactNode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DrawerTitleProps {
|
|
36
|
+
children: React.ReactNode;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DrawerDescriptionProps {
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
className?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface DrawerBodyProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
46
|
+
children: React.ReactNode;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DrawerFooterProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
50
|
+
children: React.ReactNode;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface DrawerCloseProps {
|
|
54
|
+
children?: React.ReactNode;
|
|
55
|
+
asChild?: boolean;
|
|
56
|
+
className?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================
|
|
60
|
+
// Close Icon
|
|
61
|
+
// ============================================
|
|
62
|
+
|
|
63
|
+
function CloseIcon() {
|
|
64
|
+
return (
|
|
65
|
+
<svg
|
|
66
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
67
|
+
width="16"
|
|
68
|
+
height="16"
|
|
69
|
+
viewBox="0 0 24 24"
|
|
70
|
+
fill="none"
|
|
71
|
+
stroke="currentColor"
|
|
72
|
+
strokeWidth="2"
|
|
73
|
+
strokeLinecap="round"
|
|
74
|
+
strokeLinejoin="round"
|
|
75
|
+
aria-hidden="true"
|
|
76
|
+
>
|
|
77
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
78
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
79
|
+
</svg>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ============================================
|
|
84
|
+
// Components
|
|
85
|
+
// ============================================
|
|
86
|
+
|
|
87
|
+
function DrawerRoot({
|
|
88
|
+
children,
|
|
89
|
+
open,
|
|
90
|
+
defaultOpen,
|
|
91
|
+
onOpenChange,
|
|
92
|
+
modal = true,
|
|
93
|
+
}: DrawerProps) {
|
|
94
|
+
return (
|
|
95
|
+
<BaseDialog.Root
|
|
96
|
+
open={open}
|
|
97
|
+
defaultOpen={defaultOpen}
|
|
98
|
+
onOpenChange={onOpenChange}
|
|
99
|
+
modal={modal}
|
|
100
|
+
>
|
|
101
|
+
{children}
|
|
102
|
+
</BaseDialog.Root>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function DrawerTrigger({
|
|
107
|
+
children,
|
|
108
|
+
asChild,
|
|
109
|
+
className,
|
|
110
|
+
}: DrawerTriggerProps) {
|
|
111
|
+
if (asChild) {
|
|
112
|
+
return (
|
|
113
|
+
<BaseDialog.Trigger className={className} render={children as React.ReactElement}>
|
|
114
|
+
{null}
|
|
115
|
+
</BaseDialog.Trigger>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<BaseDialog.Trigger className={className}>
|
|
121
|
+
{children}
|
|
122
|
+
</BaseDialog.Trigger>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function DrawerContent({
|
|
127
|
+
children,
|
|
128
|
+
side = 'right',
|
|
129
|
+
size = 'md',
|
|
130
|
+
className,
|
|
131
|
+
...htmlProps
|
|
132
|
+
}: DrawerContentProps) {
|
|
133
|
+
const popupClasses = [
|
|
134
|
+
styles.popup,
|
|
135
|
+
styles[`side-${side}`],
|
|
136
|
+
styles[`size-${size}`],
|
|
137
|
+
className,
|
|
138
|
+
]
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.join(' ');
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<BaseDialog.Portal>
|
|
144
|
+
<BaseDialog.Backdrop className={styles.backdrop} />
|
|
145
|
+
<BaseDialog.Popup initialFocus {...htmlProps} data-side={side} className={popupClasses}>
|
|
146
|
+
{children}
|
|
147
|
+
</BaseDialog.Popup>
|
|
148
|
+
</BaseDialog.Portal>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function DrawerHeader({ children, className, ...htmlProps }: DrawerHeaderProps) {
|
|
153
|
+
const classes = [styles.header, className].filter(Boolean).join(' ');
|
|
154
|
+
return <div {...htmlProps} className={classes}>{children}</div>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function DrawerTitle({ children, className }: DrawerTitleProps) {
|
|
158
|
+
const classes = [styles.title, className].filter(Boolean).join(' ');
|
|
159
|
+
return <BaseDialog.Title className={classes}>{children}</BaseDialog.Title>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function DrawerDescription({ children, className }: DrawerDescriptionProps) {
|
|
163
|
+
const classes = [styles.description, className].filter(Boolean).join(' ');
|
|
164
|
+
return (
|
|
165
|
+
<BaseDialog.Description className={classes}>
|
|
166
|
+
{children}
|
|
167
|
+
</BaseDialog.Description>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function DrawerBody({ children, className, ...htmlProps }: DrawerBodyProps) {
|
|
172
|
+
const classes = [styles.body, className].filter(Boolean).join(' ');
|
|
173
|
+
return <div {...htmlProps} className={classes}>{children}</div>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function DrawerFooter({ children, className, ...htmlProps }: DrawerFooterProps) {
|
|
177
|
+
const classes = [styles.footer, className].filter(Boolean).join(' ');
|
|
178
|
+
return <div {...htmlProps} className={classes}>{children}</div>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function DrawerClose({ children, asChild, className }: DrawerCloseProps) {
|
|
182
|
+
if (!children) {
|
|
183
|
+
return (
|
|
184
|
+
<BaseDialog.Close
|
|
185
|
+
data-drawer-close
|
|
186
|
+
aria-label="Close drawer"
|
|
187
|
+
className={[styles.close, className].filter(Boolean).join(' ')}
|
|
188
|
+
>
|
|
189
|
+
<CloseIcon />
|
|
190
|
+
</BaseDialog.Close>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (asChild) {
|
|
195
|
+
return (
|
|
196
|
+
<BaseDialog.Close
|
|
197
|
+
data-drawer-close
|
|
198
|
+
className={className}
|
|
199
|
+
render={children as React.ReactElement}
|
|
200
|
+
>
|
|
201
|
+
{null}
|
|
202
|
+
</BaseDialog.Close>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<BaseDialog.Close data-drawer-close className={className}>
|
|
208
|
+
{children}
|
|
209
|
+
</BaseDialog.Close>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ============================================
|
|
214
|
+
// Export compound component
|
|
215
|
+
// ============================================
|
|
216
|
+
|
|
217
|
+
export const Drawer = Object.assign(DrawerRoot, {
|
|
218
|
+
Trigger: DrawerTrigger,
|
|
219
|
+
Content: DrawerContent,
|
|
220
|
+
Header: DrawerHeader,
|
|
221
|
+
Title: DrawerTitle,
|
|
222
|
+
Description: DrawerDescription,
|
|
223
|
+
Body: DrawerBody,
|
|
224
|
+
Footer: DrawerFooter,
|
|
225
|
+
Close: DrawerClose,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Re-export individual components for tree-shaking
|
|
229
|
+
export {
|
|
230
|
+
DrawerRoot,
|
|
231
|
+
DrawerTrigger,
|
|
232
|
+
DrawerContent,
|
|
233
|
+
DrawerHeader,
|
|
234
|
+
DrawerTitle,
|
|
235
|
+
DrawerDescription,
|
|
236
|
+
DrawerBody,
|
|
237
|
+
DrawerFooter,
|
|
238
|
+
DrawerClose,
|
|
239
|
+
};
|