@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,173 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { defineSegment } from '@fragments/core';
|
|
3
|
+
import { Select } from './index.js';
|
|
4
|
+
|
|
5
|
+
// Stateful wrapper for interactive demos
|
|
6
|
+
function StatefulSelect(props: React.ComponentProps<typeof Select> & {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
initialValue?: string;
|
|
9
|
+
}) {
|
|
10
|
+
const { initialValue, children, ...rest } = props;
|
|
11
|
+
const [value, setValue] = useState<string | null>(initialValue ?? null);
|
|
12
|
+
return (
|
|
13
|
+
<Select {...rest} value={value} onValueChange={setValue}>
|
|
14
|
+
{children}
|
|
15
|
+
</Select>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default defineSegment({
|
|
20
|
+
component: Select,
|
|
21
|
+
|
|
22
|
+
meta: {
|
|
23
|
+
name: 'Select',
|
|
24
|
+
description: 'Dropdown for choosing from a list of options. Use when there are more than 4-5 choices that would clutter the UI.',
|
|
25
|
+
category: 'forms',
|
|
26
|
+
status: 'stable',
|
|
27
|
+
tags: ['select', 'dropdown', 'form', 'options', 'picker'],
|
|
28
|
+
since: '0.1.0',
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
usage: {
|
|
32
|
+
when: [
|
|
33
|
+
'Choosing from a predefined list of options',
|
|
34
|
+
'More than 4-5 options that would clutter UI as radio buttons',
|
|
35
|
+
'Space-constrained forms',
|
|
36
|
+
'When users need to see all options at once',
|
|
37
|
+
],
|
|
38
|
+
whenNot: [
|
|
39
|
+
'Very few options (2-3) - use radio buttons',
|
|
40
|
+
'Users might type custom values - use Combobox',
|
|
41
|
+
'Multiple selections needed - use Checkbox group or MultiSelect',
|
|
42
|
+
'Actions, not selection - use Menu',
|
|
43
|
+
],
|
|
44
|
+
guidelines: [
|
|
45
|
+
'Include a placeholder that explains what to select',
|
|
46
|
+
'Group related options with SelectGroup',
|
|
47
|
+
'Keep option text concise',
|
|
48
|
+
'Order options logically (alphabetical, by frequency, or by category)',
|
|
49
|
+
],
|
|
50
|
+
accessibility: [
|
|
51
|
+
'Full keyboard navigation support',
|
|
52
|
+
'Type-ahead search within options',
|
|
53
|
+
'Proper ARIA roles and attributes',
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
props: {
|
|
58
|
+
children: {
|
|
59
|
+
type: 'node',
|
|
60
|
+
description: 'Select trigger and content',
|
|
61
|
+
required: true,
|
|
62
|
+
},
|
|
63
|
+
value: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: 'Controlled selected value',
|
|
66
|
+
},
|
|
67
|
+
defaultValue: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: 'Default selected value (uncontrolled)',
|
|
70
|
+
},
|
|
71
|
+
onValueChange: {
|
|
72
|
+
type: 'function',
|
|
73
|
+
description: 'Called when selection changes',
|
|
74
|
+
},
|
|
75
|
+
placeholder: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
description: 'Placeholder text when no value selected',
|
|
78
|
+
},
|
|
79
|
+
disabled: {
|
|
80
|
+
type: 'boolean',
|
|
81
|
+
description: 'Disable the select',
|
|
82
|
+
default: 'false',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
relations: [
|
|
87
|
+
{ component: 'Menu', relationship: 'alternative', note: 'Use Menu for action-based dropdowns' },
|
|
88
|
+
{ component: 'Input', relationship: 'sibling', note: 'Use Input for free-form text entry' },
|
|
89
|
+
{ component: 'Checkbox', relationship: 'alternative', note: 'Use Checkbox group for multiple selections' },
|
|
90
|
+
],
|
|
91
|
+
|
|
92
|
+
contract: {
|
|
93
|
+
propsSummary: [
|
|
94
|
+
'value: string - controlled selected value',
|
|
95
|
+
'onValueChange: (value) => void - selection handler',
|
|
96
|
+
'placeholder: string - placeholder text',
|
|
97
|
+
'disabled: boolean - disable select',
|
|
98
|
+
],
|
|
99
|
+
scenarioTags: [
|
|
100
|
+
'form.select',
|
|
101
|
+
'form.dropdown',
|
|
102
|
+
'input.options',
|
|
103
|
+
],
|
|
104
|
+
a11yRules: ['A11Y_SELECT_KEYBOARD', 'A11Y_SELECT_LABEL'],
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
variants: [
|
|
108
|
+
{
|
|
109
|
+
name: 'Default',
|
|
110
|
+
description: 'Basic select dropdown',
|
|
111
|
+
render: () => (
|
|
112
|
+
<StatefulSelect placeholder="Select a fruit">
|
|
113
|
+
<Select.Trigger />
|
|
114
|
+
<Select.Content>
|
|
115
|
+
<Select.Item value="apple">Apple</Select.Item>
|
|
116
|
+
<Select.Item value="banana">Banana</Select.Item>
|
|
117
|
+
<Select.Item value="orange">Orange</Select.Item>
|
|
118
|
+
<Select.Item value="grape">Grape</Select.Item>
|
|
119
|
+
</Select.Content>
|
|
120
|
+
</StatefulSelect>
|
|
121
|
+
),
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'With Groups',
|
|
125
|
+
description: 'Options organized into groups',
|
|
126
|
+
render: () => (
|
|
127
|
+
<StatefulSelect placeholder="Select a country">
|
|
128
|
+
<Select.Trigger />
|
|
129
|
+
<Select.Content>
|
|
130
|
+
<Select.Group>
|
|
131
|
+
<Select.GroupLabel>North America</Select.GroupLabel>
|
|
132
|
+
<Select.Item value="us">United States</Select.Item>
|
|
133
|
+
<Select.Item value="ca">Canada</Select.Item>
|
|
134
|
+
<Select.Item value="mx">Mexico</Select.Item>
|
|
135
|
+
</Select.Group>
|
|
136
|
+
<Select.Group>
|
|
137
|
+
<Select.GroupLabel>Europe</Select.GroupLabel>
|
|
138
|
+
<Select.Item value="uk">United Kingdom</Select.Item>
|
|
139
|
+
<Select.Item value="de">Germany</Select.Item>
|
|
140
|
+
<Select.Item value="fr">France</Select.Item>
|
|
141
|
+
</Select.Group>
|
|
142
|
+
</Select.Content>
|
|
143
|
+
</StatefulSelect>
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'With Disabled Options',
|
|
148
|
+
description: 'Some options are disabled',
|
|
149
|
+
render: () => (
|
|
150
|
+
<StatefulSelect placeholder="Select a plan">
|
|
151
|
+
<Select.Trigger />
|
|
152
|
+
<Select.Content>
|
|
153
|
+
<Select.Item value="free">Free</Select.Item>
|
|
154
|
+
<Select.Item value="pro">Pro</Select.Item>
|
|
155
|
+
<Select.Item value="enterprise" disabled>Enterprise (Contact Sales)</Select.Item>
|
|
156
|
+
</Select.Content>
|
|
157
|
+
</StatefulSelect>
|
|
158
|
+
),
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'Disabled',
|
|
162
|
+
description: 'Disabled select',
|
|
163
|
+
render: () => (
|
|
164
|
+
<Select disabled placeholder="Select an option">
|
|
165
|
+
<Select.Trigger />
|
|
166
|
+
<Select.Content>
|
|
167
|
+
<Select.Item value="1">Option 1</Select.Item>
|
|
168
|
+
</Select.Content>
|
|
169
|
+
</Select>
|
|
170
|
+
),
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
@use '../../tokens/mixins' as *;
|
|
3
|
+
|
|
4
|
+
// Trigger button
|
|
5
|
+
.trigger {
|
|
6
|
+
@include button-reset;
|
|
7
|
+
@include interactive-base;
|
|
8
|
+
@include text-base;
|
|
9
|
+
|
|
10
|
+
display: inline-flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: space-between;
|
|
13
|
+
gap: var(--fui-space-2, $fui-space-2);
|
|
14
|
+
width: 100%;
|
|
15
|
+
min-width: 10rem;
|
|
16
|
+
height: var(--fui-input-height, $fui-input-height);
|
|
17
|
+
padding: 0 var(--fui-space-3, $fui-space-3);
|
|
18
|
+
background-color: var(--fui-bg-elevated, $fui-bg-elevated);
|
|
19
|
+
border: 1px solid var(--fui-border-strong, $fui-border-strong);
|
|
20
|
+
border-radius: var(--fui-radius-md, $fui-radius-md);
|
|
21
|
+
text-align: left;
|
|
22
|
+
|
|
23
|
+
&:hover:not([data-disabled]) {
|
|
24
|
+
border-color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
&[data-popup-open] {
|
|
28
|
+
border-color: var(--fui-color-accent, $fui-color-accent);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
&[data-disabled] {
|
|
32
|
+
background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
|
|
33
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Value display
|
|
38
|
+
.value {
|
|
39
|
+
flex: 1;
|
|
40
|
+
overflow: hidden;
|
|
41
|
+
text-overflow: ellipsis;
|
|
42
|
+
white-space: nowrap;
|
|
43
|
+
|
|
44
|
+
&[data-placeholder] {
|
|
45
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Chevron icon
|
|
50
|
+
.icon {
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
justify-content: center;
|
|
54
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
55
|
+
transition: transform var(--fui-transition-fast, $fui-transition-fast);
|
|
56
|
+
|
|
57
|
+
[data-popup-open] > & {
|
|
58
|
+
transform: rotate(180deg);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
svg {
|
|
62
|
+
width: 1rem;
|
|
63
|
+
height: 1rem;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Positioner
|
|
68
|
+
.positioner {
|
|
69
|
+
z-index: 50;
|
|
70
|
+
outline: none;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Popup container
|
|
74
|
+
.popup {
|
|
75
|
+
@include surface-elevated;
|
|
76
|
+
|
|
77
|
+
min-width: var(--anchor-width);
|
|
78
|
+
max-height: 20rem;
|
|
79
|
+
overflow-y: auto;
|
|
80
|
+
padding: var(--fui-space-1, $fui-space-1);
|
|
81
|
+
box-shadow: var(--fui-shadow-md, $fui-shadow-md);
|
|
82
|
+
|
|
83
|
+
// Animation
|
|
84
|
+
opacity: 0;
|
|
85
|
+
transform: scale(0.95);
|
|
86
|
+
transform-origin: var(--transform-origin);
|
|
87
|
+
transition:
|
|
88
|
+
opacity var(--fui-transition-fast, $fui-transition-fast),
|
|
89
|
+
transform var(--fui-transition-fast, $fui-transition-fast);
|
|
90
|
+
|
|
91
|
+
&[data-open] {
|
|
92
|
+
opacity: 1;
|
|
93
|
+
transform: scale(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
&[data-starting-style],
|
|
97
|
+
&[data-ending-style] {
|
|
98
|
+
opacity: 0;
|
|
99
|
+
transform: scale(0.95);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Individual option
|
|
104
|
+
.item {
|
|
105
|
+
@include button-reset;
|
|
106
|
+
@include text-base;
|
|
107
|
+
|
|
108
|
+
display: flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
gap: var(--fui-space-2, $fui-space-2);
|
|
111
|
+
width: 100%;
|
|
112
|
+
padding: var(--fui-space-2, $fui-space-2) var(--fui-space-3, $fui-space-3);
|
|
113
|
+
border-radius: var(--fui-radius-sm, $fui-radius-sm);
|
|
114
|
+
cursor: pointer;
|
|
115
|
+
outline: none;
|
|
116
|
+
|
|
117
|
+
&[data-highlighted] {
|
|
118
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
&[data-selected] {
|
|
122
|
+
background-color: var(--fui-color-accent, $fui-color-accent);
|
|
123
|
+
color: var(--fui-text-inverse, $fui-text-inverse);
|
|
124
|
+
|
|
125
|
+
&[data-highlighted] {
|
|
126
|
+
background-color: var(--fui-color-accent-hover, $fui-color-accent-hover);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
&[data-disabled] {
|
|
131
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
132
|
+
cursor: not-allowed;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Selection indicator (checkmark)
|
|
137
|
+
.itemIndicator {
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
justify-content: center;
|
|
141
|
+
width: 1rem;
|
|
142
|
+
height: 1rem;
|
|
143
|
+
margin-left: auto;
|
|
144
|
+
|
|
145
|
+
svg {
|
|
146
|
+
width: 0.875rem;
|
|
147
|
+
height: 0.875rem;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Group container
|
|
152
|
+
.group {
|
|
153
|
+
&:not(:first-child) {
|
|
154
|
+
margin-top: var(--fui-space-1, $fui-space-1);
|
|
155
|
+
padding-top: var(--fui-space-1, $fui-space-1);
|
|
156
|
+
border-top: 1px solid var(--fui-border, $fui-border);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Group label
|
|
161
|
+
.groupLabel {
|
|
162
|
+
padding: var(--fui-space-1, $fui-space-1) var(--fui-space-3, $fui-space-3);
|
|
163
|
+
font-size: var(--fui-font-size-xs, $fui-font-size-xs);
|
|
164
|
+
font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
|
|
165
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
166
|
+
text-transform: uppercase;
|
|
167
|
+
letter-spacing: 0.05em;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Arrow
|
|
171
|
+
.arrow {
|
|
172
|
+
width: 10px;
|
|
173
|
+
height: 10px;
|
|
174
|
+
transform: rotate(45deg);
|
|
175
|
+
background-color: var(--fui-bg-elevated, $fui-bg-elevated);
|
|
176
|
+
border: 1px solid var(--fui-border, $fui-border);
|
|
177
|
+
|
|
178
|
+
&[data-side='top'] {
|
|
179
|
+
border-top: none;
|
|
180
|
+
border-left: none;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
&[data-side='bottom'] {
|
|
184
|
+
border-bottom: none;
|
|
185
|
+
border-right: none;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Select as BaseSelect } from '@base-ui/react/select';
|
|
3
|
+
import styles from './Select.module.scss';
|
|
4
|
+
// Import globals to ensure CSS variables are defined
|
|
5
|
+
import '../../styles/globals.scss';
|
|
6
|
+
|
|
7
|
+
// ============================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================
|
|
10
|
+
|
|
11
|
+
export type SelectValue = string;
|
|
12
|
+
|
|
13
|
+
export interface SelectOption {
|
|
14
|
+
value: SelectValue;
|
|
15
|
+
label: string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SelectProps {
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
value?: SelectValue | null;
|
|
22
|
+
defaultValue?: SelectValue;
|
|
23
|
+
onValueChange?: (value: SelectValue | null) => void;
|
|
24
|
+
open?: boolean;
|
|
25
|
+
defaultOpen?: boolean;
|
|
26
|
+
onOpenChange?: (open: boolean) => void;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
required?: boolean;
|
|
29
|
+
name?: string;
|
|
30
|
+
placeholder?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SelectTriggerProps {
|
|
34
|
+
children?: React.ReactNode;
|
|
35
|
+
placeholder?: string;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SelectContentProps {
|
|
40
|
+
children: React.ReactNode;
|
|
41
|
+
className?: string;
|
|
42
|
+
sideOffset?: number;
|
|
43
|
+
align?: 'start' | 'center' | 'end';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SelectItemProps {
|
|
47
|
+
children: React.ReactNode;
|
|
48
|
+
value: SelectValue;
|
|
49
|
+
disabled?: boolean;
|
|
50
|
+
className?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SelectGroupProps {
|
|
54
|
+
children: React.ReactNode;
|
|
55
|
+
className?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface SelectGroupLabelProps {
|
|
59
|
+
children: React.ReactNode;
|
|
60
|
+
className?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================
|
|
64
|
+
// Icons
|
|
65
|
+
// ============================================
|
|
66
|
+
|
|
67
|
+
function ChevronDownIcon() {
|
|
68
|
+
return (
|
|
69
|
+
<svg
|
|
70
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
71
|
+
width="16"
|
|
72
|
+
height="16"
|
|
73
|
+
viewBox="0 0 24 24"
|
|
74
|
+
fill="none"
|
|
75
|
+
stroke="currentColor"
|
|
76
|
+
strokeWidth="2"
|
|
77
|
+
strokeLinecap="round"
|
|
78
|
+
strokeLinejoin="round"
|
|
79
|
+
aria-hidden="true"
|
|
80
|
+
>
|
|
81
|
+
<polyline points="6 9 12 15 18 9" />
|
|
82
|
+
</svg>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function CheckIcon() {
|
|
87
|
+
return (
|
|
88
|
+
<svg
|
|
89
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
90
|
+
width="14"
|
|
91
|
+
height="14"
|
|
92
|
+
viewBox="0 0 24 24"
|
|
93
|
+
fill="none"
|
|
94
|
+
stroke="currentColor"
|
|
95
|
+
strokeWidth="2.5"
|
|
96
|
+
strokeLinecap="round"
|
|
97
|
+
strokeLinejoin="round"
|
|
98
|
+
aria-hidden="true"
|
|
99
|
+
>
|
|
100
|
+
<polyline points="20 6 9 17 4 12" />
|
|
101
|
+
</svg>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ============================================
|
|
106
|
+
// Context for placeholder
|
|
107
|
+
// ============================================
|
|
108
|
+
|
|
109
|
+
const SelectContext = React.createContext<{ placeholder?: string }>({});
|
|
110
|
+
|
|
111
|
+
// ============================================
|
|
112
|
+
// Components
|
|
113
|
+
// ============================================
|
|
114
|
+
|
|
115
|
+
function SelectRoot({
|
|
116
|
+
children,
|
|
117
|
+
value,
|
|
118
|
+
defaultValue,
|
|
119
|
+
onValueChange,
|
|
120
|
+
open,
|
|
121
|
+
defaultOpen,
|
|
122
|
+
onOpenChange,
|
|
123
|
+
disabled,
|
|
124
|
+
required,
|
|
125
|
+
name,
|
|
126
|
+
placeholder,
|
|
127
|
+
}: SelectProps) {
|
|
128
|
+
return (
|
|
129
|
+
<SelectContext.Provider value={{ placeholder }}>
|
|
130
|
+
<BaseSelect.Root
|
|
131
|
+
value={value}
|
|
132
|
+
defaultValue={defaultValue}
|
|
133
|
+
onValueChange={onValueChange}
|
|
134
|
+
open={open}
|
|
135
|
+
defaultOpen={defaultOpen}
|
|
136
|
+
onOpenChange={onOpenChange}
|
|
137
|
+
disabled={disabled}
|
|
138
|
+
required={required}
|
|
139
|
+
name={name}
|
|
140
|
+
>
|
|
141
|
+
{children}
|
|
142
|
+
</BaseSelect.Root>
|
|
143
|
+
</SelectContext.Provider>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function SelectTrigger({ children, placeholder, className }: SelectTriggerProps) {
|
|
148
|
+
const context = React.useContext(SelectContext);
|
|
149
|
+
const placeholderText = placeholder ?? context.placeholder;
|
|
150
|
+
|
|
151
|
+
const classes = [styles.trigger, className].filter(Boolean).join(' ');
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<BaseSelect.Trigger className={classes}>
|
|
155
|
+
{children ?? (
|
|
156
|
+
<>
|
|
157
|
+
<BaseSelect.Value placeholder={placeholderText} className={styles.value} />
|
|
158
|
+
<BaseSelect.Icon className={styles.icon}>
|
|
159
|
+
<ChevronDownIcon />
|
|
160
|
+
</BaseSelect.Icon>
|
|
161
|
+
</>
|
|
162
|
+
)}
|
|
163
|
+
</BaseSelect.Trigger>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function SelectContent({
|
|
168
|
+
children,
|
|
169
|
+
className,
|
|
170
|
+
sideOffset = 4,
|
|
171
|
+
align = 'start',
|
|
172
|
+
}: SelectContentProps) {
|
|
173
|
+
const popupClasses = [styles.popup, className].filter(Boolean).join(' ');
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<BaseSelect.Portal>
|
|
177
|
+
<BaseSelect.Positioner
|
|
178
|
+
sideOffset={sideOffset}
|
|
179
|
+
align={align}
|
|
180
|
+
className={styles.positioner}
|
|
181
|
+
>
|
|
182
|
+
<BaseSelect.Popup className={popupClasses}>
|
|
183
|
+
{children}
|
|
184
|
+
</BaseSelect.Popup>
|
|
185
|
+
</BaseSelect.Positioner>
|
|
186
|
+
</BaseSelect.Portal>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function SelectItem({ children, value, disabled, className }: SelectItemProps) {
|
|
191
|
+
const classes = [styles.item, className].filter(Boolean).join(' ');
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<BaseSelect.Item value={value} disabled={disabled} className={classes}>
|
|
195
|
+
<BaseSelect.ItemText>{children}</BaseSelect.ItemText>
|
|
196
|
+
<BaseSelect.ItemIndicator className={styles.itemIndicator}>
|
|
197
|
+
<CheckIcon />
|
|
198
|
+
</BaseSelect.ItemIndicator>
|
|
199
|
+
</BaseSelect.Item>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function SelectGroup({ children, className }: SelectGroupProps) {
|
|
204
|
+
const classes = [styles.group, className].filter(Boolean).join(' ');
|
|
205
|
+
return <BaseSelect.Group className={classes}>{children}</BaseSelect.Group>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function SelectGroupLabel({ children, className }: SelectGroupLabelProps) {
|
|
209
|
+
const classes = [styles.groupLabel, className].filter(Boolean).join(' ');
|
|
210
|
+
return <BaseSelect.GroupLabel className={classes}>{children}</BaseSelect.GroupLabel>;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ============================================
|
|
214
|
+
// Export compound component
|
|
215
|
+
// ============================================
|
|
216
|
+
|
|
217
|
+
export const Select = Object.assign(SelectRoot, {
|
|
218
|
+
Trigger: SelectTrigger,
|
|
219
|
+
Content: SelectContent,
|
|
220
|
+
Item: SelectItem,
|
|
221
|
+
Group: SelectGroup,
|
|
222
|
+
GroupLabel: SelectGroupLabel,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Re-export individual components
|
|
226
|
+
export {
|
|
227
|
+
SelectRoot,
|
|
228
|
+
SelectTrigger,
|
|
229
|
+
SelectContent,
|
|
230
|
+
SelectItem,
|
|
231
|
+
SelectGroup,
|
|
232
|
+
SelectGroupLabel,
|
|
233
|
+
};
|