@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,190 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
@use '../../tokens/mixins' as *;
|
|
3
|
+
|
|
4
|
+
// Positioner
|
|
5
|
+
.positioner {
|
|
6
|
+
z-index: 50;
|
|
7
|
+
outline: none;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Popup container
|
|
11
|
+
.popup {
|
|
12
|
+
@include surface-elevated;
|
|
13
|
+
|
|
14
|
+
min-width: 12rem;
|
|
15
|
+
padding: var(--fui-space-1, $fui-space-1);
|
|
16
|
+
box-shadow: var(--fui-shadow-md, $fui-shadow-md);
|
|
17
|
+
|
|
18
|
+
// Animation
|
|
19
|
+
opacity: 0;
|
|
20
|
+
transform: scale(0.95);
|
|
21
|
+
transform-origin: var(--transform-origin);
|
|
22
|
+
transition:
|
|
23
|
+
opacity var(--fui-transition-fast, $fui-transition-fast),
|
|
24
|
+
transform var(--fui-transition-fast, $fui-transition-fast);
|
|
25
|
+
|
|
26
|
+
&[data-open] {
|
|
27
|
+
opacity: 1;
|
|
28
|
+
transform: scale(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
&[data-starting-style],
|
|
32
|
+
&[data-ending-style] {
|
|
33
|
+
opacity: 0;
|
|
34
|
+
transform: scale(0.95);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Menu item (shared base)
|
|
39
|
+
.item {
|
|
40
|
+
@include button-reset;
|
|
41
|
+
@include text-base;
|
|
42
|
+
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
gap: var(--fui-space-2, $fui-space-2);
|
|
46
|
+
width: 100%;
|
|
47
|
+
padding: var(--fui-space-2, $fui-space-2) var(--fui-space-3, $fui-space-3);
|
|
48
|
+
border-radius: var(--fui-radius-sm, $fui-radius-sm);
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
outline: none;
|
|
51
|
+
|
|
52
|
+
&[data-highlighted] {
|
|
53
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
&[data-disabled] {
|
|
57
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
58
|
+
cursor: not-allowed;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Danger variant for destructive actions
|
|
63
|
+
.itemDanger {
|
|
64
|
+
color: var(--fui-color-danger, $fui-color-danger);
|
|
65
|
+
|
|
66
|
+
&[data-highlighted] {
|
|
67
|
+
background-color: var(--fui-color-danger-bg, $fui-color-danger-bg);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Item with icon
|
|
72
|
+
.itemIcon {
|
|
73
|
+
display: flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
justify-content: center;
|
|
76
|
+
width: 1rem;
|
|
77
|
+
height: 1rem;
|
|
78
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
79
|
+
|
|
80
|
+
.itemDanger & {
|
|
81
|
+
color: var(--fui-color-danger, $fui-color-danger);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
svg {
|
|
85
|
+
width: 1rem;
|
|
86
|
+
height: 1rem;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Item label (main text)
|
|
91
|
+
.itemLabel {
|
|
92
|
+
flex: 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Item shortcut (keyboard shortcut hint)
|
|
96
|
+
.itemShortcut {
|
|
97
|
+
font-size: var(--fui-font-size-xs, $fui-font-size-xs);
|
|
98
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
99
|
+
margin-left: auto;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Checkbox/Radio indicator
|
|
103
|
+
.itemIndicator {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: center;
|
|
107
|
+
width: 1rem;
|
|
108
|
+
height: 1rem;
|
|
109
|
+
|
|
110
|
+
svg {
|
|
111
|
+
width: 0.75rem;
|
|
112
|
+
height: 0.75rem;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Checkbox item states
|
|
117
|
+
.checkboxItem {
|
|
118
|
+
&[data-checked] .itemIndicator {
|
|
119
|
+
color: var(--fui-color-accent, $fui-color-accent);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Radio item states
|
|
124
|
+
.radioItem {
|
|
125
|
+
&[data-checked] .itemIndicator {
|
|
126
|
+
color: var(--fui-color-accent, $fui-color-accent);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Separator
|
|
131
|
+
.separator {
|
|
132
|
+
height: 1px;
|
|
133
|
+
margin: var(--fui-space-1, $fui-space-1) 0;
|
|
134
|
+
background-color: var(--fui-border, $fui-border);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Group container
|
|
138
|
+
.group {
|
|
139
|
+
// No specific styles needed
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Group label
|
|
143
|
+
.groupLabel {
|
|
144
|
+
padding: var(--fui-space-1, $fui-space-1) var(--fui-space-3, $fui-space-3);
|
|
145
|
+
font-size: var(--fui-font-size-xs, $fui-font-size-xs);
|
|
146
|
+
font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
|
|
147
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Arrow
|
|
151
|
+
.arrow {
|
|
152
|
+
width: 10px;
|
|
153
|
+
height: 10px;
|
|
154
|
+
transform: rotate(45deg);
|
|
155
|
+
background-color: var(--fui-bg-elevated, $fui-bg-elevated);
|
|
156
|
+
border: 1px solid var(--fui-border, $fui-border);
|
|
157
|
+
|
|
158
|
+
&[data-side='top'] {
|
|
159
|
+
border-top: none;
|
|
160
|
+
border-left: none;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
&[data-side='bottom'] {
|
|
164
|
+
border-bottom: none;
|
|
165
|
+
border-right: none;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
&[data-side='left'] {
|
|
169
|
+
border-left: none;
|
|
170
|
+
border-bottom: none;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
&[data-side='right'] {
|
|
174
|
+
border-right: none;
|
|
175
|
+
border-top: none;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Submenu trigger (has arrow on right)
|
|
180
|
+
.submenuTrigger {
|
|
181
|
+
&::after {
|
|
182
|
+
content: '';
|
|
183
|
+
margin-left: auto;
|
|
184
|
+
width: 0;
|
|
185
|
+
height: 0;
|
|
186
|
+
border-style: solid;
|
|
187
|
+
border-width: 4px 0 4px 6px;
|
|
188
|
+
border-color: transparent transparent transparent var(--fui-text-secondary, $fui-text-secondary);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Menu as BaseMenu } from '@base-ui/react/menu';
|
|
3
|
+
import styles from './Menu.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 MenuProps {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
open?: boolean;
|
|
14
|
+
defaultOpen?: boolean;
|
|
15
|
+
onOpenChange?: (open: boolean) => void;
|
|
16
|
+
modal?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MenuTriggerProps {
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
asChild?: boolean;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MenuContentProps {
|
|
26
|
+
children: React.ReactNode;
|
|
27
|
+
className?: string;
|
|
28
|
+
side?: 'top' | 'bottom' | 'left' | 'right';
|
|
29
|
+
align?: 'start' | 'center' | 'end';
|
|
30
|
+
sideOffset?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface MenuItemProps {
|
|
34
|
+
children: React.ReactNode;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
danger?: boolean;
|
|
37
|
+
onSelect?: () => void;
|
|
38
|
+
className?: string;
|
|
39
|
+
icon?: React.ReactNode;
|
|
40
|
+
shortcut?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MenuCheckboxItemProps {
|
|
44
|
+
children: React.ReactNode;
|
|
45
|
+
checked?: boolean;
|
|
46
|
+
defaultChecked?: boolean;
|
|
47
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
className?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface MenuRadioGroupProps {
|
|
53
|
+
children: React.ReactNode;
|
|
54
|
+
value?: string;
|
|
55
|
+
defaultValue?: string;
|
|
56
|
+
onValueChange?: (value: string) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface MenuRadioItemProps {
|
|
60
|
+
children: React.ReactNode;
|
|
61
|
+
value: string;
|
|
62
|
+
disabled?: boolean;
|
|
63
|
+
className?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface MenuGroupProps {
|
|
67
|
+
children: React.ReactNode;
|
|
68
|
+
className?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface MenuGroupLabelProps {
|
|
72
|
+
children: React.ReactNode;
|
|
73
|
+
className?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface MenuSeparatorProps {
|
|
77
|
+
className?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ============================================
|
|
81
|
+
// Icons
|
|
82
|
+
// ============================================
|
|
83
|
+
|
|
84
|
+
function CheckIcon() {
|
|
85
|
+
return (
|
|
86
|
+
<svg
|
|
87
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
88
|
+
width="12"
|
|
89
|
+
height="12"
|
|
90
|
+
viewBox="0 0 24 24"
|
|
91
|
+
fill="none"
|
|
92
|
+
stroke="currentColor"
|
|
93
|
+
strokeWidth="3"
|
|
94
|
+
strokeLinecap="round"
|
|
95
|
+
strokeLinejoin="round"
|
|
96
|
+
aria-hidden="true"
|
|
97
|
+
>
|
|
98
|
+
<polyline points="20 6 9 17 4 12" />
|
|
99
|
+
</svg>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function DotIcon() {
|
|
104
|
+
return (
|
|
105
|
+
<svg
|
|
106
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
107
|
+
width="12"
|
|
108
|
+
height="12"
|
|
109
|
+
viewBox="0 0 24 24"
|
|
110
|
+
fill="currentColor"
|
|
111
|
+
aria-hidden="true"
|
|
112
|
+
>
|
|
113
|
+
<circle cx="12" cy="12" r="5" />
|
|
114
|
+
</svg>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ============================================
|
|
119
|
+
// Components
|
|
120
|
+
// ============================================
|
|
121
|
+
|
|
122
|
+
function MenuRoot({
|
|
123
|
+
children,
|
|
124
|
+
open,
|
|
125
|
+
defaultOpen,
|
|
126
|
+
onOpenChange,
|
|
127
|
+
modal = true,
|
|
128
|
+
}: MenuProps) {
|
|
129
|
+
return (
|
|
130
|
+
<BaseMenu.Root
|
|
131
|
+
open={open}
|
|
132
|
+
defaultOpen={defaultOpen}
|
|
133
|
+
onOpenChange={onOpenChange}
|
|
134
|
+
modal={modal}
|
|
135
|
+
>
|
|
136
|
+
{children}
|
|
137
|
+
</BaseMenu.Root>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function MenuTrigger({ children, asChild, className }: MenuTriggerProps) {
|
|
142
|
+
if (asChild) {
|
|
143
|
+
return (
|
|
144
|
+
<BaseMenu.Trigger className={className} render={children as React.ReactElement}>
|
|
145
|
+
{null}
|
|
146
|
+
</BaseMenu.Trigger>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<BaseMenu.Trigger className={className}>
|
|
152
|
+
{children}
|
|
153
|
+
</BaseMenu.Trigger>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function MenuContent({
|
|
158
|
+
children,
|
|
159
|
+
className,
|
|
160
|
+
side = 'bottom',
|
|
161
|
+
align = 'start',
|
|
162
|
+
sideOffset = 4,
|
|
163
|
+
}: MenuContentProps) {
|
|
164
|
+
const popupClasses = [styles.popup, className].filter(Boolean).join(' ');
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<BaseMenu.Portal>
|
|
168
|
+
<BaseMenu.Positioner
|
|
169
|
+
side={side}
|
|
170
|
+
align={align}
|
|
171
|
+
sideOffset={sideOffset}
|
|
172
|
+
className={styles.positioner}
|
|
173
|
+
>
|
|
174
|
+
<BaseMenu.Popup className={popupClasses}>
|
|
175
|
+
{children}
|
|
176
|
+
</BaseMenu.Popup>
|
|
177
|
+
</BaseMenu.Positioner>
|
|
178
|
+
</BaseMenu.Portal>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function MenuItem({
|
|
183
|
+
children,
|
|
184
|
+
disabled,
|
|
185
|
+
danger,
|
|
186
|
+
onSelect,
|
|
187
|
+
className,
|
|
188
|
+
icon,
|
|
189
|
+
shortcut,
|
|
190
|
+
}: MenuItemProps) {
|
|
191
|
+
const classes = [
|
|
192
|
+
styles.item,
|
|
193
|
+
danger && styles.itemDanger,
|
|
194
|
+
className,
|
|
195
|
+
].filter(Boolean).join(' ');
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<BaseMenu.Item
|
|
199
|
+
disabled={disabled}
|
|
200
|
+
onClick={onSelect}
|
|
201
|
+
className={classes}
|
|
202
|
+
>
|
|
203
|
+
{icon && <span className={styles.itemIcon}>{icon}</span>}
|
|
204
|
+
<span className={styles.itemLabel}>{children}</span>
|
|
205
|
+
{shortcut && <span className={styles.itemShortcut}>{shortcut}</span>}
|
|
206
|
+
</BaseMenu.Item>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function MenuCheckboxItem({
|
|
211
|
+
children,
|
|
212
|
+
checked,
|
|
213
|
+
defaultChecked,
|
|
214
|
+
onCheckedChange,
|
|
215
|
+
disabled,
|
|
216
|
+
className,
|
|
217
|
+
}: MenuCheckboxItemProps) {
|
|
218
|
+
const classes = [styles.item, styles.checkboxItem, className]
|
|
219
|
+
.filter(Boolean)
|
|
220
|
+
.join(' ');
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<BaseMenu.CheckboxItem
|
|
224
|
+
checked={checked}
|
|
225
|
+
defaultChecked={defaultChecked}
|
|
226
|
+
onCheckedChange={onCheckedChange}
|
|
227
|
+
disabled={disabled}
|
|
228
|
+
className={classes}
|
|
229
|
+
>
|
|
230
|
+
<span className={styles.itemIndicator}>
|
|
231
|
+
<CheckIcon />
|
|
232
|
+
</span>
|
|
233
|
+
<span className={styles.itemLabel}>{children}</span>
|
|
234
|
+
</BaseMenu.CheckboxItem>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function MenuRadioGroup({
|
|
239
|
+
children,
|
|
240
|
+
value,
|
|
241
|
+
defaultValue,
|
|
242
|
+
onValueChange,
|
|
243
|
+
}: MenuRadioGroupProps) {
|
|
244
|
+
return (
|
|
245
|
+
<BaseMenu.RadioGroup
|
|
246
|
+
value={value}
|
|
247
|
+
defaultValue={defaultValue}
|
|
248
|
+
onValueChange={onValueChange}
|
|
249
|
+
>
|
|
250
|
+
{children}
|
|
251
|
+
</BaseMenu.RadioGroup>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function MenuRadioItem({
|
|
256
|
+
children,
|
|
257
|
+
value,
|
|
258
|
+
disabled,
|
|
259
|
+
className,
|
|
260
|
+
}: MenuRadioItemProps) {
|
|
261
|
+
const classes = [styles.item, styles.radioItem, className]
|
|
262
|
+
.filter(Boolean)
|
|
263
|
+
.join(' ');
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<BaseMenu.RadioItem value={value} disabled={disabled} className={classes}>
|
|
267
|
+
<span className={styles.itemIndicator}>
|
|
268
|
+
<DotIcon />
|
|
269
|
+
</span>
|
|
270
|
+
<span className={styles.itemLabel}>{children}</span>
|
|
271
|
+
</BaseMenu.RadioItem>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function MenuGroup({ children, className }: MenuGroupProps) {
|
|
276
|
+
const classes = [styles.group, className].filter(Boolean).join(' ');
|
|
277
|
+
return <BaseMenu.Group className={classes}>{children}</BaseMenu.Group>;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function MenuGroupLabel({ children, className }: MenuGroupLabelProps) {
|
|
281
|
+
const classes = [styles.groupLabel, className].filter(Boolean).join(' ');
|
|
282
|
+
return <BaseMenu.GroupLabel className={classes}>{children}</BaseMenu.GroupLabel>;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function MenuSeparator({ className }: MenuSeparatorProps) {
|
|
286
|
+
const classes = [styles.separator, className].filter(Boolean).join(' ');
|
|
287
|
+
return <BaseMenu.Separator className={classes} />;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ============================================
|
|
291
|
+
// Export compound component
|
|
292
|
+
// ============================================
|
|
293
|
+
|
|
294
|
+
export const Menu = Object.assign(MenuRoot, {
|
|
295
|
+
Trigger: MenuTrigger,
|
|
296
|
+
Content: MenuContent,
|
|
297
|
+
Item: MenuItem,
|
|
298
|
+
CheckboxItem: MenuCheckboxItem,
|
|
299
|
+
RadioGroup: MenuRadioGroup,
|
|
300
|
+
RadioItem: MenuRadioItem,
|
|
301
|
+
Group: MenuGroup,
|
|
302
|
+
GroupLabel: MenuGroupLabel,
|
|
303
|
+
Separator: MenuSeparator,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Re-export individual components
|
|
307
|
+
export {
|
|
308
|
+
MenuRoot,
|
|
309
|
+
MenuTrigger,
|
|
310
|
+
MenuContent,
|
|
311
|
+
MenuItem,
|
|
312
|
+
MenuCheckboxItem,
|
|
313
|
+
MenuRadioGroup,
|
|
314
|
+
MenuRadioItem,
|
|
315
|
+
MenuGroup,
|
|
316
|
+
MenuGroupLabel,
|
|
317
|
+
MenuSeparator,
|
|
318
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineSegment } from '@fragments/core';
|
|
3
|
+
import { Popover } from './index.js';
|
|
4
|
+
import { Button } from '../Button/index.js';
|
|
5
|
+
import { Input } from '../Input/index.js';
|
|
6
|
+
|
|
7
|
+
export default defineSegment({
|
|
8
|
+
component: Popover,
|
|
9
|
+
|
|
10
|
+
meta: {
|
|
11
|
+
name: 'Popover',
|
|
12
|
+
description: 'Rich content overlay anchored to a trigger element. Use for forms, detailed information, or interactive content that should stay in context.',
|
|
13
|
+
category: 'overlays',
|
|
14
|
+
status: 'stable',
|
|
15
|
+
tags: ['popover', 'overlay', 'dropdown', 'floating', 'contextual'],
|
|
16
|
+
since: '0.1.0',
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
usage: {
|
|
20
|
+
when: [
|
|
21
|
+
'Inline editing forms',
|
|
22
|
+
'Rich preview content',
|
|
23
|
+
'Filter panels',
|
|
24
|
+
'Date/color pickers',
|
|
25
|
+
'Content that needs more space than a tooltip',
|
|
26
|
+
],
|
|
27
|
+
whenNot: [
|
|
28
|
+
'Simple hints (use Tooltip)',
|
|
29
|
+
'Action lists (use Menu)',
|
|
30
|
+
'Blocking user interaction (use Dialog)',
|
|
31
|
+
'System notifications (use Toast or Alert)',
|
|
32
|
+
],
|
|
33
|
+
guidelines: [
|
|
34
|
+
'Keep popover content focused and minimal',
|
|
35
|
+
'Include a clear way to close (X button or action buttons)',
|
|
36
|
+
'Position to avoid covering important content',
|
|
37
|
+
'Use arrow to visually connect popover to trigger',
|
|
38
|
+
],
|
|
39
|
+
accessibility: [
|
|
40
|
+
'Focus is moved to popover content on open',
|
|
41
|
+
'Closes on Escape key',
|
|
42
|
+
'Focus returns to trigger on close',
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
props: {
|
|
47
|
+
children: {
|
|
48
|
+
type: 'node',
|
|
49
|
+
description: 'Popover trigger and content',
|
|
50
|
+
required: true,
|
|
51
|
+
},
|
|
52
|
+
open: {
|
|
53
|
+
type: 'boolean',
|
|
54
|
+
description: 'Controlled open state',
|
|
55
|
+
},
|
|
56
|
+
defaultOpen: {
|
|
57
|
+
type: 'boolean',
|
|
58
|
+
description: 'Default open state (uncontrolled)',
|
|
59
|
+
default: 'false',
|
|
60
|
+
},
|
|
61
|
+
onOpenChange: {
|
|
62
|
+
type: 'function',
|
|
63
|
+
description: 'Called when open state changes',
|
|
64
|
+
},
|
|
65
|
+
modal: {
|
|
66
|
+
type: 'boolean',
|
|
67
|
+
description: 'Whether to block page interaction',
|
|
68
|
+
default: 'false',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
relations: [
|
|
73
|
+
{ component: 'Tooltip', relationship: 'alternative', note: 'Use Tooltip for brief, non-interactive hints' },
|
|
74
|
+
{ component: 'Menu', relationship: 'alternative', note: 'Use Menu for action lists' },
|
|
75
|
+
{ component: 'Dialog', relationship: 'alternative', note: 'Use Dialog for blocking interactions' },
|
|
76
|
+
],
|
|
77
|
+
|
|
78
|
+
contract: {
|
|
79
|
+
propsSummary: [
|
|
80
|
+
'open: boolean - controlled open state',
|
|
81
|
+
'onOpenChange: (open) => void - state handler',
|
|
82
|
+
'modal: boolean - blocks page interaction (default: false)',
|
|
83
|
+
'Popover.Content side: top|bottom|left|right - position',
|
|
84
|
+
],
|
|
85
|
+
scenarioTags: [
|
|
86
|
+
'overlay.popover',
|
|
87
|
+
'form.inline',
|
|
88
|
+
'content.preview',
|
|
89
|
+
],
|
|
90
|
+
a11yRules: ['A11Y_POPOVER_FOCUS', 'A11Y_POPOVER_ESCAPE'],
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
variants: [
|
|
94
|
+
{
|
|
95
|
+
name: 'Default',
|
|
96
|
+
description: 'Basic popover with content',
|
|
97
|
+
render: () => (
|
|
98
|
+
<Popover>
|
|
99
|
+
<Popover.Trigger asChild>
|
|
100
|
+
<Button variant="secondary">Open Popover</Button>
|
|
101
|
+
</Popover.Trigger>
|
|
102
|
+
<Popover.Content>
|
|
103
|
+
<Popover.Close />
|
|
104
|
+
<Popover.Title>Popover Title</Popover.Title>
|
|
105
|
+
<Popover.Description>
|
|
106
|
+
This is a popover with some content. It can contain text, forms, or other elements.
|
|
107
|
+
</Popover.Description>
|
|
108
|
+
</Popover.Content>
|
|
109
|
+
</Popover>
|
|
110
|
+
),
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'With Form',
|
|
114
|
+
description: 'Popover containing a form',
|
|
115
|
+
render: () => (
|
|
116
|
+
<Popover>
|
|
117
|
+
<Popover.Trigger asChild>
|
|
118
|
+
<Button variant="secondary">Edit Name</Button>
|
|
119
|
+
</Popover.Trigger>
|
|
120
|
+
<Popover.Content size="sm">
|
|
121
|
+
<Popover.Close />
|
|
122
|
+
<Popover.Title>Edit Name</Popover.Title>
|
|
123
|
+
<Popover.Body>
|
|
124
|
+
<Input label="Display Name" placeholder="Enter name" />
|
|
125
|
+
</Popover.Body>
|
|
126
|
+
<Popover.Footer>
|
|
127
|
+
<Popover.Close asChild>
|
|
128
|
+
<Button variant="secondary" size="sm">Cancel</Button>
|
|
129
|
+
</Popover.Close>
|
|
130
|
+
<Button variant="primary" size="sm">Save</Button>
|
|
131
|
+
</Popover.Footer>
|
|
132
|
+
</Popover.Content>
|
|
133
|
+
</Popover>
|
|
134
|
+
),
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'With Arrow',
|
|
138
|
+
description: 'Popover with pointing arrow',
|
|
139
|
+
render: () => (
|
|
140
|
+
<Popover>
|
|
141
|
+
<Popover.Trigger asChild>
|
|
142
|
+
<Button variant="secondary">Info</Button>
|
|
143
|
+
</Popover.Trigger>
|
|
144
|
+
<Popover.Content arrow>
|
|
145
|
+
<Popover.Title>Quick Tip</Popover.Title>
|
|
146
|
+
<Popover.Description>
|
|
147
|
+
This popover has an arrow pointing to its trigger element.
|
|
148
|
+
</Popover.Description>
|
|
149
|
+
</Popover.Content>
|
|
150
|
+
</Popover>
|
|
151
|
+
),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'Positions',
|
|
155
|
+
description: 'Popover on different sides',
|
|
156
|
+
render: () => (
|
|
157
|
+
<div style={{ display: 'flex', gap: '16px', padding: '60px' }}>
|
|
158
|
+
<Popover>
|
|
159
|
+
<Popover.Trigger asChild>
|
|
160
|
+
<Button variant="secondary">Top</Button>
|
|
161
|
+
</Popover.Trigger>
|
|
162
|
+
<Popover.Content side="top" size="sm">
|
|
163
|
+
<Popover.Description>Popover on top</Popover.Description>
|
|
164
|
+
</Popover.Content>
|
|
165
|
+
</Popover>
|
|
166
|
+
<Popover>
|
|
167
|
+
<Popover.Trigger asChild>
|
|
168
|
+
<Button variant="secondary">Bottom</Button>
|
|
169
|
+
</Popover.Trigger>
|
|
170
|
+
<Popover.Content side="bottom" size="sm">
|
|
171
|
+
<Popover.Description>Popover on bottom</Popover.Description>
|
|
172
|
+
</Popover.Content>
|
|
173
|
+
</Popover>
|
|
174
|
+
</div>
|
|
175
|
+
),
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
});
|