@proyecto-viviana/ui 0.2.5 → 0.3.2
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/dist/index.js +210 -557
- package/dist/index.js.map +7 -1
- package/dist/index.jsx +6658 -0
- package/dist/index.jsx.map +7 -0
- package/dist/index.ssr.js +42 -399
- package/dist/index.ssr.js.map +7 -1
- package/dist/radio/index.d.ts +27 -12
- package/dist/radio/index.d.ts.map +1 -1
- package/package.json +11 -12
- package/src/alert/index.tsx +0 -48
- package/src/assets/favicon.png +0 -0
- package/src/assets/fire.gif +0 -0
- package/src/autocomplete/index.tsx +0 -313
- package/src/avatar/index.tsx +0 -75
- package/src/badge/index.tsx +0 -43
- package/src/breadcrumbs/index.tsx +0 -207
- package/src/button/Button.tsx +0 -74
- package/src/button/index.ts +0 -2
- package/src/button/types.ts +0 -24
- package/src/calendar/DateField.tsx +0 -200
- package/src/calendar/DatePicker.tsx +0 -298
- package/src/calendar/RangeCalendar.tsx +0 -236
- package/src/calendar/TimeField.tsx +0 -196
- package/src/calendar/index.tsx +0 -223
- package/src/checkbox/index.tsx +0 -257
- package/src/color/index.tsx +0 -687
- package/src/combobox/index.tsx +0 -383
- package/src/components.css +0 -1077
- package/src/custom/calendar-card/index.tsx +0 -66
- package/src/custom/chip/index.tsx +0 -46
- package/src/custom/conversation/index.tsx +0 -105
- package/src/custom/event-card/index.tsx +0 -132
- package/src/custom/header/index.tsx +0 -33
- package/src/custom/lateral-nav/index.tsx +0 -88
- package/src/custom/logo/index.tsx +0 -58
- package/src/custom/nav-header/index.tsx +0 -42
- package/src/custom/page-layout/index.tsx +0 -29
- package/src/custom/profile-card/index.tsx +0 -64
- package/src/custom/project-card/index.tsx +0 -59
- package/src/custom/timeline-item/index.tsx +0 -105
- package/src/dialog/Dialog.tsx +0 -260
- package/src/dialog/index.tsx +0 -3
- package/src/disclosure/index.tsx +0 -307
- package/src/gridlist/index.tsx +0 -403
- package/src/icon/icons/GitHubIcon.tsx +0 -20
- package/src/icon/index.tsx +0 -48
- package/src/index.ts +0 -322
- package/src/landmark/index.tsx +0 -231
- package/src/link/index.tsx +0 -130
- package/src/listbox/index.tsx +0 -231
- package/src/menu/index.tsx +0 -297
- package/src/meter/index.tsx +0 -163
- package/src/numberfield/index.tsx +0 -482
- package/src/popover/index.tsx +0 -260
- package/src/progress-bar/index.tsx +0 -169
- package/src/radio/index.tsx +0 -173
- package/src/searchfield/index.tsx +0 -453
- package/src/select/index.tsx +0 -349
- package/src/separator/index.tsx +0 -141
- package/src/slider/index.tsx +0 -382
- package/src/styles.css +0 -450
- package/src/switch/ToggleSwitch.tsx +0 -112
- package/src/switch/index.tsx +0 -90
- package/src/table/index.tsx +0 -531
- package/src/tabs/index.tsx +0 -273
- package/src/tag-group/index.tsx +0 -240
- package/src/test-utils/index.ts +0 -32
- package/src/textfield/index.tsx +0 -211
- package/src/theme.css +0 -101
- package/src/toast/index.tsx +0 -324
- package/src/toolbar/index.tsx +0 -108
- package/src/tooltip/index.tsx +0 -197
- package/src/tree/index.tsx +0 -494
package/src/link/index.tsx
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Link component for proyecto-viviana-ui
|
|
3
|
-
*
|
|
4
|
-
* Styled link component built on top of solidaria-components.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { type JSX, splitProps } from 'solid-js';
|
|
8
|
-
import {
|
|
9
|
-
Link as HeadlessLink,
|
|
10
|
-
type LinkProps as HeadlessLinkProps,
|
|
11
|
-
type LinkRenderProps,
|
|
12
|
-
} from '@proyecto-viviana/solidaria-components';
|
|
13
|
-
|
|
14
|
-
// ============================================
|
|
15
|
-
// TYPES
|
|
16
|
-
// ============================================
|
|
17
|
-
|
|
18
|
-
export type LinkVariant = 'primary' | 'secondary' | 'subtle';
|
|
19
|
-
|
|
20
|
-
export interface LinkProps extends Omit<HeadlessLinkProps, 'class' | 'style' | 'children'> {
|
|
21
|
-
/** The visual style of the link. @default 'primary' */
|
|
22
|
-
variant?: LinkVariant;
|
|
23
|
-
/** Whether the link is on its own vs inside a longer string of text. */
|
|
24
|
-
isStandalone?: boolean;
|
|
25
|
-
/** Whether the link should be displayed with a quiet style (no underline by default). */
|
|
26
|
-
isQuiet?: boolean;
|
|
27
|
-
/** Additional CSS class name. */
|
|
28
|
-
class?: string;
|
|
29
|
-
/** The content of the link. */
|
|
30
|
-
children?: JSX.Element;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ============================================
|
|
34
|
-
// STYLES
|
|
35
|
-
// ============================================
|
|
36
|
-
|
|
37
|
-
const variantStyles = {
|
|
38
|
-
primary: 'text-accent hover:text-accent-300',
|
|
39
|
-
secondary: 'text-primary-300 hover:text-primary-200',
|
|
40
|
-
subtle: 'text-primary-400 hover:text-primary-300',
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// ============================================
|
|
44
|
-
// LINK COMPONENT
|
|
45
|
-
// ============================================
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Links allow users to navigate to a different location.
|
|
49
|
-
* They can be presented inline inside a paragraph or as standalone text.
|
|
50
|
-
*
|
|
51
|
-
* Built on solidaria-components Link for full accessibility support.
|
|
52
|
-
*
|
|
53
|
-
* @example
|
|
54
|
-
* ```tsx
|
|
55
|
-
* <Link href="/about">About Us</Link>
|
|
56
|
-
*
|
|
57
|
-
* // Secondary variant
|
|
58
|
-
* <Link href="/help" variant="secondary">Help</Link>
|
|
59
|
-
*
|
|
60
|
-
* // Standalone (bold, no underline until hover)
|
|
61
|
-
* <Link href="/home" isStandalone isQuiet>Home</Link>
|
|
62
|
-
* ```
|
|
63
|
-
*/
|
|
64
|
-
export function Link(props: LinkProps): JSX.Element {
|
|
65
|
-
const [local, headlessProps] = splitProps(props, [
|
|
66
|
-
'variant',
|
|
67
|
-
'isStandalone',
|
|
68
|
-
'isQuiet',
|
|
69
|
-
'class',
|
|
70
|
-
]);
|
|
71
|
-
|
|
72
|
-
const variant = local.variant ?? 'primary';
|
|
73
|
-
const customClass = local.class ?? '';
|
|
74
|
-
|
|
75
|
-
// Generate class based on render props
|
|
76
|
-
const getClassName = (renderProps: LinkRenderProps): string => {
|
|
77
|
-
const base = 'transition-colors duration-200 cursor-pointer rounded-sm outline-none';
|
|
78
|
-
|
|
79
|
-
// Variant colors
|
|
80
|
-
const variantClass = variantStyles[variant];
|
|
81
|
-
|
|
82
|
-
// Underline behavior
|
|
83
|
-
let underlineClass: string;
|
|
84
|
-
if (local.isStandalone && local.isQuiet) {
|
|
85
|
-
// Quiet standalone: no underline by default, underline on hover/focus
|
|
86
|
-
underlineClass = renderProps.isHovered || renderProps.isFocusVisible
|
|
87
|
-
? 'underline'
|
|
88
|
-
: 'no-underline';
|
|
89
|
-
} else {
|
|
90
|
-
// Inline links always have underline for accessibility
|
|
91
|
-
underlineClass = 'underline';
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Font weight for standalone
|
|
95
|
-
const weightClass = local.isStandalone ? 'font-medium' : '';
|
|
96
|
-
|
|
97
|
-
// Focus ring
|
|
98
|
-
const focusClass = renderProps.isFocusVisible
|
|
99
|
-
? 'ring-2 ring-accent-300 ring-offset-2 ring-offset-bg-400'
|
|
100
|
-
: '';
|
|
101
|
-
|
|
102
|
-
// Disabled state
|
|
103
|
-
const disabledClass = renderProps.isDisabled
|
|
104
|
-
? 'opacity-50 cursor-not-allowed'
|
|
105
|
-
: '';
|
|
106
|
-
|
|
107
|
-
// Pressed state
|
|
108
|
-
const pressedClass = renderProps.isPressed ? 'opacity-80' : '';
|
|
109
|
-
|
|
110
|
-
return [
|
|
111
|
-
base,
|
|
112
|
-
variantClass,
|
|
113
|
-
underlineClass,
|
|
114
|
-
weightClass,
|
|
115
|
-
focusClass,
|
|
116
|
-
disabledClass,
|
|
117
|
-
pressedClass,
|
|
118
|
-
customClass,
|
|
119
|
-
].filter(Boolean).join(' ');
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
return (
|
|
123
|
-
<HeadlessLink
|
|
124
|
-
{...headlessProps}
|
|
125
|
-
class={getClassName}
|
|
126
|
-
>
|
|
127
|
-
{props.children}
|
|
128
|
-
</HeadlessLink>
|
|
129
|
-
);
|
|
130
|
-
}
|
package/src/listbox/index.tsx
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ListBox component for proyecto-viviana-ui
|
|
3
|
-
*
|
|
4
|
-
* Styled listbox component built on top of solidaria-components.
|
|
5
|
-
* Inspired by Spectrum 2's ListBox component patterns.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { type JSX, splitProps, createContext, useContext, Show } from 'solid-js'
|
|
9
|
-
import {
|
|
10
|
-
ListBox as HeadlessListBox,
|
|
11
|
-
ListBoxOption as HeadlessListBoxOption,
|
|
12
|
-
type ListBoxProps as HeadlessListBoxProps,
|
|
13
|
-
type ListBoxOptionProps as HeadlessListBoxOptionProps,
|
|
14
|
-
type ListBoxRenderProps,
|
|
15
|
-
type ListBoxOptionRenderProps,
|
|
16
|
-
} from '@proyecto-viviana/solidaria-components'
|
|
17
|
-
import type { Key } from '@proyecto-viviana/solid-stately'
|
|
18
|
-
|
|
19
|
-
// ============================================
|
|
20
|
-
// SIZE CONTEXT
|
|
21
|
-
// ============================================
|
|
22
|
-
|
|
23
|
-
export type ListBoxSize = 'sm' | 'md' | 'lg'
|
|
24
|
-
|
|
25
|
-
const ListBoxSizeContext = createContext<ListBoxSize>('md')
|
|
26
|
-
|
|
27
|
-
// ============================================
|
|
28
|
-
// TYPES
|
|
29
|
-
// ============================================
|
|
30
|
-
|
|
31
|
-
export interface ListBoxProps<T> extends Omit<HeadlessListBoxProps<T>, 'class' | 'style'> {
|
|
32
|
-
/** The size of the listbox. */
|
|
33
|
-
size?: ListBoxSize
|
|
34
|
-
/** Additional CSS class name. */
|
|
35
|
-
class?: string
|
|
36
|
-
/** Label for the listbox. */
|
|
37
|
-
label?: string
|
|
38
|
-
/** Description for the listbox. */
|
|
39
|
-
description?: string
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface ListBoxOptionProps<T> extends Omit<HeadlessListBoxOptionProps<T>, 'class' | 'style'> {
|
|
43
|
-
/** Additional CSS class name. */
|
|
44
|
-
class?: string
|
|
45
|
-
/** Optional description text. */
|
|
46
|
-
description?: string
|
|
47
|
-
/**
|
|
48
|
-
* Optional icon to display before the label.
|
|
49
|
-
* Use a function returning JSX for SSR compatibility: `icon={() => <MyIcon />}`
|
|
50
|
-
*/
|
|
51
|
-
icon?: () => JSX.Element
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ============================================
|
|
55
|
-
// STYLES
|
|
56
|
-
// ============================================
|
|
57
|
-
|
|
58
|
-
const sizeStyles = {
|
|
59
|
-
sm: {
|
|
60
|
-
list: 'py-1',
|
|
61
|
-
option: 'text-sm py-1.5 px-3 gap-2',
|
|
62
|
-
icon: 'h-4 w-4',
|
|
63
|
-
label: 'text-sm',
|
|
64
|
-
description: 'text-xs',
|
|
65
|
-
},
|
|
66
|
-
md: {
|
|
67
|
-
list: 'py-1.5',
|
|
68
|
-
option: 'text-base py-2 px-4 gap-3',
|
|
69
|
-
icon: 'h-5 w-5',
|
|
70
|
-
label: 'text-base',
|
|
71
|
-
description: 'text-sm',
|
|
72
|
-
},
|
|
73
|
-
lg: {
|
|
74
|
-
list: 'py-2',
|
|
75
|
-
option: 'text-lg py-2.5 px-5 gap-3',
|
|
76
|
-
icon: 'h-6 w-6',
|
|
77
|
-
label: 'text-lg',
|
|
78
|
-
description: 'text-base',
|
|
79
|
-
},
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// ============================================
|
|
83
|
-
// LISTBOX COMPONENT
|
|
84
|
-
// ============================================
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* A listbox displays a list of options and allows a user to select one or more of them.
|
|
88
|
-
*
|
|
89
|
-
* Built on solidaria-components ListBox for full accessibility support.
|
|
90
|
-
*/
|
|
91
|
-
export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
|
|
92
|
-
const [local, headlessProps] = splitProps(props, [
|
|
93
|
-
'size',
|
|
94
|
-
'class',
|
|
95
|
-
'label',
|
|
96
|
-
'description',
|
|
97
|
-
'renderEmptyState',
|
|
98
|
-
])
|
|
99
|
-
|
|
100
|
-
const size = local.size ?? 'md'
|
|
101
|
-
const styles = sizeStyles[size]
|
|
102
|
-
const customClass = local.class ?? ''
|
|
103
|
-
|
|
104
|
-
const getClassName = (renderProps: ListBoxRenderProps): string => {
|
|
105
|
-
const base = 'rounded-lg border-2 border-primary-600 bg-bg-400 overflow-auto focus:outline-none'
|
|
106
|
-
const sizeClass = styles.list
|
|
107
|
-
|
|
108
|
-
let stateClass: string
|
|
109
|
-
if (renderProps.isDisabled) {
|
|
110
|
-
stateClass = 'opacity-50'
|
|
111
|
-
} else {
|
|
112
|
-
stateClass = ''
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const focusClass = renderProps.isFocusVisible
|
|
116
|
-
? 'ring-2 ring-accent-300 ring-offset-2 ring-offset-bg-400'
|
|
117
|
-
: ''
|
|
118
|
-
|
|
119
|
-
return [base, sizeClass, stateClass, focusClass, customClass].filter(Boolean).join(' ')
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const defaultEmptyState = () => (
|
|
123
|
-
<li class="py-4 px-4 text-center text-primary-500">
|
|
124
|
-
No items
|
|
125
|
-
</li>
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
return (
|
|
129
|
-
<ListBoxSizeContext.Provider value={size}>
|
|
130
|
-
<div class="flex flex-col gap-1.5">
|
|
131
|
-
<Show when={local.label}>
|
|
132
|
-
<label class={`text-primary-200 font-medium ${styles.label}`}>
|
|
133
|
-
{local.label}
|
|
134
|
-
</label>
|
|
135
|
-
</Show>
|
|
136
|
-
<HeadlessListBox
|
|
137
|
-
{...headlessProps}
|
|
138
|
-
class={getClassName}
|
|
139
|
-
renderEmptyState={local.renderEmptyState ?? defaultEmptyState}
|
|
140
|
-
children={props.children}
|
|
141
|
-
/>
|
|
142
|
-
<Show when={local.description}>
|
|
143
|
-
<span class="text-primary-400 text-sm">{local.description}</span>
|
|
144
|
-
</Show>
|
|
145
|
-
</div>
|
|
146
|
-
</ListBoxSizeContext.Provider>
|
|
147
|
-
)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ============================================
|
|
151
|
-
// LISTBOX OPTION COMPONENT
|
|
152
|
-
// ============================================
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* An option in a listbox.
|
|
156
|
-
* SSR-compatible - renders icon, check, content, and description directly without render props.
|
|
157
|
-
*/
|
|
158
|
-
export function ListBoxOption<T>(props: ListBoxOptionProps<T>): JSX.Element {
|
|
159
|
-
const [local, headlessProps] = splitProps(props, ['class', 'description', 'icon'])
|
|
160
|
-
const size = useContext(ListBoxSizeContext)
|
|
161
|
-
const sizeStyle = sizeStyles[size]
|
|
162
|
-
const customClass = local.class ?? ''
|
|
163
|
-
|
|
164
|
-
const getClassName = (renderProps: ListBoxOptionRenderProps): string => {
|
|
165
|
-
const base = 'flex items-center cursor-pointer transition-colors duration-150 outline-none'
|
|
166
|
-
const sizeClass = sizeStyle.option
|
|
167
|
-
|
|
168
|
-
let colorClass: string
|
|
169
|
-
if (renderProps.isDisabled) {
|
|
170
|
-
colorClass = 'text-primary-500 cursor-not-allowed'
|
|
171
|
-
} else if (renderProps.isSelected) {
|
|
172
|
-
if (renderProps.isFocused || renderProps.isHovered) {
|
|
173
|
-
colorClass = 'bg-accent/30 text-accent'
|
|
174
|
-
} else {
|
|
175
|
-
colorClass = 'bg-accent/20 text-accent'
|
|
176
|
-
}
|
|
177
|
-
} else if (renderProps.isFocused || renderProps.isHovered) {
|
|
178
|
-
colorClass = 'bg-bg-300 text-primary-100'
|
|
179
|
-
} else {
|
|
180
|
-
colorClass = 'text-primary-200'
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const focusClass = renderProps.isFocusVisible
|
|
184
|
-
? 'ring-2 ring-inset ring-accent-300'
|
|
185
|
-
: ''
|
|
186
|
-
|
|
187
|
-
return [base, sizeClass, colorClass, focusClass, customClass].filter(Boolean).join(' ')
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return (
|
|
191
|
-
<HeadlessListBoxOption
|
|
192
|
-
{...headlessProps}
|
|
193
|
-
class={getClassName}
|
|
194
|
-
>
|
|
195
|
-
{local.icon && <span class={`shrink-0 ${sizeStyle.icon}`}>{local.icon()}</span>}
|
|
196
|
-
<CheckIcon class={`shrink-0 ${sizeStyle.icon} text-accent hidden data-selected:block`} />
|
|
197
|
-
<div class="flex flex-col flex-1 min-w-0">
|
|
198
|
-
<span class="truncate">{props.children as JSX.Element}</span>
|
|
199
|
-
{local.description && (
|
|
200
|
-
<span class={`text-primary-400 truncate ${sizeStyle.description}`}>
|
|
201
|
-
{local.description}
|
|
202
|
-
</span>
|
|
203
|
-
)}
|
|
204
|
-
</div>
|
|
205
|
-
</HeadlessListBoxOption>
|
|
206
|
-
)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// ============================================
|
|
210
|
-
// ICONS
|
|
211
|
-
// ============================================
|
|
212
|
-
|
|
213
|
-
function CheckIcon(props: { class?: string }): JSX.Element {
|
|
214
|
-
return (
|
|
215
|
-
<svg
|
|
216
|
-
class={props.class}
|
|
217
|
-
fill="none"
|
|
218
|
-
viewBox="0 0 24 24"
|
|
219
|
-
stroke="currentColor"
|
|
220
|
-
stroke-width="2"
|
|
221
|
-
>
|
|
222
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
|
223
|
-
</svg>
|
|
224
|
-
)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Attach sub-components for convenience
|
|
228
|
-
ListBox.Option = ListBoxOption
|
|
229
|
-
|
|
230
|
-
// Re-export Key type for convenience
|
|
231
|
-
export type { Key }
|
package/src/menu/index.tsx
DELETED
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Menu component for proyecto-viviana-ui
|
|
3
|
-
*
|
|
4
|
-
* Styled menu component built on top of solidaria-components.
|
|
5
|
-
* Inspired by Spectrum 2's Menu component patterns.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { type JSX, splitProps, createContext, useContext } from 'solid-js'
|
|
9
|
-
import {
|
|
10
|
-
Menu as HeadlessMenu,
|
|
11
|
-
MenuItem as HeadlessMenuItem,
|
|
12
|
-
MenuTrigger as HeadlessMenuTrigger,
|
|
13
|
-
MenuButton as HeadlessMenuButton,
|
|
14
|
-
type MenuProps as HeadlessMenuProps,
|
|
15
|
-
type MenuItemProps as HeadlessMenuItemProps,
|
|
16
|
-
type MenuTriggerProps as HeadlessMenuTriggerProps,
|
|
17
|
-
type MenuButtonProps as HeadlessMenuButtonProps,
|
|
18
|
-
type MenuRenderProps,
|
|
19
|
-
type MenuItemRenderProps,
|
|
20
|
-
type MenuTriggerRenderProps,
|
|
21
|
-
} from '@proyecto-viviana/solidaria-components'
|
|
22
|
-
import type { Key } from '@proyecto-viviana/solid-stately'
|
|
23
|
-
|
|
24
|
-
// ============================================
|
|
25
|
-
// SIZE CONTEXT
|
|
26
|
-
// ============================================
|
|
27
|
-
|
|
28
|
-
export type MenuSize = 'sm' | 'md' | 'lg'
|
|
29
|
-
|
|
30
|
-
const MenuSizeContext = createContext<MenuSize>('md')
|
|
31
|
-
|
|
32
|
-
// ============================================
|
|
33
|
-
// TYPES
|
|
34
|
-
// ============================================
|
|
35
|
-
|
|
36
|
-
export interface MenuTriggerProps extends Omit<HeadlessMenuTriggerProps, 'class' | 'style'> {
|
|
37
|
-
/** The size of the menu. */
|
|
38
|
-
size?: MenuSize
|
|
39
|
-
/** Additional CSS class name. */
|
|
40
|
-
class?: string
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface MenuButtonProps extends Omit<HeadlessMenuButtonProps, 'class' | 'style'> {
|
|
44
|
-
/** Additional CSS class name. */
|
|
45
|
-
class?: string
|
|
46
|
-
/** Visual variant of the button. */
|
|
47
|
-
variant?: 'primary' | 'secondary' | 'quiet'
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface MenuProps<T> extends Omit<HeadlessMenuProps<T>, 'class' | 'style'> {
|
|
51
|
-
/** Additional CSS class name. */
|
|
52
|
-
class?: string
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface MenuItemProps<T> extends Omit<HeadlessMenuItemProps<T>, 'class' | 'style'> {
|
|
56
|
-
/** Additional CSS class name. */
|
|
57
|
-
class?: string
|
|
58
|
-
/**
|
|
59
|
-
* Optional icon to display before the label.
|
|
60
|
-
* Use a function returning JSX for SSR compatibility: `icon={() => <MyIcon />}`
|
|
61
|
-
*/
|
|
62
|
-
icon?: () => JSX.Element
|
|
63
|
-
/** Optional keyboard shortcut to display. */
|
|
64
|
-
shortcut?: string
|
|
65
|
-
/** Whether this is a destructive action. */
|
|
66
|
-
isDestructive?: boolean
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ============================================
|
|
70
|
-
// STYLES
|
|
71
|
-
// ============================================
|
|
72
|
-
|
|
73
|
-
const sizeStyles = {
|
|
74
|
-
sm: {
|
|
75
|
-
button: 'h-8 text-sm px-3 gap-2',
|
|
76
|
-
menu: 'py-1',
|
|
77
|
-
item: 'text-sm py-1.5 px-3 gap-2',
|
|
78
|
-
icon: 'h-4 w-4',
|
|
79
|
-
},
|
|
80
|
-
md: {
|
|
81
|
-
button: 'h-10 text-base px-4 gap-2',
|
|
82
|
-
menu: 'py-1.5',
|
|
83
|
-
item: 'text-base py-2 px-4 gap-3',
|
|
84
|
-
icon: 'h-5 w-5',
|
|
85
|
-
},
|
|
86
|
-
lg: {
|
|
87
|
-
button: 'h-12 text-lg px-5 gap-3',
|
|
88
|
-
menu: 'py-2',
|
|
89
|
-
item: 'text-lg py-2.5 px-5 gap-3',
|
|
90
|
-
icon: 'h-6 w-6',
|
|
91
|
-
},
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const buttonVariants = {
|
|
95
|
-
primary: 'bg-accent text-bg-500 border-accent hover:bg-accent-300 hover:border-accent-300',
|
|
96
|
-
secondary: 'bg-bg-400 text-primary-200 border-primary-600 hover:bg-bg-300 hover:border-accent-300',
|
|
97
|
-
quiet: 'bg-transparent text-primary-200 border-transparent hover:bg-bg-300',
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ============================================
|
|
101
|
-
// MENU TRIGGER COMPONENT
|
|
102
|
-
// ============================================
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* A menu trigger wraps a button and menu, handling the open/close state.
|
|
106
|
-
*/
|
|
107
|
-
export function MenuTrigger(props: MenuTriggerProps): JSX.Element {
|
|
108
|
-
const [local, headlessProps] = splitProps(props, ['size', 'class'])
|
|
109
|
-
const size = local.size ?? 'md'
|
|
110
|
-
|
|
111
|
-
return (
|
|
112
|
-
<MenuSizeContext.Provider value={size}>
|
|
113
|
-
<div class={`relative inline-block ${local.class ?? ''}`}>
|
|
114
|
-
<HeadlessMenuTrigger {...headlessProps}>
|
|
115
|
-
{props.children}
|
|
116
|
-
</HeadlessMenuTrigger>
|
|
117
|
-
</div>
|
|
118
|
-
</MenuSizeContext.Provider>
|
|
119
|
-
)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ============================================
|
|
123
|
-
// MENU BUTTON COMPONENT
|
|
124
|
-
// ============================================
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* A button that opens a menu.
|
|
128
|
-
* SSR-compatible - renders children and chevron icon directly without render props.
|
|
129
|
-
*/
|
|
130
|
-
export function MenuButton(props: MenuButtonProps): JSX.Element {
|
|
131
|
-
const [local, headlessProps] = splitProps(props, ['class', 'variant'])
|
|
132
|
-
const size = useContext(MenuSizeContext)
|
|
133
|
-
const sizeStyle = sizeStyles[size]
|
|
134
|
-
const variant = local.variant ?? 'secondary'
|
|
135
|
-
const customClass = local.class ?? ''
|
|
136
|
-
|
|
137
|
-
const getClassName = (renderProps: MenuTriggerRenderProps): string => {
|
|
138
|
-
const base = 'inline-flex items-center justify-center rounded-lg border-2 font-medium transition-all duration-200'
|
|
139
|
-
const sizeClass = sizeStyle.button
|
|
140
|
-
const variantClass = buttonVariants[variant]
|
|
141
|
-
|
|
142
|
-
let stateClass: string
|
|
143
|
-
if (renderProps.isDisabled) {
|
|
144
|
-
stateClass = 'opacity-50 cursor-not-allowed'
|
|
145
|
-
} else if (renderProps.isPressed) {
|
|
146
|
-
stateClass = 'scale-95'
|
|
147
|
-
} else {
|
|
148
|
-
stateClass = 'cursor-pointer'
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const focusClass = renderProps.isFocusVisible
|
|
152
|
-
? 'ring-2 ring-accent-300 ring-offset-2 ring-offset-bg-400'
|
|
153
|
-
: ''
|
|
154
|
-
|
|
155
|
-
return [base, sizeClass, variantClass, stateClass, focusClass, customClass].filter(Boolean).join(' ')
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return (
|
|
159
|
-
<HeadlessMenuButton
|
|
160
|
-
{...headlessProps}
|
|
161
|
-
class={getClassName}
|
|
162
|
-
>
|
|
163
|
-
{props.children as JSX.Element}
|
|
164
|
-
{/* Chevron rotates via CSS based on data-open attribute */}
|
|
165
|
-
<ChevronIcon class={`${sizeStyle.icon} transition-transform duration-200 data-open:rotate-180`} />
|
|
166
|
-
</HeadlessMenuButton>
|
|
167
|
-
)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ============================================
|
|
171
|
-
// MENU COMPONENT
|
|
172
|
-
// ============================================
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* A menu displays a list of actions or options for the user to choose from.
|
|
176
|
-
*/
|
|
177
|
-
export function Menu<T>(props: MenuProps<T>): JSX.Element {
|
|
178
|
-
const [local, headlessProps] = splitProps(props, ['class'])
|
|
179
|
-
const size = useContext(MenuSizeContext)
|
|
180
|
-
const styles = () => sizeStyles[size]
|
|
181
|
-
const customClass = local.class ?? ''
|
|
182
|
-
|
|
183
|
-
const getClassName = (_renderProps: MenuRenderProps): string => {
|
|
184
|
-
const base = 'absolute z-50 mt-1 min-w-[12rem] rounded-lg border-2 border-primary-600 bg-bg-400 shadow-lg overflow-hidden'
|
|
185
|
-
const sizeClass = styles().menu
|
|
186
|
-
return [base, sizeClass, customClass].filter(Boolean).join(' ')
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return (
|
|
190
|
-
<HeadlessMenu
|
|
191
|
-
{...headlessProps}
|
|
192
|
-
class={getClassName}
|
|
193
|
-
children={props.children}
|
|
194
|
-
/>
|
|
195
|
-
)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// ============================================
|
|
199
|
-
// MENU ITEM COMPONENT
|
|
200
|
-
// ============================================
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* An item in a menu.
|
|
204
|
-
* SSR-compatible - renders icon, content, and shortcut directly without render props.
|
|
205
|
-
*/
|
|
206
|
-
export function MenuItem<T>(props: MenuItemProps<T>): JSX.Element {
|
|
207
|
-
const [local, headlessProps] = splitProps(props, ['class', 'icon', 'shortcut', 'isDestructive'])
|
|
208
|
-
const size = useContext(MenuSizeContext)
|
|
209
|
-
const sizeStyle = sizeStyles[size]
|
|
210
|
-
const customClass = local.class ?? ''
|
|
211
|
-
|
|
212
|
-
const getClassName = (renderProps: MenuItemRenderProps): string => {
|
|
213
|
-
const base = 'flex items-center cursor-pointer transition-colors duration-150 outline-none'
|
|
214
|
-
const sizeClass = sizeStyle.item
|
|
215
|
-
|
|
216
|
-
let colorClass: string
|
|
217
|
-
if (renderProps.isDisabled) {
|
|
218
|
-
colorClass = 'text-primary-500 cursor-not-allowed'
|
|
219
|
-
} else if (local.isDestructive) {
|
|
220
|
-
if (renderProps.isFocused || renderProps.isHovered) {
|
|
221
|
-
colorClass = 'bg-danger-400/20 text-danger-400'
|
|
222
|
-
} else {
|
|
223
|
-
colorClass = 'text-danger-400'
|
|
224
|
-
}
|
|
225
|
-
} else if (renderProps.isFocused || renderProps.isHovered) {
|
|
226
|
-
colorClass = 'bg-bg-300 text-primary-100'
|
|
227
|
-
} else {
|
|
228
|
-
colorClass = 'text-primary-200'
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const pressedClass = renderProps.isPressed ? 'bg-bg-200' : ''
|
|
232
|
-
|
|
233
|
-
const focusClass = renderProps.isFocusVisible
|
|
234
|
-
? 'ring-2 ring-inset ring-accent-300'
|
|
235
|
-
: ''
|
|
236
|
-
|
|
237
|
-
return [base, sizeClass, colorClass, pressedClass, focusClass, customClass].filter(Boolean).join(' ')
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return (
|
|
241
|
-
<HeadlessMenuItem
|
|
242
|
-
{...headlessProps}
|
|
243
|
-
class={getClassName}
|
|
244
|
-
>
|
|
245
|
-
{local.icon && <span class={`shrink-0 ${sizeStyle.icon}`}>{local.icon()}</span>}
|
|
246
|
-
<span class="flex-1">{props.children as JSX.Element}</span>
|
|
247
|
-
{local.shortcut && <span class="text-primary-500 text-sm ml-auto">{local.shortcut}</span>}
|
|
248
|
-
</HeadlessMenuItem>
|
|
249
|
-
)
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// ============================================
|
|
253
|
-
// MENU SEPARATOR COMPONENT
|
|
254
|
-
// ============================================
|
|
255
|
-
|
|
256
|
-
export interface MenuSeparatorProps {
|
|
257
|
-
/** Additional CSS class name. */
|
|
258
|
-
class?: string
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* A visual separator between menu items.
|
|
263
|
-
*/
|
|
264
|
-
export function MenuSeparator(props: MenuSeparatorProps): JSX.Element {
|
|
265
|
-
return (
|
|
266
|
-
<li
|
|
267
|
-
role="separator"
|
|
268
|
-
class={`my-1 border-t border-primary-600 ${props.class ?? ''}`}
|
|
269
|
-
/>
|
|
270
|
-
)
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// ============================================
|
|
274
|
-
// ICONS
|
|
275
|
-
// ============================================
|
|
276
|
-
|
|
277
|
-
function ChevronIcon(props: { class?: string }): JSX.Element {
|
|
278
|
-
return (
|
|
279
|
-
<svg
|
|
280
|
-
class={props.class}
|
|
281
|
-
fill="none"
|
|
282
|
-
viewBox="0 0 24 24"
|
|
283
|
-
stroke="currentColor"
|
|
284
|
-
stroke-width="2"
|
|
285
|
-
>
|
|
286
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
|
287
|
-
</svg>
|
|
288
|
-
)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Attach sub-components for convenience
|
|
292
|
-
Menu.Item = MenuItem
|
|
293
|
-
Menu.Separator = MenuSeparator
|
|
294
|
-
MenuTrigger.Button = MenuButton
|
|
295
|
-
|
|
296
|
-
// Re-export Key type for convenience
|
|
297
|
-
export type { Key }
|